Skip to content

Commit 88bce03

Browse files
Merge pull request #2431 from mynameistito/fix/random-project-name-template
feat: add `{random}` / `{random:N}` project name placeholder's
2 parents d27360a + f2b1d6c commit 88bce03

6 files changed

Lines changed: 152 additions & 5 deletions

File tree

‎apps/desktop-gpui/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1039,7 +1039,7 @@ Settings-specific deviations:
10391039
| **No auth, so the free-plan variant** | There is no auth store here (same gap as the main window's plan badge), so the profile row shows the signed-out "Click to sign in" state and does nothing when clicked, and the Cap Pro section renders as it does for a free user: Instant Mode quality pinned to 720p, the other tiers inert. In the Tauri app clicking a locked tier raises an upgrade toast. `instantModeMaxResolution` is therefore displayed but never written. |
10401040
| **Selects are in-window menus** | `SelectSettingItem` and the excluded-windows Add button pop a real `NSMenu` via `Menu.popup()`. `ui::Menu` draws a menu-shaped panel at the pointer (which is where `popup()` with no argument puts it), with the same check marks, the same click-away dismiss, and the `KSelect` keyboard contract on top: arrows, Home/End, Enter, Escape. It does not flip or shift to stay inside the window, so a menu opened near the right edge is clipped by it — as it was before the consolidation. |
10411041
| **Text fields commit on the button, not on Return** | The project-name template and the server URL are `ui::TextInput::settings` — the same field as the search, differing only in its fills. Both are drafts committed by Save / Update, which is the Tauri card's own shape: neither `<input>` there binds `onKeyDown`, so Return does nothing in either app. Escape reverts to what is stored. The recordings folder is *not* a text field in either app — `pickRecordingsFolder` opens an `NSOpenPanel` and the path is a readout. |
1042-
| **The project-name preview is literal-only** | `commands.formatProjectName` understands `{moment:<format>}` and custom `{date:...}`/`{time:...}` formats through a moment-to-chrono translation. The preview here substitutes the six literal placeholders the card documents and leaves anything else alone — which is also what an unknown placeholder does there. |
1042+
| **The project-name preview is literal-only** | `commands.formatProjectName` understands `{moment:<format>}` and custom `{date:...}`/`{time:...}` formats through a moment-to-chrono translation. The preview here substitutes the documented placeholders and leaves any unsupported placeholder alone. `{random}` and `{random:N}` generate 10 or 1–32 lowercase hexadecimal characters in the recording name; the GPUI preview does not generate random values. |
10431043
| **Theme tiles keep a fixed height** | `aspect-[5/3]` has no gpui equivalent, so the three previews are 93px tall, the height they have at the window's default 782 width. Widen the window and they stay 93. |
10441044
| **`AccentColor` is macOS blue** | `--macos-settings-accent: AccentColor` resolves to the user's system accent; gpui exposes no query for it, so the checked toggles and the selected sidebar icon use `#007aff`. A user on a non-blue accent sees blue here and their own colour in the shipping app. |
10451045
| **No toggle bevel** | `.cap-toggle` carries `box-shadow: inset 0 1px 2px rgba(0,0,0,0.16)`; there is no inset-shadow hook in this gpui rev, so `ui::Toggle`'s track is flat on every surface. |

‎apps/desktop-gpui/src/recording.rs‎

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2591,15 +2591,51 @@ fn format_recording_project_name(
25912591
RecordingMode::Studio => ("Studio", "studio"),
25922592
RecordingMode::Instant => ("Instant", "instant"),
25932593
};
2594+
let template = replace_random_template_tokens(
2595+
template.unwrap_or(crate::store::DEFAULT_PROJECT_NAME_TEMPLATE),
2596+
);
25942597
let formatted = template
2595-
.unwrap_or(crate::store::DEFAULT_PROJECT_NAME_TEMPLATE)
25962598
.replace("{recording_mode}", recording_mode)
25972599
.replace("{mode}", mode)
25982600
.replace("{target_kind}", target_kind)
25992601
.replace("{target_name}", &target_name);
26002602
let formatted = replace_datetime_template_token(&formatted, "date", "%Y-%m-%d", datetime);
26012603
let formatted = replace_datetime_template_token(&formatted, "time", "%I:%M %p", datetime);
2602-
replace_datetime_template_token(&formatted, "moment", "%Y-%m-%d %H:%M", datetime)
2604+
let formatted =
2605+
replace_datetime_template_token(&formatted, "moment", "%Y-%m-%d %H:%M", datetime);
2606+
formatted
2607+
}
2608+
2609+
fn replace_random_template_tokens(input: &str) -> String {
2610+
let mut output = String::with_capacity(input.len());
2611+
let mut remaining = input;
2612+
2613+
while let Some(start) = remaining.find("{random") {
2614+
output.push_str(&remaining[..start]);
2615+
let candidate = &remaining[start..];
2616+
let Some(end) = candidate.find('}') else {
2617+
output.push_str(candidate);
2618+
return output;
2619+
};
2620+
let token = &candidate[..=end];
2621+
let length = if token == "{random}" {
2622+
Some(10)
2623+
} else {
2624+
token
2625+
.strip_prefix("{random:")
2626+
.and_then(|value| value.strip_suffix('}'))
2627+
.and_then(|value| value.parse::<usize>().ok())
2628+
};
2629+
if let Some(length @ 1..=32) = length {
2630+
let random = crate::store::new_uuid_v4().replace('-', "");
2631+
output.push_str(&random[..length]);
2632+
} else {
2633+
output.push_str(token);
2634+
}
2635+
remaining = &candidate[end + 1..];
2636+
}
2637+
output.push_str(remaining);
2638+
output
26032639
}
26042640

26052641
fn replace_datetime_template_token(
@@ -2712,6 +2748,49 @@ mod tests {
27122748
);
27132749
}
27142750

2751+
#[test]
2752+
fn recording_project_names_expand_filename_safe_random_tokens() {
2753+
let timestamp = chrono::Local
2754+
.with_ymd_and_hms(2026, 8, 25, 9, 15, 0)
2755+
.single()
2756+
.unwrap();
2757+
let name = format_recording_project_name(
2758+
Some("{random}-{random:16}-{random:0}"),
2759+
"Example Window",
2760+
"Window",
2761+
RecordingMode::Studio,
2762+
timestamp,
2763+
);
2764+
let parts = name.split('-').collect::<Vec<_>>();
2765+
2766+
assert_eq!(parts[0].len(), 10);
2767+
assert_eq!(parts[1].len(), 16);
2768+
assert!(
2769+
parts[..2]
2770+
.iter()
2771+
.all(|part| part.chars().all(|character| character.is_ascii_hexdigit()))
2772+
);
2773+
assert_eq!(parts[2], "{random:0}");
2774+
}
2775+
2776+
#[test]
2777+
fn recording_project_names_preserve_random_placeholder_in_window_title() {
2778+
let timestamp = chrono::Local
2779+
.with_ymd_and_hms(2026, 8, 25, 9, 15, 0)
2780+
.single()
2781+
.unwrap();
2782+
2783+
let name = format_recording_project_name(
2784+
None,
2785+
"Implement {random} placeholder",
2786+
"Window",
2787+
RecordingMode::Studio,
2788+
timestamp,
2789+
);
2790+
2791+
assert!(name.contains("Implement {random} placeholder"));
2792+
}
2793+
27152794
#[test]
27162795
fn window_title_with_windows_reserved_characters_creates_a_project_directory() {
27172796
let filename = project_bundle_filename(

‎apps/desktop-gpui/src/settings_window.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3782,6 +3782,11 @@ impl SettingsWindow {
37823782
("", "{target_name}", "Monitor name or window title."),
37833783
("Date & time", "{date}", "the recording's date"),
37843784
("", "{time}", "the recording's time"),
3785+
(
3786+
"Random",
3787+
"{random}",
3788+
"10 filename-safe hexadecimal characters; use {random:N} for 1–32.",
3789+
),
37853790
];
37863791

37873792
let body = div()

‎apps/desktop/src-tauri/src/recording.rs‎

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1867,6 +1867,7 @@ pub fn format_project_name<'a>(
18671867
static ref DATE_REGEX: Regex = Regex::new(r"\{date(?::([^}]+))?\}").unwrap();
18681868
static ref TIME_REGEX: Regex = Regex::new(r"\{time(?::([^}]+))?\}").unwrap();
18691869
static ref MOMENT_REGEX: Regex = Regex::new(r"\{moment(?::([^}]+))?\}").unwrap();
1870+
static ref RANDOM_REGEX: Regex = Regex::new(r"\{random(?::(\d+))?\}").unwrap();
18701871
static ref AC: aho_corasick::AhoCorasick = {
18711872
aho_corasick::AhoCorasick::new([
18721873
"{recording_mode}",
@@ -1878,6 +1879,19 @@ pub fn format_project_name<'a>(
18781879
};
18791880
}
18801881
let haystack = template.unwrap_or(DEFAULT_FILENAME_TEMPLATE);
1882+
let haystack = RANDOM_REGEX.replace_all(haystack, |caps: &regex::Captures| {
1883+
let length = match caps.get(1) {
1884+
Some(value) => match value.as_str().parse::<usize>() {
1885+
Ok(length) => length,
1886+
Err(_) => return caps.get(0).unwrap().as_str().to_owned(),
1887+
},
1888+
None => 10,
1889+
};
1890+
if !(1..=32).contains(&length) {
1891+
return caps.get(0).unwrap().as_str().to_owned();
1892+
}
1893+
uuid::Uuid::new_v4().simple().to_string()[..length].to_owned()
1894+
});
18811895

18821896
// Get recording mode information
18831897
let (recording_mode, mode) = match recording_mode {
@@ -1888,7 +1902,7 @@ pub fn format_project_name<'a>(
18881902

18891903
let result = AC
18901904
.try_replace_all(
1891-
haystack,
1905+
&haystack,
18921906
&[recording_mode, mode, target_kind, &truncated_target_name],
18931907
)
18941908
.expect("AhoCorasick replace should never fail with default configuration");
@@ -7075,8 +7089,48 @@ async fn emit_recording_started_telemetry(app: &AppHandle, state_mtx: &MutableSt
70757089
#[cfg(test)]
70767090
mod tests {
70777091
use super::*;
7092+
use chrono::TimeZone as _;
70787093
use tempfile::tempdir;
70797094

7095+
#[test]
7096+
fn project_name_random_placeholder_is_filename_safe_and_sized() {
7097+
let name = format_project_name(
7098+
Some("{random}-{random:16}-{random:0}"),
7099+
"Example Window",
7100+
"Window",
7101+
RecordingMode::Studio,
7102+
None,
7103+
);
7104+
let parts = name.split('-').collect::<Vec<_>>();
7105+
7106+
assert_eq!(parts[0].len(), 10);
7107+
assert_eq!(parts[1].len(), 16);
7108+
assert!(
7109+
parts[..2]
7110+
.iter()
7111+
.all(|part| part.chars().all(|character| character.is_ascii_hexdigit()))
7112+
);
7113+
assert_eq!(parts[2], "{random:0}");
7114+
}
7115+
7116+
#[test]
7117+
fn project_name_preserves_random_placeholder_in_window_title() {
7118+
let timestamp = chrono::Local
7119+
.with_ymd_and_hms(2026, 8, 25, 9, 15, 0)
7120+
.single()
7121+
.unwrap();
7122+
7123+
let name = format_project_name(
7124+
None,
7125+
"Implement {random} placeholder",
7126+
"Window",
7127+
RecordingMode::Studio,
7128+
Some(timestamp),
7129+
);
7130+
7131+
assert!(name.contains("Implement {random} placeholder"));
7132+
}
7133+
70807134
#[test]
70817135
fn requested_microphone_absence_is_an_error_not_an_empty_track() {
70827136
let error = selected_microphone_for_start(Some("Requested mic".into()), &[])

‎apps/desktop/src/routes/(window-chrome)/settings/general.tsx‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,6 +1178,15 @@ function DefaultProjectNameCard(props: {
11781178
{momentExample()}
11791179
</p>
11801180
</div>
1181+
1182+
<div class="space-y-1">
1183+
<p class="font-medium text-gray-12">Random</p>
1184+
<p>
1185+
<CodeView>{"{random}"}</CodeView> → 10 filename-safe
1186+
hexadecimal characters; use{" "}
1187+
<CodeView>{"{random:16}"}</CodeView> for 16 (1–32).
1188+
</p>
1189+
</div>
11811190
</Collapsible.Content>
11821191
</Collapsible>
11831192
</div>

‎apps/web/lib/messenger/constants.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ DESKTOP APP SETTINGS:
125125
- Crash-recoverable recording: on/off (fragments recording for recovery)
126126
- Max capture framerate: 30, 60, or 120 FPS
127127
- Automatically open shareable links (Pro): on/off
128-
- Default project name template with placeholders: {target_name}, {target_kind}, {date}, {time}, {recording_mode}, {mode}, {moment:...}
128+
- Default project name template with placeholders: {target_name}, {target_kind}, {date}, {time}, {recording_mode}, {mode}, {moment:...}, {random} or {random:N}
129129
- Excluded windows: hide specific windows from recordings (useful for hiding Cap itself or other tools)
130130
- Self-host server URL: point desktop app to a self-hosted Cap server
131131
- Configurable keyboard shortcuts for: screenshot (display/window/area), open recording picker, stop recording, restart recording, pause/resume recording, cycle recording mode, record display/window/area

0 commit comments

Comments
 (0)