mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
feat(ui): GUI localization for en and zh-Hans (#303)
* feat(ui): add GUI localization for en and zh-Hans
* feat(ui): localize search placeholders and relative time
* feat(ui): localize palette, switcher, and sftp strings
* feat(ui): localize home shortcut labels
* feat(ui): localize tray, ssh prompt, and editor strings
* feat(ui): add plural/select i18n helpers and localize sftp/settings labels
* feat(ui): localize settings search, forwards panel, and file tree
* feat(ui): localize code editor and right panel
* feat(ui): localize stop/delete workspace confirmations with plural support
* feat(ui): localize diff overlay with plural-aware summary
* feat(ui): localize pending pane, worktree prompt, and home time strings
* feat(ui): localize app menus, tray, tab strip/sidebar, and remote status strings
* feat(ui): localize switcher, file_tree, machine_mirror fallback strings
* feat(ui): localize ssh prompts, theme presets, host error wrapper, and finish remote strings
* feat(ui): localize command palette strings
* feat(ui): localize app.rs notifications, prompts, placeholders, and parse errors
* feat(ui): localize remaining theme, switcher, settings, and sftp strings
* style: cargo fmt
* feat(ui): add language selector to settings
* fix(ui): refresh locales across windows
* refactor(ui): make GUI language selection explicit
* fix(ui): localize Explorer settings after merge
* fix(ui): keep persisted theme names out of the GUI locale
A theme's name is data, not chrome: it is written into the theme YAML and
matched back with `trim_end_matches(" (custom)")`. Translating it meant a
Chinese GUI forked "Nord" into "Nord(自定义)", the next fork stacked a second
suffix on it, and the name stayed Chinese after switching back to English. The
derived-name fallback had the same problem. Both are English again.
Also in this pass:
- Give each test thread its own locale override. The locale is process-wide and
tests run in parallel, so the two tests that switched to zh-CN could flip the
language out from under another thread's English assertions.
- Rebuild the menu bar when gui_language changes in config.json, the way the
in-app picker already does — otherwise the menus kept the old language.
- Document the values the setting actually accepts. The docs still described
`auto` and `zh-Hans`, which sanitize() resets to `en`.
- Put the English words back into the Chinese search keywords for the language
setting; the other 58 keyword sets keep them.
- Drop the unused is_zh_hans helper.
---------
Co-authored-by: thomas <thomas@gmail.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
This commit is contained in:
co-authored by
thomas
l0ng-ai
parent
e8525e1b33
commit
8a342f2ca9
@@ -167,6 +167,10 @@ pub struct Config {
|
||||
/// package manager's copy — and do not want it shadowed.
|
||||
#[serde(default = "default_true")]
|
||||
pub install_cli_on_path: bool,
|
||||
/// GUI-only locale selection. Values: `en` or `zh-CN`.
|
||||
/// CLI output stays English so agent/script integrations are stable.
|
||||
#[serde(default = "default_gui_language")]
|
||||
pub gui_language: String,
|
||||
#[serde(default = "default_notify_threshold_secs")]
|
||||
pub notify_threshold_secs: u64,
|
||||
#[serde(default = "default_true")]
|
||||
@@ -409,6 +413,7 @@ impl Default for Config {
|
||||
notify_on_command_finish: NotifyMode::Unfocused,
|
||||
check_for_updates: true,
|
||||
install_cli_on_path: true,
|
||||
gui_language: default_gui_language(),
|
||||
notify_threshold_secs: default_notify_threshold_secs(),
|
||||
restore_session: true,
|
||||
show_tray_icon: true,
|
||||
@@ -496,6 +501,10 @@ impl Config {
|
||||
{
|
||||
self.link_file_command = None;
|
||||
}
|
||||
match self.gui_language.as_str() {
|
||||
"en" | "zh-CN" => {}
|
||||
_ => self.gui_language = default_gui_language(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&self) {
|
||||
@@ -647,6 +656,10 @@ fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_gui_language() -> String {
|
||||
"en".to_string()
|
||||
}
|
||||
|
||||
fn default_word_separators() -> String {
|
||||
",│`|:\"' ()[]{}<>\t".to_string()
|
||||
}
|
||||
@@ -1076,6 +1089,19 @@ mod tests {
|
||||
assert_eq!(clamp(100_000), 3600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gui_language_defaults_to_english_and_rejects_unsupported_values() {
|
||||
let cfg = Config::default();
|
||||
assert_eq!(cfg.gui_language, "en");
|
||||
|
||||
let cfg: Config = serde_json::from_str(r#"{"gui_language": "zh-CN"}"#).unwrap();
|
||||
assert_eq!(cfg.gui_language, "zh-CN");
|
||||
|
||||
let mut cfg: Config = serde_json::from_str(r#"{"gui_language": "ko"}"#).unwrap();
|
||||
cfg.sanitize();
|
||||
assert_eq!(cfg.gui_language, "en");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keybinding_preset_and_prefix_default_and_round_trip() {
|
||||
let cfg = Config::default();
|
||||
|
||||
@@ -120,3 +120,16 @@ a brief pause; `prefix` + an unbound key passes straight through.
|
||||
- The PTY is read at device speed and parsed in large batches, off the render path
|
||||
- Hot paths are lock-free — a big `cat` never waits on drawing
|
||||
- The daemon buffers up to 16 MiB ahead of the window before backpressure applies
|
||||
|
||||
## Localization
|
||||
|
||||
The GUI ships English and Simplified Chinese strings. Pick one in Settings →
|
||||
Appearance → Language, or in `config.json`:
|
||||
|
||||
```json
|
||||
{ "gui_language": "zh-CN" }
|
||||
```
|
||||
|
||||
`en` and `zh-CN` are the only accepted values; anything else falls back to `en`.
|
||||
The choice is explicit — the system language is never inferred. CLI output stays
|
||||
English so agent and script integrations keep a stable, predictable surface.
|
||||
|
||||
@@ -116,3 +116,15 @@ Aider、Amp、OpenCode 等约 17 个)并在其外围加功能 —— 绝不包
|
||||
- 以设备速度读取 PTY,在渲染路径之外成批解析
|
||||
- 热路径全程无锁 —— 再大的 `cat` 也不会阻塞在渲染上
|
||||
- 触发背压前,守护进程最多可领先窗口缓冲 16 MiB
|
||||
|
||||
## 本地化
|
||||
|
||||
GUI 目前提供英文和简体中文两套文案。在「设置 → 外观 → 语言」中选择,或直接改
|
||||
`config.json`:
|
||||
|
||||
```json
|
||||
{ "gui_language": "zh-CN" }
|
||||
```
|
||||
|
||||
只接受 `en` 和 `zh-CN` 两个值,其它值一律回落到 `en`。语言必须显式指定,不会
|
||||
去猜系统语言。CLI 输出保持英文,保证 agent、脚本和开发者工作流的输出稳定可预测。
|
||||
|
||||
+10
-1
@@ -74,10 +74,17 @@ fn spawn_config_watcher(cx: &mut App) {
|
||||
while rx.try_recv().is_ok() {}
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.set_global(Config::load());
|
||||
let config = Config::load();
|
||||
crate::ui::i18n::set_locale(&config.gui_language);
|
||||
cx.set_global(config);
|
||||
crate::ui::presets::load_registry(cx);
|
||||
crate::ui::theme::apply_cursor_hide_mode(cx);
|
||||
crate::ui::theme::apply_theme(None, cx);
|
||||
// The menu bar is built once from the current locale, so editing
|
||||
// gui_language by hand has to rebuild it the same way the
|
||||
// in-app language picker does.
|
||||
crate::ui::theme::set_menus(cx);
|
||||
crate::ui::windows::WindowRegistry::refresh_locale(cx, None);
|
||||
cx.refresh_windows();
|
||||
});
|
||||
}
|
||||
@@ -319,6 +326,7 @@ fn main() {
|
||||
return;
|
||||
}
|
||||
let config = crate::core::config::Config::load();
|
||||
let gui_language = config.gui_language.clone();
|
||||
|
||||
// After the PATH enrichment above, which is what makes the candidate scan
|
||||
// see the user's real PATH rather than the stub a Finder launch inherits —
|
||||
@@ -344,6 +352,7 @@ fn main() {
|
||||
cx.activate(true);
|
||||
#[cfg(target_os = "macos")]
|
||||
set_dock_icon_for_bare_binary();
|
||||
crate::ui::i18n::set_locale(&gui_language);
|
||||
cx.set_global(Config::load());
|
||||
crate::ui::theme::refresh_system_appearance(cx);
|
||||
crate::core::session::WorkspaceStore::init(cx);
|
||||
|
||||
+306
-152
@@ -27,13 +27,15 @@ use crate::core::window_state::{WindowGeometry as _, WindowState};
|
||||
use crate::daemon::protocol::{RemoteContext, ShellSpec, ssh_option_takes_value};
|
||||
use crate::terminal::view::{ChildExited, TerminalView};
|
||||
use crate::ui::host_registry::HostId;
|
||||
use crate::ui::i18n::{L10nKey, set_locale, t, t_fmt};
|
||||
use crate::ui::palette::{
|
||||
ChromeState, Command, CommandGroup, CommandKind, PaletteEvent, PaletteView,
|
||||
};
|
||||
use crate::ui::pane::{CloseOutcome, Dir, Pane, PaneSlot};
|
||||
use crate::ui::presets::Fill;
|
||||
use crate::ui::settings::{
|
||||
Recording, SettingsSection, SettingsState, ThemeEditor, humanize_action,
|
||||
ExplorerContextMenuNote, Recording, SettingsSection, SettingsState, ThemeEditor,
|
||||
humanize_action,
|
||||
};
|
||||
use crate::ui::theme::{apply_theme, set_menus, window_background};
|
||||
|
||||
@@ -479,27 +481,21 @@ impl Tty7App {
|
||||
};
|
||||
let ours = crate::daemon::protocol::PROTOCOL_VERSION;
|
||||
let detail = match mismatch.version {
|
||||
Some(v) => format!(
|
||||
"The server holding your shells is from another build \
|
||||
(v{}, protocol {} — this app speaks {}). You can keep using it and \
|
||||
your shells stay, but features whose wire format changed may \
|
||||
misbehave until it's restarted. Restarting starts a clean server: \
|
||||
tabs reopen with fresh shells and anything running in them is \
|
||||
terminated.",
|
||||
v.build, v.protocol, ours
|
||||
Some(v) => t_fmt(
|
||||
L10nKey::AppRestartServerMismatchDetail,
|
||||
&[
|
||||
("build", &v.build.to_string()),
|
||||
("protocol", &v.protocol.to_string()),
|
||||
("ours", &ours.to_string()),
|
||||
],
|
||||
),
|
||||
None => "The server holding your shells is from an older \
|
||||
version of the app. You can keep using it and your shells stay, \
|
||||
but newer features may misbehave until it's restarted. Restarting \
|
||||
starts a clean server: tabs reopen with fresh shells and anything \
|
||||
running in them is terminated."
|
||||
.to_string(),
|
||||
None => t(L10nKey::AppRestartServerOldDetail).to_string(),
|
||||
};
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Warning,
|
||||
"Restart Server?",
|
||||
t(L10nKey::AppRestartServerTitle),
|
||||
Some(&detail),
|
||||
&["Keep Shells", "Restart"],
|
||||
&[t(L10nKey::AppKeepShells), t(L10nKey::AppRestart)],
|
||||
cx,
|
||||
);
|
||||
cx.spawn(async move |this, cx| {
|
||||
@@ -560,7 +556,9 @@ impl Tty7App {
|
||||
let mf_bind_port = cx.new(|cx| InputState::new(window, cx).placeholder("8080"));
|
||||
let mf_target_host = cx.new(|cx| InputState::new(window, cx).placeholder("127.0.0.1"));
|
||||
let mf_target_port = cx.new(|cx| InputState::new(window, cx).placeholder("80"));
|
||||
let mf_description = cx.new(|cx| InputState::new(window, cx).placeholder("description"));
|
||||
let mf_description = cx.new(|cx| {
|
||||
InputState::new(window, cx).placeholder(t(L10nKey::AppPlaceholderDescription))
|
||||
});
|
||||
let sidebar_width = cx.global::<Config>().sidebar_width;
|
||||
let right_panel_width = cx.global::<Config>().right_panel_width;
|
||||
let right_panel_visible = cx.global::<Config>().right_panel_visible;
|
||||
@@ -627,14 +625,18 @@ impl Tty7App {
|
||||
},
|
||||
some => tabs_from_session(pane_ws.as_ref(), workspace, some, font_size, window, cx),
|
||||
};
|
||||
let sidebar_search = cx.new(|cx| InputState::new(window, cx).placeholder("Search tabs…"));
|
||||
let sidebar_search = cx.new(|cx| {
|
||||
InputState::new(window, cx).placeholder(t(crate::ui::i18n::L10nKey::SearchTabs))
|
||||
});
|
||||
let sidebar_search_sub =
|
||||
cx.subscribe_in(&sidebar_search, window, |_this, _i, ev, _w, cx| {
|
||||
if matches!(ev, InputEvent::Change) {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
let file_search = cx.new(|cx| InputState::new(window, cx).placeholder("Search files…"));
|
||||
let file_search = cx.new(|cx| {
|
||||
InputState::new(window, cx).placeholder(t(crate::ui::i18n::L10nKey::SearchFiles))
|
||||
});
|
||||
let file_search_sub = cx.subscribe_in(&file_search, window, |_this, _i, ev, _w, cx| {
|
||||
if matches!(ev, InputEvent::Change) {
|
||||
cx.notify();
|
||||
@@ -753,14 +755,12 @@ impl Tty7App {
|
||||
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Info,
|
||||
"Close Window?",
|
||||
Some(
|
||||
"Your sessions keep running in the background. This \
|
||||
workspace will be waiting on the home page, and in the \
|
||||
workspace menu in the title bar, the next time you open \
|
||||
tty7.",
|
||||
),
|
||||
&["Cancel", "Close"],
|
||||
t(crate::ui::i18n::L10nKey::CloseWindowTitle),
|
||||
Some(t(crate::ui::i18n::L10nKey::CloseWindowBody)),
|
||||
&[
|
||||
t(crate::ui::i18n::L10nKey::Cancel),
|
||||
t(crate::ui::i18n::L10nKey::Close),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
let close_confirmed = close_confirmed.clone();
|
||||
@@ -976,7 +976,7 @@ impl Tty7App {
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
window.push_notification("Could not reopen the tab: no terminal started", cx);
|
||||
window.push_notification(t(L10nKey::AppReopenTabFailed), cx);
|
||||
self.closed.push(st);
|
||||
return;
|
||||
};
|
||||
@@ -1105,14 +1105,12 @@ impl Tty7App {
|
||||
window.activate_window();
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Warning,
|
||||
"Quit and Stop Server?",
|
||||
Some(
|
||||
"This quits tty7 and stops the background server — anything \
|
||||
still running in your shells is terminated. Your tabs and \
|
||||
layout are kept and reopen with fresh shells next launch. \
|
||||
(Plain Quit keeps shells running.)",
|
||||
),
|
||||
&["Cancel", "Quit and Stop"],
|
||||
t(crate::ui::i18n::L10nKey::QuitStopServerTitle),
|
||||
Some(t(crate::ui::i18n::L10nKey::QuitStopServerBody)),
|
||||
&[
|
||||
t(crate::ui::i18n::L10nKey::Cancel),
|
||||
t(crate::ui::i18n::L10nKey::QuitAndStop),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
cx.spawn(async move |_this, cx| {
|
||||
@@ -1135,10 +1133,7 @@ impl Tty7App {
|
||||
let label = crate::ui::remote_connect::label_for(&target, cx);
|
||||
if !target.is_ssh() {
|
||||
window.push_notification(
|
||||
format!(
|
||||
"tty7 can only restart the server on machines it reaches over SSH. \
|
||||
{label} is served from this computer — stop its workspace instead."
|
||||
),
|
||||
t_fmt(L10nKey::AppRestartServerNotSsh, &[("label", &label)]),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
@@ -1149,13 +1144,9 @@ impl Tty7App {
|
||||
pub(crate) fn restart_daemon(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Warning,
|
||||
"Restart Server?",
|
||||
Some(
|
||||
"This stops every running shell on this computer — anything still \
|
||||
running in them will be terminated. Your tabs and layout are kept \
|
||||
and reopened with fresh shells.",
|
||||
),
|
||||
&["Cancel", "Restart"],
|
||||
t(L10nKey::AppRestartServerTitle),
|
||||
Some(t(L10nKey::AppRestartServerBody)),
|
||||
&[t(crate::ui::i18n::L10nKey::Cancel), t(L10nKey::AppRestart)],
|
||||
cx,
|
||||
);
|
||||
cx.spawn(async move |this, cx| {
|
||||
@@ -1538,17 +1529,29 @@ impl Tty7App {
|
||||
let seed_specs: [(ThemeEdit, &str, u32); 5] = [
|
||||
(
|
||||
ThemeEdit::Background,
|
||||
"Background",
|
||||
t(L10nKey::AppThemeColorBackground),
|
||||
theme.background_color(),
|
||||
),
|
||||
(ThemeEdit::Foreground, "Foreground", theme.foreground),
|
||||
(ThemeEdit::Accent, "Accent", theme.accent),
|
||||
(
|
||||
ThemeEdit::Foreground,
|
||||
t(L10nKey::AppThemeColorForeground),
|
||||
theme.foreground,
|
||||
),
|
||||
(
|
||||
ThemeEdit::Accent,
|
||||
t(L10nKey::AppThemeColorAccent),
|
||||
theme.accent,
|
||||
),
|
||||
(
|
||||
ThemeEdit::Cursor,
|
||||
"Cursor",
|
||||
t(L10nKey::AppThemeColorCursor),
|
||||
theme.caret.unwrap_or(theme.accent),
|
||||
),
|
||||
(ThemeEdit::Selection, "Selection", neutrals.selection),
|
||||
(
|
||||
ThemeEdit::Selection,
|
||||
t(L10nKey::AppThemeColorSelection),
|
||||
neutrals.selection,
|
||||
),
|
||||
];
|
||||
|
||||
let mut subs = Vec::new();
|
||||
@@ -1956,13 +1959,10 @@ impl Tty7App {
|
||||
crate::core::explorer_context_menu::unregister()
|
||||
};
|
||||
let note = match result {
|
||||
Ok(()) if register => {
|
||||
"Registered. Right-click a folder or folder background in Explorer to open it in tty7."
|
||||
.to_string()
|
||||
}
|
||||
Ok(()) => "Unregistered from Windows Explorer.".to_string(),
|
||||
Err(error) if register => format!("Could not register: {error}"),
|
||||
Err(error) => format!("Could not unregister: {error}"),
|
||||
Ok(()) if register => ExplorerContextMenuNote::Registered,
|
||||
Ok(()) => ExplorerContextMenuNote::Unregistered,
|
||||
Err(error) if register => ExplorerContextMenuNote::RegisterFailed(error.to_string()),
|
||||
Err(error) => ExplorerContextMenuNote::UnregisterFailed(error.to_string()),
|
||||
};
|
||||
let status =
|
||||
crate::core::explorer_context_menu::status().map_err(|error| error.to_string());
|
||||
@@ -2310,7 +2310,10 @@ impl Tty7App {
|
||||
Ok(view) => view,
|
||||
Err(e) => {
|
||||
log::error!("new tab spawn failed: {e}");
|
||||
window.push_notification(format!("Could not open a terminal: {e}"), cx);
|
||||
window.push_notification(
|
||||
t_fmt(L10nKey::AppOpenTerminalFailed, &[("error", &e.to_string())]),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -2339,7 +2342,13 @@ impl Tty7App {
|
||||
Ok(view) => view,
|
||||
Err(e) => {
|
||||
log::error!("native SSH spawn failed: {e}");
|
||||
window.push_notification(format!("SSH connection failed: {e}"), cx);
|
||||
window.push_notification(
|
||||
t_fmt(
|
||||
L10nKey::AppSshConnectionFailed,
|
||||
&[("error", &e.to_string())],
|
||||
),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -2366,7 +2375,10 @@ impl Tty7App {
|
||||
Ok(view) => view,
|
||||
Err(e) => {
|
||||
log::error!("native SSH respawn failed: {e}");
|
||||
window.push_notification(format!("SSH reconnect failed: {e}"), cx);
|
||||
window.push_notification(
|
||||
t_fmt(L10nKey::AppSshReconnectFailed, &[("error", &e.to_string())]),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -2403,7 +2415,13 @@ impl Tty7App {
|
||||
Ok(view) => PaneSlot::Ready(view),
|
||||
Err(e) => {
|
||||
log::error!("native SSH split spawn failed: {e}");
|
||||
window.push_notification(format!("SSH connection failed: {e}"), cx);
|
||||
window.push_notification(
|
||||
t_fmt(
|
||||
L10nKey::AppSshConnectionFailed,
|
||||
&[("error", &e.to_string())],
|
||||
),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -2422,7 +2440,10 @@ impl Tty7App {
|
||||
Ok(view) => view,
|
||||
Err(e) => {
|
||||
log::error!("split spawn failed: {e}");
|
||||
window.push_notification(format!("Could not split the pane: {e}"), cx);
|
||||
window.push_notification(
|
||||
t_fmt(L10nKey::AppSplitPaneFailed, &[("error", &e.to_string())]),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -2701,31 +2722,32 @@ impl Tty7App {
|
||||
},
|
||||
move |_this, found, cx| {
|
||||
let Some(wt) = found else { return };
|
||||
let path = wt.path.display().to_string();
|
||||
let detail = if wt.dirty {
|
||||
format!(
|
||||
"The closed tab's worktree at {} has uncommitted changes.",
|
||||
wt.path.display()
|
||||
)
|
||||
t_fmt(L10nKey::AppWorktreeRemoveDetailDirty, &[("path", &path)])
|
||||
} else {
|
||||
format!(
|
||||
"The closed tab's worktree at {} is clean.",
|
||||
wt.path.display()
|
||||
)
|
||||
t_fmt(L10nKey::AppWorktreeRemoveDetailClean, &[("path", &path)])
|
||||
};
|
||||
let title = format!("Remove worktree \"{}\"?", wt.branch);
|
||||
let title = t_fmt(L10nKey::AppWorktreeRemoveTitle, &[("branch", &wt.branch)]);
|
||||
let level = if wt.dirty {
|
||||
PromptLevel::Warning
|
||||
} else {
|
||||
PromptLevel::Info
|
||||
};
|
||||
let remove_label = if wt.dirty {
|
||||
"Discard Changes & Remove"
|
||||
t(L10nKey::AppWorktreeDiscardAndRemove)
|
||||
} else {
|
||||
"Remove Worktree"
|
||||
t(L10nKey::AppWorktreeRemove)
|
||||
};
|
||||
cx.spawn(async move |this, cx| {
|
||||
let Ok(answer) = this.update_in(cx, |_, window, cx| {
|
||||
window.prompt(level, &title, Some(&detail), &["Keep", remove_label], cx)
|
||||
window.prompt(
|
||||
level,
|
||||
&title,
|
||||
Some(&detail),
|
||||
&[t(L10nKey::AppWorktreeKeep), remove_label],
|
||||
cx,
|
||||
)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
@@ -2742,11 +2764,16 @@ impl Tty7App {
|
||||
move |h| crate::core::worktree::remove(h, &wt, force),
|
||||
move |_this, result, window, cx| match result {
|
||||
Ok(()) => window.push_notification(
|
||||
format!("Removed worktree \"{branch}\""),
|
||||
t_fmt(L10nKey::AppWorktreeRemoved, &[("branch", &branch)]),
|
||||
cx,
|
||||
),
|
||||
Err(e) => window.push_notification(
|
||||
t_fmt(
|
||||
L10nKey::AppWorktreeRemoveFailed,
|
||||
&[("error", &e.to_string())],
|
||||
),
|
||||
cx,
|
||||
),
|
||||
Err(e) => window
|
||||
.push_notification(format!("Worktree removal failed: {e}"), cx),
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -2889,13 +2916,16 @@ impl Tty7App {
|
||||
Ok(view) => view,
|
||||
Err(e) => {
|
||||
log::error!("fork spawn failed: {e}");
|
||||
window.push_notification(format!("Could not open a terminal: {e}"), cx);
|
||||
window.push_notification(
|
||||
t_fmt(L10nKey::AppOpenTerminalFailed, &[("error", &e.to_string())]),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some(terminal) = new.terminal() else {
|
||||
log::error!("fork spawn produced a pane that is still connecting");
|
||||
window.push_notification("Could not fork: the pane is still connecting", cx);
|
||||
window.push_notification(t(L10nKey::AppForkStillConnecting), cx);
|
||||
return;
|
||||
};
|
||||
terminal.read(cx).run_command_line(&cmd);
|
||||
@@ -2961,38 +2991,32 @@ impl Tty7App {
|
||||
let view = source.read(cx);
|
||||
let (agent, session, remote) = (view.agent(), view.agent_session(), view.remote_context());
|
||||
let Some(agent) = agent else {
|
||||
window.push_notification("This pane isn't running a coding agent", cx);
|
||||
window.push_notification(t(L10nKey::AppPaneNoCodingAgent), cx);
|
||||
return None;
|
||||
};
|
||||
let name = agent.display_name();
|
||||
if agent.fork_label().is_none() {
|
||||
window.push_notification(format!("tty7 has no fork command for {name}"), cx);
|
||||
window.push_notification(t_fmt(L10nKey::AppForkNoCommand, &[("name", &name)]), cx);
|
||||
return None;
|
||||
}
|
||||
if remote.is_some() {
|
||||
window.push_notification(
|
||||
format!("{name} sessions can only be forked from a local pane"),
|
||||
cx,
|
||||
);
|
||||
window.push_notification(t_fmt(L10nKey::AppForkLocalOnly, &[("name", &name)]), cx);
|
||||
return None;
|
||||
}
|
||||
let session = session.unwrap_or_default();
|
||||
let Some(id) = session.session_id.as_deref() else {
|
||||
window.push_notification(
|
||||
format!("tty7 hasn't seen a {name} session id in this pane — install its hooks in Settings → Agents"),
|
||||
cx,
|
||||
);
|
||||
window.push_notification(t_fmt(L10nKey::AppForkNoSessionId, &[("name", &name)]), cx);
|
||||
return None;
|
||||
};
|
||||
let Some(cmd) = agent.fork_command(id, session.launch_argv.as_deref()) else {
|
||||
window.push_notification(format!("{name}'s session id isn't a plain token"), cx);
|
||||
window.push_notification(
|
||||
t_fmt(L10nKey::AppForkSessionIdNotToken, &[("name", &name)]),
|
||||
cx,
|
||||
);
|
||||
return None;
|
||||
};
|
||||
if session.status == AgentStatus::Working {
|
||||
window.push_notification(
|
||||
format!("{name} is mid-turn — the fork won't include the turn in flight"),
|
||||
cx,
|
||||
);
|
||||
window.push_notification(t_fmt(L10nKey::AppForkMidTurn, &[("name", &name)]), cx);
|
||||
}
|
||||
Some(cmd)
|
||||
}
|
||||
@@ -3038,7 +3062,7 @@ impl Tty7App {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some((host, cwd)) = self.tab_host_cwd(index, window, cx) else {
|
||||
window.push_notification("This tab has no working directory yet", cx);
|
||||
window.push_notification(t(L10nKey::AppTabNoWorkingDirectory), cx);
|
||||
return;
|
||||
};
|
||||
let sheet_host = host.clone();
|
||||
@@ -3050,7 +3074,10 @@ impl Tty7App {
|
||||
move |h| crate::core::worktree::defaults(h, &probe_cwd),
|
||||
move |this, result, window, cx| match result {
|
||||
Ok(defaults) => this.open_worktree_prompt(sheet_host, cwd, defaults, window, cx),
|
||||
Err(e) => window.push_notification(format!("New worktree failed: {e}"), cx),
|
||||
Err(e) => window.push_notification(
|
||||
t_fmt(L10nKey::AppNewWorktreeFailed, &[("error", &e.to_string())]),
|
||||
cx,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -3074,7 +3101,10 @@ impl Tty7App {
|
||||
Ok(view) => view,
|
||||
Err(e) => {
|
||||
log::error!("worktree tab spawn failed: {e}");
|
||||
window.push_notification(format!("Could not open a terminal: {e}"), cx);
|
||||
window.push_notification(
|
||||
t_fmt(L10nKey::AppOpenTerminalFailed, &[("error", &e.to_string())]),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -3214,7 +3244,7 @@ impl Tty7App {
|
||||
};
|
||||
commands.push(
|
||||
Command::new(
|
||||
format!("SSH: {title}"),
|
||||
t_fmt(L10nKey::AppCmdSshProfileTitle, &[("title", &title)]),
|
||||
CommandKind::ConnectSavedProfile(p.id),
|
||||
)
|
||||
.with_subtitle(subtitle)
|
||||
@@ -3229,7 +3259,7 @@ impl Tty7App {
|
||||
let label = self.tab_label(tab, i, None, cx);
|
||||
commands.push(
|
||||
Command::new(
|
||||
format!("Switch to Tab: {label}"),
|
||||
t_fmt(L10nKey::AppCmdSwitchToTab, &[("label", &label)]),
|
||||
CommandKind::ActivateTab(i),
|
||||
)
|
||||
.in_group(CommandGroup::TabsPanes),
|
||||
@@ -3430,10 +3460,7 @@ impl Tty7App {
|
||||
|
||||
fn deliver_agent_prompt(&mut self, prompt: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(target) = self.agent_target_leaf(cx) else {
|
||||
crate::terminal::notify_desktop(
|
||||
Some("tty7"),
|
||||
"No running coding agent found — start one (claude, codex, …) in a pane first.",
|
||||
);
|
||||
crate::terminal::notify_desktop(Some("tty7"), t(L10nKey::AppNoRunningCodingAgent));
|
||||
return;
|
||||
};
|
||||
target.read(cx).send_agent_prompt(prompt);
|
||||
@@ -3456,10 +3483,7 @@ impl Tty7App {
|
||||
None => (None, None),
|
||||
};
|
||||
let Some(selection) = selection else {
|
||||
crate::terminal::notify_desktop(
|
||||
Some("tty7"),
|
||||
"Nothing selected — select some terminal output first.",
|
||||
);
|
||||
crate::terminal::notify_desktop(Some("tty7"), t(L10nKey::AppNothingSelected));
|
||||
return;
|
||||
};
|
||||
let cwd = cwd.map(|c| c.to_string_lossy().into_owned());
|
||||
@@ -3480,7 +3504,7 @@ impl Tty7App {
|
||||
Some((view.host(cx)?, view.host_cwd()?))
|
||||
});
|
||||
let Some((host, cwd)) = target else {
|
||||
crate::terminal::notify_desktop(Some("tty7"), "This pane has no known directory.");
|
||||
crate::terminal::notify_desktop(Some("tty7"), t(L10nKey::AppPaneNoKnownDirectory));
|
||||
return;
|
||||
};
|
||||
crate::ui::host_ops::HostOps::run_in(
|
||||
@@ -3503,7 +3527,7 @@ impl Tty7App {
|
||||
Some(prompt) => this.deliver_agent_prompt(&prompt, window, cx),
|
||||
None => crate::terminal::notify_desktop(
|
||||
Some("tty7"),
|
||||
&format!("No uncommitted changes in {cwd_s} (or not a git repository)."),
|
||||
&t_fmt(L10nKey::AppNoUncommittedChanges, &[("cwd", &cwd_s)]),
|
||||
),
|
||||
}
|
||||
},
|
||||
@@ -3520,12 +3544,15 @@ impl Tty7App {
|
||||
let mut subs = Vec::new();
|
||||
let (font_select, font_bold_select, font_italic_select) =
|
||||
self.build_font_selects(&mut subs, window, cx);
|
||||
let language_select = self.build_language_select(&mut subs, window, cx);
|
||||
let (shell_program_input, shell_args_input, wd_path_input) =
|
||||
self.build_shell_inputs(&mut subs, window, cx);
|
||||
let link_file_command_input = self.build_link_file_command_input(&mut subs, window, cx);
|
||||
let scroll_slider = self.build_scroll_slider(&mut subs, window, cx);
|
||||
let window_opacity_slider = self.build_window_opacity_slider(&mut subs, window, cx);
|
||||
let theme_search = cx.new(|cx| InputState::new(window, cx).placeholder("Search themes…"));
|
||||
let theme_search = cx.new(|cx| {
|
||||
InputState::new(window, cx).placeholder(t(crate::ui::i18n::L10nKey::SearchThemes))
|
||||
});
|
||||
subs.push(
|
||||
cx.subscribe_in(&theme_search, window, |_this, _i, ev, _w, cx| {
|
||||
if matches!(ev, InputEvent::Change) {
|
||||
@@ -3533,8 +3560,9 @@ impl Tty7App {
|
||||
}
|
||||
}),
|
||||
);
|
||||
let settings_search =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("Search settings…"));
|
||||
let settings_search = cx.new(|cx| {
|
||||
InputState::new(window, cx).placeholder(t(crate::ui::i18n::L10nKey::SearchSettings))
|
||||
});
|
||||
subs.push(
|
||||
cx.subscribe_in(&settings_search, window, |this, _i, ev, _w, cx| {
|
||||
if matches!(ev, InputEvent::Change) {
|
||||
@@ -3544,7 +3572,9 @@ impl Tty7App {
|
||||
}),
|
||||
);
|
||||
|
||||
let ssh_filter = cx.new(|cx| InputState::new(window, cx).placeholder("Filter hosts…"));
|
||||
let ssh_filter = cx.new(|cx| {
|
||||
InputState::new(window, cx).placeholder(t(crate::ui::i18n::L10nKey::FilterHosts))
|
||||
});
|
||||
subs.push(
|
||||
cx.subscribe_in(&ssh_filter, window, |_this, _i, ev, _w, cx| {
|
||||
if matches!(ev, InputEvent::Change) {
|
||||
@@ -3553,8 +3583,9 @@ impl Tty7App {
|
||||
}),
|
||||
);
|
||||
|
||||
let ssh_quick_connect =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("user@host or user@host:port"));
|
||||
let ssh_quick_connect = cx.new(|cx| {
|
||||
InputState::new(window, cx).placeholder(t(L10nKey::AppPlaceholderSshQuickConnect))
|
||||
});
|
||||
subs.push(
|
||||
cx.subscribe_in(&ssh_quick_connect, window, |_this, _i, ev, _w, cx| {
|
||||
if matches!(ev, InputEvent::Change) {
|
||||
@@ -3570,6 +3601,7 @@ impl Tty7App {
|
||||
font_select,
|
||||
font_bold_select,
|
||||
font_italic_select,
|
||||
language_select,
|
||||
shell_program_input,
|
||||
shell_args_input,
|
||||
wd_path_input,
|
||||
@@ -3646,7 +3678,7 @@ impl Tty7App {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>| {
|
||||
let mut rows = Vec::with_capacity(names.len() + 1);
|
||||
rows.push(crate::ui::settings::FONT_DEFAULT_LABEL.to_string());
|
||||
rows.push(crate::ui::settings::font_default_label().to_string());
|
||||
rows.extend(names.iter().cloned());
|
||||
let selected = value
|
||||
.as_ref()
|
||||
@@ -3694,6 +3726,117 @@ impl Tty7App {
|
||||
(font_select, font_bold_select, font_italic_select)
|
||||
}
|
||||
|
||||
fn build_language_select(
|
||||
&mut self,
|
||||
subs: &mut Vec<Subscription>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<SelectState<SearchableVec<String>>> {
|
||||
const CODES: &[&str] = &["en", "zh-CN"];
|
||||
let labels = || {
|
||||
vec![
|
||||
t(L10nKey::SettingsLanguageEnglish).to_string(),
|
||||
t(L10nKey::SettingsLanguageChinese).to_string(),
|
||||
]
|
||||
};
|
||||
let cfg = cx.global::<Config>();
|
||||
let current = Self::normalize_gui_language(&cfg.gui_language);
|
||||
let rows = labels();
|
||||
let selected = CODES.iter().position(|c| *c == current).unwrap_or(0);
|
||||
let language_select = cx.new(|cx| {
|
||||
SelectState::new(
|
||||
SearchableVec::new(rows),
|
||||
Some(IndexPath::default().row(selected)),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
subs.push(cx.subscribe_in(
|
||||
&language_select,
|
||||
window,
|
||||
move |this, _select, ev: &SelectEvent<SearchableVec<String>>, window, cx| {
|
||||
if let SelectEvent::Confirm(Some(label)) = ev {
|
||||
let rows = labels();
|
||||
if let Some(idx) = rows.iter().position(|r| r == label) {
|
||||
this.set_gui_language(CODES[idx], window, cx);
|
||||
}
|
||||
}
|
||||
},
|
||||
));
|
||||
language_select
|
||||
}
|
||||
|
||||
fn normalize_gui_language(code: &str) -> &'static str {
|
||||
match code {
|
||||
"zh-CN" => "zh-CN",
|
||||
_ => "en",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_gui_language(
|
||||
&mut self,
|
||||
code: &'static str,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let code = Self::normalize_gui_language(code);
|
||||
{
|
||||
let cfg = cx.global_mut::<Config>();
|
||||
cfg.gui_language = code.to_string();
|
||||
}
|
||||
set_locale(code);
|
||||
cx.global::<Config>().save();
|
||||
set_menus(cx);
|
||||
self.refresh_locale_state(window, cx);
|
||||
crate::ui::windows::WindowRegistry::refresh_locale(cx, Some(self.workspace));
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_locale_state(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
const CODES: &[&str] = &["en", "zh-CN"];
|
||||
self.sidebar_search.update(cx, |state, cx| {
|
||||
state.set_placeholder(t(L10nKey::SearchTabs), window, cx)
|
||||
});
|
||||
self.file_search.update(cx, |state, cx| {
|
||||
state.set_placeholder(t(L10nKey::SearchFiles), window, cx)
|
||||
});
|
||||
if let Some(s) = self.active_settings() {
|
||||
let rows = vec![
|
||||
t(L10nKey::SettingsLanguageEnglish).to_string(),
|
||||
t(L10nKey::SettingsLanguageChinese).to_string(),
|
||||
];
|
||||
s.language_select.update(cx, |state, cx| {
|
||||
state.set_items(SearchableVec::new(rows), window, cx);
|
||||
let code = Self::normalize_gui_language(&cx.global::<Config>().gui_language);
|
||||
let selected = CODES.iter().position(|c| *c == code).unwrap_or(0);
|
||||
state.set_selected_index(Some(IndexPath::default().row(selected)), window, cx);
|
||||
});
|
||||
s.search.update(cx, |state, cx| {
|
||||
state.set_placeholder(t(L10nKey::SearchSettings), window, cx)
|
||||
});
|
||||
s.theme_search.update(cx, |state, cx| {
|
||||
state.set_placeholder(t(L10nKey::SearchThemes), window, cx)
|
||||
});
|
||||
s.ssh_filter.update(cx, |state, cx| {
|
||||
state.set_placeholder(t(L10nKey::FilterHosts), window, cx)
|
||||
});
|
||||
s.ssh_quick_connect.update(cx, |state, cx| {
|
||||
state.set_placeholder(t(L10nKey::AppPlaceholderSshQuickConnect), window, cx)
|
||||
});
|
||||
s.shell_args_input.update(cx, |state, cx| {
|
||||
state.set_placeholder(t(L10nKey::AppPlaceholderNone), window, cx)
|
||||
});
|
||||
if !cfg!(windows) {
|
||||
s.shell_program_input.update(cx, |state, cx| {
|
||||
state.set_placeholder(t(L10nKey::AppPlaceholderLoginShell), window, cx)
|
||||
});
|
||||
}
|
||||
s.link_file_command_input.update(cx, |state, cx| {
|
||||
state.set_placeholder(t(L10nKey::AppPlaceholderOpenInDefaultApp), window, cx)
|
||||
});
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn build_shell_inputs(
|
||||
&mut self,
|
||||
subs: &mut Vec<Subscription>,
|
||||
@@ -3709,7 +3852,7 @@ impl Tty7App {
|
||||
let platform_default = if cfg!(windows) {
|
||||
"PowerShell"
|
||||
} else {
|
||||
"login shell"
|
||||
t(L10nKey::AppPlaceholderLoginShell)
|
||||
};
|
||||
let shell_program_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
@@ -3718,7 +3861,7 @@ impl Tty7App {
|
||||
});
|
||||
let shell_args_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("none")
|
||||
.placeholder(t(L10nKey::AppPlaceholderNone))
|
||||
.default_value(shell_args)
|
||||
});
|
||||
let wd_path_input = cx.new(|cx| {
|
||||
@@ -3767,7 +3910,7 @@ impl Tty7App {
|
||||
.unwrap_or_default();
|
||||
let input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("open in default app")
|
||||
.placeholder(t(L10nKey::AppPlaceholderOpenInDefaultApp))
|
||||
.default_value(value)
|
||||
});
|
||||
subs.push(
|
||||
@@ -3927,7 +4070,7 @@ impl Tty7App {
|
||||
}
|
||||
|
||||
fn commit_font_family_emphasis(&mut self, bold: bool, name: String, cx: &mut Context<Self>) {
|
||||
let family = (name != crate::ui::settings::FONT_DEFAULT_LABEL).then_some(name);
|
||||
let family = (name != crate::ui::settings::font_default_label()).then_some(name);
|
||||
for tab in &self.tabs {
|
||||
for leaf in tab.pane.terminals() {
|
||||
let family = family.clone();
|
||||
@@ -4270,7 +4413,7 @@ impl Tty7App {
|
||||
use crate::ui::settings::AgentHooksMachine;
|
||||
let mut out = vec![AgentHooksMachine {
|
||||
host: crate::ui::host_ops::HostId::LOCAL,
|
||||
label: "This Computer".to_string(),
|
||||
label: t(L10nKey::AppAgentHooksThisComputer).to_string(),
|
||||
}];
|
||||
let configured = crate::ui::remote_connect::available_hosts(cx);
|
||||
for id in crate::ui::host_registry::HostRegistry::ids(cx) {
|
||||
@@ -4281,7 +4424,7 @@ impl Tty7App {
|
||||
.iter()
|
||||
.find(|h| h.target.host_id() == id)
|
||||
.map(|h| h.label.clone())
|
||||
.unwrap_or_else(|| "Remote machine".to_string());
|
||||
.unwrap_or_else(|| t(L10nKey::AppAgentHooksRemoteMachine).to_string());
|
||||
out.push(AgentHooksMachine { host: id, label });
|
||||
}
|
||||
out
|
||||
@@ -4332,8 +4475,7 @@ impl Tty7App {
|
||||
};
|
||||
let Some((host, home)) = self.agent_hooks_link(host_id, cx) else {
|
||||
if let Some(s) = self.settings.as_mut() {
|
||||
s.agent_hooks_states =
|
||||
AgentHooksView::Unavailable(Self::AGENT_HOOKS_OFFLINE.into());
|
||||
s.agent_hooks_states = AgentHooksView::Unavailable(Self::agent_hooks_offline_msg());
|
||||
}
|
||||
cx.notify();
|
||||
return;
|
||||
@@ -4365,9 +4507,7 @@ impl Tty7App {
|
||||
s.agent_hooks_states = match rows {
|
||||
Some(rows) => AgentHooksView::Ready(rows),
|
||||
None => AgentHooksView::Unavailable(
|
||||
"tty7 could not work out this computer's home directory, so there is \
|
||||
nowhere to install to."
|
||||
.into(),
|
||||
t(L10nKey::AppAgentHooksNoHomeDir).to_string(),
|
||||
),
|
||||
};
|
||||
cx.notify();
|
||||
@@ -4376,10 +4516,9 @@ impl Tty7App {
|
||||
);
|
||||
}
|
||||
|
||||
const AGENT_HOOKS_OFFLINE: &'static str = concat!(
|
||||
"Not connected to this machine, so its agent config can't be read or ",
|
||||
"written. Open a workspace on it and come back."
|
||||
);
|
||||
fn agent_hooks_offline_msg() -> String {
|
||||
t(L10nKey::AppAgentHooksOffline).to_string()
|
||||
}
|
||||
|
||||
fn agent_hooks_link(
|
||||
&self,
|
||||
@@ -4426,9 +4565,9 @@ impl Tty7App {
|
||||
};
|
||||
let Some((host, home)) = self.agent_hooks_link(host_id, cx) else {
|
||||
if let Some(s) = self.settings.as_mut() {
|
||||
s.agent_hooks_note = Some((agent, Self::AGENT_HOOKS_OFFLINE.to_string()));
|
||||
s.agent_hooks_note = Some((agent, Self::agent_hooks_offline_msg()));
|
||||
s.agent_hooks_states = crate::ui::settings::AgentHooksView::Unavailable(
|
||||
Self::AGENT_HOOKS_OFFLINE.into(),
|
||||
Self::agent_hooks_offline_msg(),
|
||||
);
|
||||
}
|
||||
cx.notify();
|
||||
@@ -4441,8 +4580,9 @@ impl Tty7App {
|
||||
move |h| {
|
||||
let target = match &home {
|
||||
Some(home) => HookTarget::remote(h, home.clone()),
|
||||
None => HookTarget::local(h)
|
||||
.ok_or_else(|| anyhow::anyhow!("cannot resolve home directory"))?,
|
||||
None => HookTarget::local(h).ok_or_else(|| {
|
||||
anyhow::anyhow!("{}", t(L10nKey::AppAgentHooksHomeDirUnresolved))
|
||||
})?,
|
||||
};
|
||||
if install {
|
||||
crate::core::agent_hooks::install_hooks(&target, agent)
|
||||
@@ -4456,7 +4596,9 @@ impl Tty7App {
|
||||
agent,
|
||||
match result {
|
||||
Ok(summary) => summary,
|
||||
Err(e) => format!("Failed: {e}"),
|
||||
Err(e) => {
|
||||
t_fmt(L10nKey::AppAgentHooksOpFailed, &[("error", &e.to_string())])
|
||||
}
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -4600,10 +4742,12 @@ impl Tty7App {
|
||||
.find(|(a, k)| *k == spec && *a != action)
|
||||
.map(|(a, _)| a);
|
||||
let note = displaced.as_ref().map(|other| {
|
||||
format!(
|
||||
"{} took the shortcut from {}, which is now unset.",
|
||||
humanize_action(&action),
|
||||
humanize_action(other)
|
||||
t_fmt(
|
||||
L10nKey::AppKeybindingDisplacedNote,
|
||||
&[
|
||||
("action", &humanize_action(&action)),
|
||||
("previous", &humanize_action(other)),
|
||||
],
|
||||
)
|
||||
});
|
||||
self.update_config(cx, |cfg| {
|
||||
@@ -5630,7 +5774,7 @@ pub(crate) fn new_terminal(
|
||||
.workspace
|
||||
.as_ref()
|
||||
.map(|w| w.target.to_string())
|
||||
.unwrap_or_else(|| "the local server".to_string());
|
||||
.unwrap_or_else(|| t(L10nKey::AppLocalServerName).to_string());
|
||||
let pending = cx.new(|cx| crate::ui::pending_pane::PendingPane::new(machine, spawn, cx));
|
||||
cx.subscribe_in(
|
||||
&pending,
|
||||
@@ -5796,7 +5940,7 @@ pub(crate) fn parse_ssh_connect_input(input: &str) -> Result<ParsedSshConnect, S
|
||||
use crate::core::ssh_profile::{SshProfile, parse_quick_connect};
|
||||
|
||||
let mut words = parse_ssh_option_words(input)
|
||||
.map_err(|_| "Unbalanced quotes in the SSH command".to_string())?;
|
||||
.map_err(|_| t(L10nKey::AppSshParseUnbalancedQuotes).to_string())?;
|
||||
if words.first().is_some_and(|word| word == "ssh") {
|
||||
words.remove(0);
|
||||
}
|
||||
@@ -5811,7 +5955,7 @@ pub(crate) fn parse_ssh_connect_input(input: &str) -> Result<ParsedSshConnect, S
|
||||
while i < words.len() {
|
||||
let word = words[i].clone();
|
||||
if word == "--" {
|
||||
return Err("Remote commands aren't supported here".to_string());
|
||||
return Err(t(L10nKey::AppSshParseNoRemoteCommands).to_string());
|
||||
}
|
||||
if let Some((flag, attached)) = ssh_short_flag(&word) {
|
||||
let value = if ssh_option_takes_value(flag) {
|
||||
@@ -5821,7 +5965,12 @@ pub(crate) fn parse_ssh_connect_input(input: &str) -> Result<ParsedSshConnect, S
|
||||
i += 1;
|
||||
match words.get(i) {
|
||||
Some(v) => v.clone(),
|
||||
None => return Err(format!("-{flag} needs a value")),
|
||||
None => {
|
||||
return Err(t_fmt(
|
||||
L10nKey::AppSshParseFlagNeedsValue,
|
||||
&[("flag", &flag.to_string())],
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -5834,7 +5983,9 @@ pub(crate) fn parse_ssh_connect_input(input: &str) -> Result<ParsedSshConnect, S
|
||||
.parse::<u16>()
|
||||
.ok()
|
||||
.filter(|&p| p != 0)
|
||||
.ok_or_else(|| format!("Invalid port \u{201c}{value}\u{201d}"))?,
|
||||
.ok_or_else(|| {
|
||||
t_fmt(L10nKey::AppSshParseInvalidPort, &[("value", &value)])
|
||||
})?,
|
||||
)
|
||||
}
|
||||
'l' => user = Some(value),
|
||||
@@ -5844,7 +5995,10 @@ pub(crate) fn parse_ssh_connect_input(input: &str) -> Result<ParsedSshConnect, S
|
||||
_ => {}
|
||||
}
|
||||
} else if word.starts_with('-') {
|
||||
return Err(format!("Unsupported option \u{201c}{word}\u{201d}"));
|
||||
return Err(t_fmt(
|
||||
L10nKey::AppSshParseUnsupportedOption,
|
||||
&[("option", &word)],
|
||||
));
|
||||
} else if target.is_none() {
|
||||
target = Some(word);
|
||||
} else {
|
||||
@@ -5853,9 +6007,9 @@ pub(crate) fn parse_ssh_connect_input(input: &str) -> Result<ParsedSshConnect, S
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let target = target.ok_or_else(|| "Enter a host to connect to".to_string())?;
|
||||
let target = target.ok_or_else(|| t(L10nKey::AppSshParseEnterHost).to_string())?;
|
||||
let qc = parse_quick_connect(&target)
|
||||
.ok_or_else(|| format!("Can't parse host \u{201c}{target}\u{201d}"))?;
|
||||
.ok_or_else(|| t_fmt(L10nKey::AppSshParseBadHost, &[("host", &target)]))?;
|
||||
|
||||
let mut profile = SshProfile::new(qc.host.clone());
|
||||
profile.host = qc.host;
|
||||
@@ -5897,7 +6051,7 @@ fn apply_ssh_o_option(
|
||||
val.parse::<u16>()
|
||||
.ok()
|
||||
.filter(|&p| p != 0)
|
||||
.ok_or_else(|| format!("Invalid port \u{201c}{val}\u{201d}"))?,
|
||||
.ok_or_else(|| t_fmt(L10nKey::AppSshParseInvalidPort, &[("value", val)]))?,
|
||||
)
|
||||
}
|
||||
"proxyjump" => *jump = Some(val.to_string()),
|
||||
|
||||
+66
-21
@@ -15,6 +15,7 @@ use gpui_component::{
|
||||
|
||||
use crate::ui::app::Tty7App;
|
||||
use crate::ui::host_ops::{HostOps, MTime, SharedHost, WatchSub};
|
||||
use crate::ui::i18n::{L10nKey, t, t_fmt};
|
||||
|
||||
const MAX_FILE_BYTES: u64 = 4 * 1024 * 1024;
|
||||
|
||||
@@ -362,24 +363,43 @@ impl Tty7App {
|
||||
let path = h.canonicalize(&p).unwrap_or(p);
|
||||
let meta = match h.stat(&path) {
|
||||
Ok(m) => m,
|
||||
Err(e) => return Err(format!("Can't open {}: {e}", path.display())),
|
||||
Err(e) => {
|
||||
return Err(t_fmt(
|
||||
L10nKey::EditorCantOpen,
|
||||
&[("path", &path.display().to_string()), ("e", &e.to_string())],
|
||||
));
|
||||
}
|
||||
};
|
||||
if meta.len > MAX_FILE_BYTES {
|
||||
return Err(format!(
|
||||
"\"{}\" is too large for the editor ({} MB)",
|
||||
path.display(),
|
||||
meta.len / (1024 * 1024)
|
||||
return Err(t_fmt(
|
||||
L10nKey::EditorFileTooLarge,
|
||||
&[
|
||||
("path", &path.display().to_string()),
|
||||
("size", &(meta.len / (1024 * 1024)).to_string()),
|
||||
],
|
||||
));
|
||||
}
|
||||
let bytes = match h.read_file(&path, MAX_FILE_BYTES) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return Err(format!("Can't read {}: {e}", path.display())),
|
||||
Err(e) => {
|
||||
return Err(t_fmt(
|
||||
L10nKey::EditorCantRead,
|
||||
&[("path", &path.display().to_string()), ("e", &e.to_string())],
|
||||
));
|
||||
}
|
||||
};
|
||||
if looks_binary(&bytes) {
|
||||
return Err(format!("\"{}\" looks like a binary file", path.display()));
|
||||
return Err(t_fmt(
|
||||
L10nKey::EditorBinaryFile,
|
||||
&[("path", &path.display().to_string())],
|
||||
));
|
||||
}
|
||||
let text = String::from_utf8(bytes)
|
||||
.map_err(|_| format!("\"{}\" is not valid UTF-8", path.display()))?;
|
||||
let text = String::from_utf8(bytes).map_err(|_| {
|
||||
t_fmt(
|
||||
L10nKey::EditorNotUtf8,
|
||||
&[("path", &path.display().to_string())],
|
||||
)
|
||||
})?;
|
||||
Ok((path, text, meta.mtime))
|
||||
},
|
||||
move |app, opened, window, cx| match opened {
|
||||
@@ -603,7 +623,7 @@ impl Tty7App {
|
||||
Ok(mtime) => {
|
||||
f.disk_mtime = mtime;
|
||||
}
|
||||
Err(e) => HostOps::notify_err(window, cx, "Save failed", &e),
|
||||
Err(e) => HostOps::notify_err(window, cx, t(L10nKey::EditorSaveFailed), &e),
|
||||
}
|
||||
if landing.clean {
|
||||
f.dirty = false;
|
||||
@@ -658,9 +678,13 @@ impl Tty7App {
|
||||
let name = f.label();
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Warning,
|
||||
&format!("\"{name}\" has unsaved changes"),
|
||||
&t_fmt(L10nKey::EditorUnsavedChanges, &[("name", &name)]),
|
||||
None,
|
||||
&["Save", "Discard", "Cancel"],
|
||||
&[
|
||||
t(L10nKey::Save),
|
||||
t(L10nKey::EditorDiscard),
|
||||
t(L10nKey::Cancel),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
let id = f.input.entity_id();
|
||||
@@ -937,7 +961,9 @@ impl Tty7App {
|
||||
.when(name.is_none(), |d| {
|
||||
d.text_color(cx.theme().muted_foreground)
|
||||
})
|
||||
.child(name.unwrap_or_else(|| SharedString::from("No file open"))),
|
||||
.child(
|
||||
name.unwrap_or_else(|| SharedString::from(t(L10nKey::EditorNoFileOpen))),
|
||||
),
|
||||
)
|
||||
.when(dirty, |d| {
|
||||
d.child(
|
||||
@@ -958,7 +984,7 @@ impl Tty7App {
|
||||
cx,
|
||||
)
|
||||
.rounded_lg()
|
||||
.tooltip("Back to Terminal (Esc)")
|
||||
.tooltip(t(L10nKey::EditorBackToTerminal))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.toggle_code_panel(window, cx);
|
||||
})),
|
||||
@@ -992,7 +1018,14 @@ impl Tty7App {
|
||||
let active = code.and_then(|c| c.active_file());
|
||||
let cursor: Option<SharedString> = active.map(|f| {
|
||||
let pos = f.input.read(cx).cursor_position();
|
||||
format!("Ln {}, Col {}", pos.line + 1, pos.character + 1).into()
|
||||
t_fmt(
|
||||
L10nKey::EditorLnCol,
|
||||
&[
|
||||
("line", &(pos.line + 1).to_string()),
|
||||
("column", &(pos.character + 1).to_string()),
|
||||
],
|
||||
)
|
||||
.into()
|
||||
});
|
||||
let wrap: Option<bool> = active.map(|f| f.wrap);
|
||||
let is_markdown = active.is_some_and(|f| language_for_path(&f.path) == "markdown");
|
||||
@@ -1016,7 +1049,11 @@ impl Tty7App {
|
||||
.when(is_markdown, |this| {
|
||||
this.child(
|
||||
Button::new("status-md-preview")
|
||||
.label(if preview { "Edit" } else { "Preview" })
|
||||
.label(if preview {
|
||||
t(L10nKey::EditorEdit)
|
||||
} else {
|
||||
t(L10nKey::EditorPreview)
|
||||
})
|
||||
.custom(crate::ui::tab_strip::chrome_tile_variant(cx))
|
||||
.xsmall()
|
||||
.on_click(cx.listener(|this, _, _w, cx| {
|
||||
@@ -1033,7 +1070,11 @@ impl Tty7App {
|
||||
.when_some(wrap, |this, wrap| {
|
||||
this.child(
|
||||
Button::new("status-wrap")
|
||||
.label(if wrap { "Wrap: on" } else { "Wrap: off" })
|
||||
.label(if wrap {
|
||||
t(L10nKey::EditorWrapOn)
|
||||
} else {
|
||||
t(L10nKey::EditorWrapOff)
|
||||
})
|
||||
.custom(crate::ui::tab_strip::chrome_tile_variant(cx))
|
||||
.xsmall()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
@@ -1069,7 +1110,9 @@ impl Tty7App {
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("Open a file from the file tree"),
|
||||
.child(crate::ui::i18n::t(
|
||||
crate::ui::i18n::L10nKey::OpenFileFromTree,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1087,10 +1130,12 @@ impl Tty7App {
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.text_sm()
|
||||
.child(div().flex_1().child("File changed on disk"))
|
||||
.child(div().flex_1().child(crate::ui::i18n::t(
|
||||
crate::ui::i18n::L10nKey::FileChangedOnDisk,
|
||||
)))
|
||||
.child(
|
||||
Button::new("editor-conflict-reload")
|
||||
.label("Reload")
|
||||
.label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Reload))
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.editor_reload_from_disk(tab_ix, ix, window, cx);
|
||||
@@ -1098,7 +1143,7 @@ impl Tty7App {
|
||||
)
|
||||
.child(
|
||||
Button::new("editor-conflict-keep")
|
||||
.label("Keep mine")
|
||||
.label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::KeepMine))
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _, _w, cx| {
|
||||
|
||||
+42
-49
@@ -13,6 +13,7 @@ use crate::terminal::git_diff::{
|
||||
MAX_RENDERED_FILES, Truncation,
|
||||
};
|
||||
use crate::ui::app::Tty7App;
|
||||
use crate::ui::i18n::{L10nKey, t, t_fmt, t_plural};
|
||||
use crate::ui::rounding;
|
||||
use crate::ui::rounding::RoundedCorners as _;
|
||||
|
||||
@@ -241,14 +242,13 @@ impl Tty7App {
|
||||
let overlay = self.tabs.get(self.active)?.diff_overlay.as_ref()?;
|
||||
|
||||
let content = match &overlay.load {
|
||||
DiffLoad::Loading => self.diff_message("Reading diff…", cx),
|
||||
DiffLoad::NotARepo => self.diff_message("Not a git repository", cx),
|
||||
DiffLoad::Ready(snap) if empty_snapshot(snap) && snap.read_failed => self.diff_message(
|
||||
"Couldn't read the working-tree diff — retrying on the next refresh.",
|
||||
cx,
|
||||
),
|
||||
DiffLoad::Loading => self.diff_message(t(L10nKey::DiffReading), cx),
|
||||
DiffLoad::NotARepo => self.diff_message(t(L10nKey::DiffNotARepo), cx),
|
||||
DiffLoad::Ready(snap) if empty_snapshot(snap) && snap.read_failed => {
|
||||
self.diff_message(t(L10nKey::DiffReadFailed), cx)
|
||||
}
|
||||
DiffLoad::Ready(snap) if empty_snapshot(snap) => {
|
||||
self.diff_message("Working tree clean", cx)
|
||||
self.diff_message(t(L10nKey::DiffWorkingTreeClean), cx)
|
||||
}
|
||||
DiffLoad::Ready(snap) => {
|
||||
self.diff_file_list(snap, &overlay.expanded, focused_file(snap, overlay), cx)
|
||||
@@ -367,13 +367,9 @@ impl Tty7App {
|
||||
.when(
|
||||
matches!(overlay.load, DiffLoad::Ready(_)) && overlay.focus.is_none(),
|
||||
|bar| {
|
||||
let mut summary = format!(
|
||||
"{} changed file{}",
|
||||
files,
|
||||
if files == 1 { "" } else { "s" }
|
||||
);
|
||||
let mut summary = t_plural(L10nKey::DiffChangedFiles, files, &[]);
|
||||
if untracked > 0 {
|
||||
summary.push_str(&format!(" · {untracked} untracked"));
|
||||
summary.push_str(&t_plural(L10nKey::DiffUntrackedCount, untracked, &[]));
|
||||
}
|
||||
bar.child(
|
||||
div()
|
||||
@@ -406,7 +402,7 @@ impl Tty7App {
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("refreshing…"),
|
||||
.child(t(L10nKey::Refreshing)),
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -421,7 +417,7 @@ impl Tty7App {
|
||||
cx,
|
||||
)
|
||||
.rounded_lg()
|
||||
.tooltip("Close Diff (Esc)")
|
||||
.tooltip(t(L10nKey::DiffCloseTooltip))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.close_diff_overlay(window, cx);
|
||||
})),
|
||||
@@ -478,10 +474,7 @@ impl Tty7App {
|
||||
.py_1p5()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(format!(
|
||||
"… and {rest} more changed file{} — run `git diff` in the terminal to see them.",
|
||||
if rest == 1 { "" } else { "s" }
|
||||
)),
|
||||
.child(t_plural(L10nKey::DiffMoreFiles, rest, &[])),
|
||||
);
|
||||
}
|
||||
if focused.is_none() && !snap.untracked.is_empty() {
|
||||
@@ -502,10 +495,9 @@ impl Tty7App {
|
||||
stats: &DiffStats,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let text = format!(
|
||||
"This working tree is too large to render efficiently ({}). Every file is \
|
||||
collapsed — expand individual files, or run `git diff` in the terminal.",
|
||||
oversized_summary(snap, stats),
|
||||
let text = t_fmt(
|
||||
L10nKey::DiffOversizedNotice,
|
||||
&[("summary", &oversized_summary(snap, stats))],
|
||||
);
|
||||
div()
|
||||
.w_full()
|
||||
@@ -607,7 +599,7 @@ impl Tty7App {
|
||||
.flex_shrink_0()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("binary"),
|
||||
.child(t(L10nKey::Binary)),
|
||||
);
|
||||
}
|
||||
if file.added > 0 {
|
||||
@@ -671,15 +663,11 @@ impl Tty7App {
|
||||
}
|
||||
if let Some(reason) = file.truncated {
|
||||
let note = match reason {
|
||||
Truncation::PerFile => format!(
|
||||
"Diff truncated at {} lines — run `git diff` in the terminal for the rest.",
|
||||
git_diff::MAX_LINES_PER_FILE
|
||||
Truncation::PerFile => t_fmt(
|
||||
L10nKey::DiffTruncatedPerFile,
|
||||
&[("limit", &git_diff::MAX_LINES_PER_FILE.to_string())],
|
||||
),
|
||||
Truncation::Budget => {
|
||||
"Body not loaded — this working tree is past tty7's diff budget. \
|
||||
Run `git diff` in the terminal for this file."
|
||||
.to_string()
|
||||
}
|
||||
Truncation::Budget => t(L10nKey::DiffTruncatedBudget).to_string(),
|
||||
};
|
||||
body = body.child(
|
||||
div()
|
||||
@@ -778,7 +766,7 @@ impl Tty7App {
|
||||
.bg(cx.theme().secondary)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(format!("Untracked files ({total})")),
|
||||
.child(t_plural(L10nKey::DiffUntrackedHeader, total, &[])),
|
||||
);
|
||||
for path in untracked {
|
||||
section = section.child(
|
||||
@@ -809,9 +797,7 @@ impl Tty7App {
|
||||
.py_1()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(format!(
|
||||
"… and {rest} more — run `git status` in the terminal to see them.",
|
||||
)),
|
||||
.child(t_plural(L10nKey::DiffMoreUntracked, rest, &[])),
|
||||
);
|
||||
}
|
||||
section.into_any_element()
|
||||
@@ -843,31 +829,36 @@ fn file_expanded(file: &FileDiff, expanded: &HashMap<String, bool>, collapse_all
|
||||
}
|
||||
|
||||
fn oversized_summary(snap: &DiffSnapshot, stats: &DiffStats) -> String {
|
||||
let mut parts = vec![format!(
|
||||
"{} changed file{}",
|
||||
snap.files.len(),
|
||||
if snap.files.len() == 1 { "" } else { "s" }
|
||||
)];
|
||||
let mut parts = vec![t_plural(L10nKey::DiffChangedFiles, snap.files.len(), &[])];
|
||||
let (added, removed) = stats.totals;
|
||||
let total_lines = (added + removed) as usize;
|
||||
let loaded = stats.retained_lines;
|
||||
let budget = stats.budget_exhausted;
|
||||
let per_file = stats.per_file_truncated;
|
||||
parts.push(match (budget, per_file) {
|
||||
(false, false) => format!("{total_lines} diff lines"),
|
||||
(false, false) => t_plural(L10nKey::DiffLines, total_lines, &[]),
|
||||
_ => {
|
||||
let cap = match (budget, per_file) {
|
||||
(true, true) => "tty7's budget and the per-file cap",
|
||||
(true, false) => "tty7's budget",
|
||||
_ => "the per-file cap",
|
||||
let cap_key = match (budget, per_file) {
|
||||
(true, true) => L10nKey::DiffBudgetAndCap,
|
||||
(true, false) => L10nKey::DiffBudget,
|
||||
_ => L10nKey::DiffPerFileCap,
|
||||
};
|
||||
format!(
|
||||
"{total_lines} changed lines, {loaded} diff rows loaded before {cap} cut the rest"
|
||||
t_fmt(
|
||||
L10nKey::DiffChangedLines,
|
||||
&[
|
||||
("total", &total_lines.to_string()),
|
||||
("loaded", &loaded.to_string()),
|
||||
("cap", t(cap_key)),
|
||||
],
|
||||
)
|
||||
}
|
||||
});
|
||||
if stats.untracked_count > 0 {
|
||||
parts.push(format!("{} untracked", stats.untracked_count));
|
||||
parts.push(t_plural(
|
||||
L10nKey::DiffUntrackedSummary,
|
||||
stats.untracked_count,
|
||||
&[],
|
||||
));
|
||||
}
|
||||
parts.join(", ")
|
||||
}
|
||||
@@ -948,6 +939,7 @@ fn split_hunk(lines: &[git_diff::DiffLine]) -> Vec<SplitRow> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::terminal::git_diff::{DiffLine, LineKind};
|
||||
use crate::ui::i18n::set_locale;
|
||||
|
||||
fn line(kind: LineKind, old: Option<u32>, new: Option<u32>, text: &str) -> DiffLine {
|
||||
DiffLine {
|
||||
@@ -1037,6 +1029,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn banner(snap: &DiffSnapshot) -> String {
|
||||
set_locale("en");
|
||||
oversized_summary(snap, &snap.stats())
|
||||
}
|
||||
|
||||
|
||||
+111
-84
@@ -6,6 +6,7 @@ use crate::core::config::RightPanelTab;
|
||||
use crate::ui::app::Tty7App;
|
||||
use crate::ui::host_ops::{ByHost, HostId, HostOps, InFlight, SharedHost, WatchSub};
|
||||
use crate::ui::host_registry::HostRegistry;
|
||||
use crate::ui::i18n::{L10nKey, t, t_fmt};
|
||||
use gpui::prelude::*;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Entity, ExternalPaths, FocusHandle, KeyDownEvent, MouseButton,
|
||||
@@ -904,9 +905,9 @@ impl Tty7App {
|
||||
};
|
||||
let input = cx.new(|cx| {
|
||||
let mut st = InputState::new(window, cx).placeholder(match edit_for {
|
||||
TreeEditKind::NewFile => "file name",
|
||||
TreeEditKind::NewFolder => "folder name",
|
||||
TreeEditKind::Rename => "new name",
|
||||
TreeEditKind::NewFile => t(L10nKey::FileTreePlaceholderFileName),
|
||||
TreeEditKind::NewFolder => t(L10nKey::FileTreePlaceholderFolderName),
|
||||
TreeEditKind::Rename => t(L10nKey::FileTreePlaceholderNewName),
|
||||
});
|
||||
st.set_value(initial, window, cx);
|
||||
st
|
||||
@@ -1048,15 +1049,15 @@ impl Tty7App {
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| path.display().to_string());
|
||||
let detail = if is_dir {
|
||||
"The folder and everything inside it will be deleted."
|
||||
t(L10nKey::FileTreeDeleteFolderBody)
|
||||
} else {
|
||||
"The file will be deleted."
|
||||
t(L10nKey::FileTreeDeleteFileBody)
|
||||
};
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Warning,
|
||||
&format!("Delete \"{name}\"?"),
|
||||
&t_fmt(L10nKey::FileTreeDeleteTitle, &[("name", &name)]),
|
||||
Some(detail),
|
||||
&["Cancel", "Delete"],
|
||||
&[t(L10nKey::Cancel), t(L10nKey::Delete)],
|
||||
cx,
|
||||
);
|
||||
cx.spawn_in(window, async move |app, cx| {
|
||||
@@ -1096,7 +1097,12 @@ impl Tty7App {
|
||||
}
|
||||
Err(e) => {
|
||||
app.file_tree.rollback(id, &parent, rollback);
|
||||
HostOps::notify_err(window, cx, "Delete failed", &e);
|
||||
HostOps::notify_err(
|
||||
window,
|
||||
cx,
|
||||
t(L10nKey::FileTreeDeleteFailed),
|
||||
&e,
|
||||
);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
@@ -1123,10 +1129,7 @@ impl Tty7App {
|
||||
|
||||
fn file_tree_attach_to_agent(&mut self, path: &Path, cx: &mut Context<Self>) {
|
||||
let Some(target) = self.agent_target_leaf(cx) else {
|
||||
crate::terminal::notify_desktop(
|
||||
Some("tty7"),
|
||||
"No running coding agent found — start one (claude, codex, …) in a pane first.",
|
||||
);
|
||||
crate::terminal::notify_desktop(Some("tty7"), t(L10nKey::AppNoRunningCodingAgent));
|
||||
return;
|
||||
};
|
||||
let rel = self
|
||||
@@ -1353,86 +1356,110 @@ impl Tty7App {
|
||||
let p = path.to_path_buf();
|
||||
|
||||
if !is_dir {
|
||||
menu = menu.item(PopupMenuItem::new("Open").on_click({
|
||||
let app = app.clone();
|
||||
let p = p.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = app.update(cx, |this, cx| this.open_file_in_editor(&p, window, cx));
|
||||
}
|
||||
}));
|
||||
menu = menu.item(
|
||||
PopupMenuItem::new(t(L10nKey::FileTreeContextOpen)).on_click({
|
||||
let app = app.clone();
|
||||
let p = p.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = app.update(cx, |this, cx| this.open_file_in_editor(&p, window, cx));
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
if is_dir {
|
||||
menu = menu.item(PopupMenuItem::new("cd Here").on_click({
|
||||
let app = app.clone();
|
||||
let p = p.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = app.update(cx, |this, cx| this.file_tree_cd(&p, window, cx));
|
||||
}
|
||||
}));
|
||||
menu = menu.item(
|
||||
PopupMenuItem::new(t(L10nKey::FileTreeContextCdHere)).on_click({
|
||||
let app = app.clone();
|
||||
let p = p.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = app.update(cx, |this, cx| this.file_tree_cd(&p, window, cx));
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
menu = menu
|
||||
.item(PopupMenuItem::new("Insert Path in Terminal").on_click({
|
||||
let app = app.clone();
|
||||
let p = p.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = app.update(cx, |this, cx| {
|
||||
if let Some(leaf) = this
|
||||
.tabs
|
||||
.get(this.active)
|
||||
.and_then(|t| t.pane.focused_or_first(window, cx))
|
||||
{
|
||||
leaf.update(cx, |view, cx| view.paste(shell_quote(&p), cx));
|
||||
}
|
||||
});
|
||||
}
|
||||
}))
|
||||
.item(PopupMenuItem::new("Attach to Agent").on_click({
|
||||
let app = app.clone();
|
||||
let p = p.clone();
|
||||
move |_, _window, cx| {
|
||||
let _ = app.update(cx, |this, cx| this.file_tree_attach_to_agent(&p, cx));
|
||||
}
|
||||
}))
|
||||
.item(
|
||||
PopupMenuItem::new(t(L10nKey::FileTreeContextInsertPath)).on_click({
|
||||
let app = app.clone();
|
||||
let p = p.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = app.update(cx, |this, cx| {
|
||||
if let Some(leaf) = this
|
||||
.tabs
|
||||
.get(this.active)
|
||||
.and_then(|t| t.pane.focused_or_first(window, cx))
|
||||
{
|
||||
leaf.update(cx, |view, cx| view.paste(shell_quote(&p), cx));
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
PopupMenuItem::new(t(L10nKey::FileTreeContextAttachAgent)).on_click({
|
||||
let app = app.clone();
|
||||
let p = p.clone();
|
||||
move |_, _window, cx| {
|
||||
let _ = app.update(cx, |this, cx| this.file_tree_attach_to_agent(&p, cx));
|
||||
}
|
||||
}),
|
||||
)
|
||||
.separator()
|
||||
.item(PopupMenuItem::new("New File").on_click({
|
||||
let app = app.clone();
|
||||
let p = p.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = app.update(cx, |this, cx| {
|
||||
this.file_tree_begin_edit(TreeEditKind::NewFile, &p, is_dir, window, cx)
|
||||
});
|
||||
}
|
||||
}))
|
||||
.item(PopupMenuItem::new("New Folder").on_click({
|
||||
let app = app.clone();
|
||||
let p = p.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = app.update(cx, |this, cx| {
|
||||
this.file_tree_begin_edit(TreeEditKind::NewFolder, &p, is_dir, window, cx)
|
||||
});
|
||||
}
|
||||
}));
|
||||
.item(
|
||||
PopupMenuItem::new(t(L10nKey::FileTreeContextNewFile)).on_click({
|
||||
let app = app.clone();
|
||||
let p = p.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = app.update(cx, |this, cx| {
|
||||
this.file_tree_begin_edit(TreeEditKind::NewFile, &p, is_dir, window, cx)
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
PopupMenuItem::new(t(L10nKey::FileTreeContextNewFolder)).on_click({
|
||||
let app = app.clone();
|
||||
let p = p.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = app.update(cx, |this, cx| {
|
||||
this.file_tree_begin_edit(
|
||||
TreeEditKind::NewFolder,
|
||||
&p,
|
||||
is_dir,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if !is_root {
|
||||
menu = menu.item(PopupMenuItem::new("Rename").on_click({
|
||||
let app = app.clone();
|
||||
let p = p.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = app.update(cx, |this, cx| {
|
||||
this.file_tree_begin_edit(TreeEditKind::Rename, &p, is_dir, window, cx)
|
||||
});
|
||||
}
|
||||
}));
|
||||
menu = menu.item(
|
||||
PopupMenuItem::new(t(L10nKey::FileTreeContextRename)).on_click({
|
||||
let app = app.clone();
|
||||
let p = p.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = app.update(cx, |this, cx| {
|
||||
this.file_tree_begin_edit(TreeEditKind::Rename, &p, is_dir, window, cx)
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
menu = menu
|
||||
.separator()
|
||||
.item(PopupMenuItem::new("Copy Path").on_click({
|
||||
let p = p.clone();
|
||||
move |_, _window, cx| {
|
||||
cx.write_to_clipboard(gpui::ClipboardItem::new_string(p.display().to_string()));
|
||||
}
|
||||
}))
|
||||
.item(
|
||||
PopupMenuItem::new(t(L10nKey::FileTreeContextCopyPath)).on_click({
|
||||
let p = p.clone();
|
||||
move |_, _window, cx| {
|
||||
cx.write_to_clipboard(gpui::ClipboardItem::new_string(
|
||||
p.display().to_string(),
|
||||
));
|
||||
}
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
PopupMenuItem::new(crate::ui::right_panel::reveal_label()).on_click({
|
||||
let p = p.clone();
|
||||
@@ -1447,7 +1474,7 @@ impl Tty7App {
|
||||
if !is_root {
|
||||
menu = menu.separator().item(
|
||||
PopupMenuItem::element(move |_window, _cx| {
|
||||
div().text_color(danger).child("Delete")
|
||||
div().text_color(danger).child(t(L10nKey::Delete))
|
||||
})
|
||||
.on_click({
|
||||
let app = app.clone();
|
||||
@@ -1466,9 +1493,9 @@ impl Tty7App {
|
||||
fn dotfiles_menu_item(show_hidden: bool, app: &gpui::WeakEntity<Tty7App>) -> PopupMenuItem {
|
||||
let app = app.clone();
|
||||
PopupMenuItem::new(if show_hidden {
|
||||
"Hide Dotfiles"
|
||||
t(L10nKey::FileTreeContextHideDotfiles)
|
||||
} else {
|
||||
"Show Dotfiles"
|
||||
t(L10nKey::FileTreeContextShowDotfiles)
|
||||
})
|
||||
.on_click(move |_, _window, cx| {
|
||||
let _ = app.update(cx, |this, cx| {
|
||||
|
||||
+33
-16
@@ -6,6 +6,7 @@ use gpui_component::{ActiveTheme as _, Icon, IconName, Sizable as _, h_flex, v_f
|
||||
use crate::daemon::protocol::{ForwardStatus, ManagedForward, SshForwardKind};
|
||||
use crate::terminal::view::TerminalView;
|
||||
use crate::ui::app::{CONTENT_INSET, Tty7App};
|
||||
use crate::ui::i18n::{L10nKey, t, t_fmt};
|
||||
|
||||
impl Tty7App {
|
||||
pub(crate) fn render_ssh_status_strip(
|
||||
@@ -45,15 +46,15 @@ impl Tty7App {
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.text_color(theme.foreground)
|
||||
.child(if host.is_empty() {
|
||||
"Disconnected".to_string()
|
||||
t(L10nKey::ForwardDisconnected).to_string()
|
||||
} else {
|
||||
format!("Disconnected from {host}")
|
||||
t_fmt(L10nKey::ForwardDisconnectedFrom, &[("host", &host)])
|
||||
}),
|
||||
)
|
||||
.child(div().child("· ⌘⇧R"))
|
||||
.child(
|
||||
Button::new("ssh-reconnect")
|
||||
.label("Reconnect")
|
||||
.label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Reconnect))
|
||||
.primary()
|
||||
.small()
|
||||
.on_click(
|
||||
@@ -92,13 +93,13 @@ impl Tty7App {
|
||||
.child(
|
||||
div()
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.child("Close this SSH connection?"),
|
||||
.child(t(crate::ui::i18n::L10nKey::CloseSshConnectionTitle)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(theme.muted_foreground)
|
||||
.child("The connection is live. Closing will end it."),
|
||||
.child(t(crate::ui::i18n::L10nKey::CloseSshConnectionBody)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -106,7 +107,7 @@ impl Tty7App {
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("ssh-close-cancel")
|
||||
.label("Keep")
|
||||
.label(t(crate::ui::i18n::L10nKey::Keep))
|
||||
.small()
|
||||
.on_click(
|
||||
cx.listener(|this, _, _window, cx| this.cancel_ssh_close(cx)),
|
||||
@@ -114,7 +115,7 @@ impl Tty7App {
|
||||
)
|
||||
.child(
|
||||
Button::new("ssh-close-confirm")
|
||||
.label("Close")
|
||||
.label(t(crate::ui::i18n::L10nKey::Close))
|
||||
.primary()
|
||||
.small()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
@@ -151,7 +152,11 @@ impl Tty7App {
|
||||
.w(px(24.))
|
||||
.h(px(24.))
|
||||
.rounded_md()
|
||||
.tooltip(if open { "Cancel" } else { "Add forward" })
|
||||
.tooltip(if open {
|
||||
t(L10nKey::Cancel)
|
||||
} else {
|
||||
t(L10nKey::ForwardTooltipAdd)
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.toggle_managed_forward_form(pane_id, window, cx)
|
||||
}))
|
||||
@@ -173,7 +178,7 @@ impl Tty7App {
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.child(self.panel_subtitle("Forwards", true, Some(add), cx))
|
||||
.child(self.panel_subtitle(t(L10nKey::ForwardPanelTitle), true, Some(add), cx))
|
||||
.when(managed.is_empty() && !open, |this| {
|
||||
this.child(
|
||||
div()
|
||||
@@ -181,7 +186,7 @@ impl Tty7App {
|
||||
.py(px(2.))
|
||||
.text_size(px(12.))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("None."),
|
||||
.child(crate::ui::i18n::t(crate::ui::i18n::L10nKey::None)),
|
||||
)
|
||||
})
|
||||
.when(!managed.is_empty(), |this| this.child(list))
|
||||
@@ -293,7 +298,7 @@ impl Tty7App {
|
||||
.w(px(18.))
|
||||
.h(px(18.))
|
||||
.rounded(px(4.))
|
||||
.tooltip("Remove")
|
||||
.tooltip(t(L10nKey::ForwardTooltipRemove))
|
||||
.on_click(cx.listener(
|
||||
move |this, _, _window, cx| {
|
||||
this.remove_managed_forward(pane_id, forward_id, cx)
|
||||
@@ -343,7 +348,11 @@ impl Tty7App {
|
||||
.child(self.segmented_on(
|
||||
sf,
|
||||
"ssh-managed-forward-kind",
|
||||
&["Local", "Remote", "Dynamic"],
|
||||
&[
|
||||
t(L10nKey::ForwardLocal),
|
||||
t(L10nKey::ForwardRemote),
|
||||
t(L10nKey::ForwardDynamic),
|
||||
],
|
||||
selected,
|
||||
cx,
|
||||
move |this, ix, _window, cx| {
|
||||
@@ -356,7 +365,7 @@ impl Tty7App {
|
||||
},
|
||||
))
|
||||
.child(pair(
|
||||
"bind",
|
||||
t(L10nKey::ForwardBindLabel),
|
||||
&self.loopback_panel.mf_bind_host,
|
||||
&self.loopback_panel.mf_bind_port,
|
||||
))
|
||||
@@ -364,7 +373,11 @@ impl Tty7App {
|
||||
div()
|
||||
.opacity(if needs_target { 1.0 } else { 0.4 })
|
||||
.child(pair(
|
||||
if needs_target { "to" } else { "SOCKS" },
|
||||
if needs_target {
|
||||
t(L10nKey::ForwardToLabel)
|
||||
} else {
|
||||
t(L10nKey::ForwardSocksLabel)
|
||||
},
|
||||
&self.loopback_panel.mf_target_host,
|
||||
&self.loopback_panel.mf_target_port,
|
||||
)),
|
||||
@@ -377,7 +390,7 @@ impl Tty7App {
|
||||
.pt(px(1.))
|
||||
.child(
|
||||
Button::new(("ssh-managed-forward-cancel", pane_id))
|
||||
.label("Cancel")
|
||||
.label(t(L10nKey::Cancel))
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
@@ -386,7 +399,11 @@ impl Tty7App {
|
||||
)
|
||||
.child(
|
||||
Button::new(("ssh-managed-forward-add", pane_id))
|
||||
.label(if editing { "Save" } else { "Add" })
|
||||
.label(if editing {
|
||||
t(L10nKey::Save)
|
||||
} else {
|
||||
t(L10nKey::ForwardAdd)
|
||||
})
|
||||
.primary()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
|
||||
+48
-21
@@ -10,6 +10,7 @@ use gpui_component::{ActiveTheme as _, IconName, Sizable as _, h_flex, v_flex};
|
||||
|
||||
use crate::core::session::{SessionPane, SessionTab};
|
||||
use crate::ui::app::Tty7App;
|
||||
use crate::ui::i18n::{L10nKey, t, t_fmt, t_plural};
|
||||
|
||||
const LOGO: [&str; 4] = [
|
||||
" ▄▄▄ ▄▄▄ ▄ ▄ ▄▄▄▄",
|
||||
@@ -20,14 +21,14 @@ const LOGO: [&str; 4] = [
|
||||
|
||||
const LOGO_PX: f32 = 20.0;
|
||||
|
||||
const HOME_SHORTCUTS: [(&str, &str); 7] = [
|
||||
("NewTab", "New Tab"),
|
||||
("ReopenClosedTab", "Reopen Closed Tab"),
|
||||
("ToggleSwitcher", "Switch Workspace"),
|
||||
("TogglePalette", "Command Palette"),
|
||||
("SplitRight", "Split Right"),
|
||||
("SplitDown", "Split Down"),
|
||||
("OpenSettings", "Settings…"),
|
||||
const HOME_SHORTCUTS: [&str; 7] = [
|
||||
"NewTab",
|
||||
"ReopenClosedTab",
|
||||
"ToggleSwitcher",
|
||||
"TogglePalette",
|
||||
"SplitRight",
|
||||
"SplitDown",
|
||||
"OpenSettings",
|
||||
];
|
||||
|
||||
const CLOSED_LABEL_MAX: usize = 20;
|
||||
@@ -70,17 +71,17 @@ pub(crate) fn now_secs() -> u64 {
|
||||
|
||||
pub(crate) fn relative_time(now: u64, then: u64) -> String {
|
||||
if then == 0 || then >= now {
|
||||
return "just now".to_string();
|
||||
return t(L10nKey::HomeTimeJustNow).to_string();
|
||||
}
|
||||
let secs = now - then;
|
||||
match secs {
|
||||
s if s < 60 => "just now".to_string(),
|
||||
s if s < 3600 => format!("{} min ago", s / 60),
|
||||
s if s < 7200 => "1 hour ago".to_string(),
|
||||
s if s < 86_400 => format!("{} hours ago", s / 3600),
|
||||
s if s < 172_800 => "yesterday".to_string(),
|
||||
s if s < 604_800 => format!("{} days ago", s / 86_400),
|
||||
_ => "over a week ago".to_string(),
|
||||
s if s < 60 => t(L10nKey::HomeTimeJustNow).to_string(),
|
||||
s if s < 3_600 => t_plural(L10nKey::HomeTimeMinutesAgo, (s / 60) as usize, &[]),
|
||||
s if s < 7_200 => t(L10nKey::HomeTimeHourAgo).to_string(),
|
||||
s if s < 86_400 => t_plural(L10nKey::HomeTimeHoursAgo, (s / 3_600) as usize, &[]),
|
||||
s if s < 172_800 => t(L10nKey::HomeTimeYesterday).to_string(),
|
||||
s if s < 604_800 => t_plural(L10nKey::HomeTimeDaysAgo, (s / 86_400) as usize, &[]),
|
||||
_ => t(L10nKey::HomeTimeOverWeekAgo).to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +110,25 @@ fn key_hint(action: &str, cx: &App) -> Option<String> {
|
||||
Some(Kbd::format(&stroke))
|
||||
}
|
||||
|
||||
fn home_shortcut_label(action: &str, closed: Option<&str>) -> String {
|
||||
let label = match action {
|
||||
"NewTab" => crate::ui::i18n::t(crate::ui::i18n::L10nKey::HomeNewTab),
|
||||
"ReopenClosedTab" => crate::ui::i18n::t(crate::ui::i18n::L10nKey::HomeReopenClosedTab),
|
||||
"ToggleSwitcher" => crate::ui::i18n::t(crate::ui::i18n::L10nKey::HomeSwitchWorkspace),
|
||||
"TogglePalette" => crate::ui::i18n::t(crate::ui::i18n::L10nKey::HomeCommandPalette),
|
||||
"SplitRight" => crate::ui::i18n::t(crate::ui::i18n::L10nKey::HomeSplitRight),
|
||||
"SplitDown" => crate::ui::i18n::t(crate::ui::i18n::L10nKey::HomeSplitDown),
|
||||
"OpenSettings" => crate::ui::i18n::t(crate::ui::i18n::L10nKey::HomeSettings),
|
||||
_ => action,
|
||||
};
|
||||
if action == "ReopenClosedTab" {
|
||||
if let Some(name) = closed {
|
||||
return t_fmt(L10nKey::HomeReopenNamed, &[("name", name)]);
|
||||
}
|
||||
}
|
||||
label.to_string()
|
||||
}
|
||||
|
||||
impl Tty7App {
|
||||
pub(crate) fn render_home(&self, cx: &mut Context<Self>) -> impl IntoElement + use<> {
|
||||
let theme = cx.theme();
|
||||
@@ -133,11 +153,9 @@ impl Tty7App {
|
||||
|
||||
let closed_hint = self.closed.last().and_then(closed_tab_label);
|
||||
let mut list = v_flex().gap_2().w(px(300.)).text_sm().text_color(muted);
|
||||
for (action, label) in HOME_SHORTCUTS {
|
||||
let (label, emphasized) = match (&closed_hint, action) {
|
||||
(Some(name), "ReopenClosedTab") => (format!("Reopen \u{201c}{name}\u{201d}"), true),
|
||||
_ => (label.to_string(), false),
|
||||
};
|
||||
for action in HOME_SHORTCUTS {
|
||||
let emphasized = closed_hint.is_some() && action == "ReopenClosedTab";
|
||||
let label = home_shortcut_label(action, closed_hint.as_deref());
|
||||
list = list.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
@@ -219,6 +237,7 @@ impl Tty7App {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ui::i18n::set_locale;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn leaf(cwd: Option<&str>) -> SessionPane {
|
||||
@@ -311,6 +330,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn relative_time_reads_coarsely_across_the_ranges() {
|
||||
set_locale("en");
|
||||
let now = 10_000_000u64;
|
||||
assert_eq!(relative_time(now, now), "just now");
|
||||
assert_eq!(relative_time(now, now - 30), "just now");
|
||||
@@ -320,10 +340,17 @@ mod tests {
|
||||
assert_eq!(relative_time(now, now - 90_000), "yesterday");
|
||||
assert_eq!(relative_time(now, now - 3 * 86_400), "3 days ago");
|
||||
assert_eq!(relative_time(now, now - 30 * 86_400), "over a week ago");
|
||||
|
||||
set_locale("zh-CN");
|
||||
assert_eq!(relative_time(now, now - 30), "刚刚");
|
||||
assert_eq!(relative_time(now, now - 120), "2 分钟前");
|
||||
assert_eq!(relative_time(now, now - 3600), "1 小时前");
|
||||
assert_eq!(relative_time(now, now - 90_000), "昨天");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_time_never_renders_a_negative_age() {
|
||||
set_locale("en");
|
||||
let now = 1_000_000u64;
|
||||
assert_eq!(relative_time(now, 0), "just now");
|
||||
assert_eq!(relative_time(now, now + 5_000), "just now");
|
||||
|
||||
+9
-1
@@ -5,6 +5,8 @@ use std::hash::Hash;
|
||||
use gpui::{App, Context, Window};
|
||||
use gpui_component::WindowExt as _;
|
||||
|
||||
use crate::ui::i18n::{L10nKey, t_fmt};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use tty7_core::host::{
|
||||
Entry, Host, HostId, MTime, Meta, Output, SearchHit, SharedHost, WatchSub,
|
||||
@@ -200,7 +202,13 @@ impl HostOps {
|
||||
}
|
||||
|
||||
pub fn notify_err(window: &mut Window, cx: &mut App, context: &str, err: &std::io::Error) {
|
||||
window.push_notification(format!("{context}: {err}"), cx);
|
||||
window.push_notification(
|
||||
t_fmt(
|
||||
L10nKey::HostOpsError,
|
||||
&[("context", context), ("error", &err.to_string())],
|
||||
),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3896
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ use tty7_core::daemon::control::{ControlRequest, ReplyOk};
|
||||
use tty7_core::host::HostId;
|
||||
|
||||
use crate::core::session::WorkspaceId;
|
||||
use crate::ui::i18n::{L10nKey, t};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MachineMirrors {
|
||||
@@ -264,7 +265,7 @@ pub fn display_name_of(ws: &Workspace, panes: &[PaneRecord]) -> String {
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
})
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| "Untitled".to_string())
|
||||
.unwrap_or_else(|| t(L10nKey::WindowUntitled).to_string())
|
||||
}
|
||||
|
||||
pub fn subject_path_of(ws: &Workspace, panes: &[PaneRecord]) -> Option<String> {
|
||||
|
||||
@@ -10,6 +10,7 @@ pub mod home;
|
||||
pub mod host_ops;
|
||||
#[allow(dead_code)]
|
||||
pub mod host_registry;
|
||||
pub mod i18n;
|
||||
pub mod keymap;
|
||||
pub mod local_link;
|
||||
pub mod machine_mirror;
|
||||
|
||||
+116
-99
@@ -12,6 +12,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::core::config::{Config, RightPanelTab, TabBarPosition};
|
||||
use crate::core::ssh_profile::parse_quick_connect;
|
||||
use crate::ui::i18n::{L10nKey, t, t_fmt};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum CommandKind {
|
||||
@@ -297,13 +298,13 @@ impl CommandGroup {
|
||||
|
||||
fn title(self) -> &'static str {
|
||||
match self {
|
||||
CommandGroup::TabsPanes => "Tabs & Panes",
|
||||
CommandGroup::Workspaces => "Workspaces",
|
||||
CommandGroup::View => "View",
|
||||
CommandGroup::Terminal => "Terminal",
|
||||
CommandGroup::Ssh => "SSH",
|
||||
CommandGroup::Agents => "Agents",
|
||||
CommandGroup::Application => "Application",
|
||||
CommandGroup::TabsPanes => t(L10nKey::CmdGroupTabsPanes),
|
||||
CommandGroup::Workspaces => t(L10nKey::CmdGroupWorkspaces),
|
||||
CommandGroup::View => t(L10nKey::CmdGroupView),
|
||||
CommandGroup::Terminal => t(L10nKey::CmdGroupTerminal),
|
||||
CommandGroup::Ssh => t(L10nKey::CmdGroupSsh),
|
||||
CommandGroup::Agents => t(L10nKey::CmdGroupAgents),
|
||||
CommandGroup::Application => t(L10nKey::CmdGroupApplication),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -350,127 +351,134 @@ impl Command {
|
||||
let right_panel_open = chrome.right_panel_visible;
|
||||
|
||||
let tabs = [
|
||||
Command::new("New Tab", NewTab),
|
||||
Command::new("New Worktree Tab", NewWorktreeTab)
|
||||
.with_subtitle("isolated checkout on a fresh branch"),
|
||||
Command::new("Rename Tab…", RenameTab),
|
||||
Command::new("Split Right", SplitRight),
|
||||
Command::new("Split Down", SplitDown),
|
||||
Command::new("Zoom Pane", ToggleMaximizePane),
|
||||
Command::new("Next Pane", NextPane),
|
||||
Command::new("Previous Pane", PrevPane),
|
||||
Command::new("Focus Pane Left", FocusPaneLeft),
|
||||
Command::new("Focus Pane Right", FocusPaneRight),
|
||||
Command::new("Focus Pane Up", FocusPaneUp),
|
||||
Command::new("Focus Pane Down", FocusPaneDown),
|
||||
Command::new("Resize Pane Left", ResizePaneLeft),
|
||||
Command::new("Resize Pane Right", ResizePaneRight),
|
||||
Command::new("Resize Pane Up", ResizePaneUp),
|
||||
Command::new("Resize Pane Down", ResizePaneDown),
|
||||
Command::new("Swap Pane Next", SwapPaneNext),
|
||||
Command::new("Swap Pane Previous", SwapPanePrev),
|
||||
Command::new("Next Tab", NextTab),
|
||||
Command::new("Previous Tab", PrevTab),
|
||||
Command::new("Copy Working Directory", CopyWorkingDirectory),
|
||||
Command::new("Copy Session ID", CopyAgentSessionId)
|
||||
.with_subtitle("the coding agent's own session id"),
|
||||
Command::new("Fork Session", ForkAgentSession)
|
||||
.with_subtitle("branch this agent session into a new tab"),
|
||||
Command::new("Mark Tab as Unread", MarkTabUnread),
|
||||
Command::new("Close Pane / Tab", ClosePane),
|
||||
Command::new("Close Other Tabs", CloseOtherTabs),
|
||||
Command::new("Close Tabs to the Right", CloseTabsToTheRight),
|
||||
Command::new("Reopen Closed Tab", ReopenClosedTab),
|
||||
Command::new(t(L10nKey::CmdNewTab), NewTab),
|
||||
Command::new(t(L10nKey::CmdNewWorktreeTab), NewWorktreeTab)
|
||||
.with_subtitle(t(L10nKey::CmdNewWorktreeTabSubtitle)),
|
||||
Command::new(t(L10nKey::CmdRenameTab), RenameTab),
|
||||
Command::new(t(L10nKey::CmdSplitRight), SplitRight),
|
||||
Command::new(t(L10nKey::CmdSplitDown), SplitDown),
|
||||
Command::new(t(L10nKey::CmdZoomPane), ToggleMaximizePane),
|
||||
Command::new(t(L10nKey::CmdNextPane), NextPane),
|
||||
Command::new(t(L10nKey::CmdPreviousPane), PrevPane),
|
||||
Command::new(t(L10nKey::CmdFocusPaneLeft), FocusPaneLeft),
|
||||
Command::new(t(L10nKey::CmdFocusPaneRight), FocusPaneRight),
|
||||
Command::new(t(L10nKey::CmdFocusPaneUp), FocusPaneUp),
|
||||
Command::new(t(L10nKey::CmdFocusPaneDown), FocusPaneDown),
|
||||
Command::new(t(L10nKey::CmdResizePaneLeft), ResizePaneLeft),
|
||||
Command::new(t(L10nKey::CmdResizePaneRight), ResizePaneRight),
|
||||
Command::new(t(L10nKey::CmdResizePaneUp), ResizePaneUp),
|
||||
Command::new(t(L10nKey::CmdResizePaneDown), ResizePaneDown),
|
||||
Command::new(t(L10nKey::CmdSwapPaneNext), SwapPaneNext),
|
||||
Command::new(t(L10nKey::CmdSwapPanePrevious), SwapPanePrev),
|
||||
Command::new(t(L10nKey::CmdNextTab), NextTab),
|
||||
Command::new(t(L10nKey::CmdPreviousTab), PrevTab),
|
||||
Command::new(t(L10nKey::CmdCopyWorkingDirectory), CopyWorkingDirectory),
|
||||
Command::new(t(L10nKey::CmdCopySessionId), CopyAgentSessionId)
|
||||
.with_subtitle(t(L10nKey::CmdCopySessionIdSubtitle)),
|
||||
Command::new(t(L10nKey::CmdForkSession), ForkAgentSession)
|
||||
.with_subtitle(t(L10nKey::CmdForkSessionSubtitle)),
|
||||
Command::new(t(L10nKey::CmdMarkTabAsUnread), MarkTabUnread),
|
||||
Command::new(t(L10nKey::CmdClosePaneTab), ClosePane),
|
||||
Command::new(t(L10nKey::CmdCloseOtherTabs), CloseOtherTabs),
|
||||
Command::new(t(L10nKey::CmdCloseTabsToTheRight), CloseTabsToTheRight),
|
||||
Command::new(t(L10nKey::CmdReopenClosedTab), ReopenClosedTab),
|
||||
];
|
||||
|
||||
let workspaces = [
|
||||
Command::new("New Workspace", NewWorkspace),
|
||||
Command::new("Switch Workspace…", OpenWorkspacePicker),
|
||||
Command::new("Rename Workspace…", RenameWorkspace),
|
||||
Command::new("Stop Workspace…", StopWorkspace)
|
||||
.with_subtitle("ends its shells, keeps the layout"),
|
||||
Command::new("Delete Workspace…", DeleteWorkspace)
|
||||
.with_subtitle("ends its shells and forgets the layout"),
|
||||
Command::new(t(L10nKey::CmdNewWorkspace), NewWorkspace),
|
||||
Command::new(t(L10nKey::CmdSwitchWorkspace), OpenWorkspacePicker),
|
||||
Command::new(t(L10nKey::CmdRenameWorkspace), RenameWorkspace),
|
||||
Command::new(t(L10nKey::CmdStopWorkspace), StopWorkspace)
|
||||
.with_subtitle(t(L10nKey::CmdStopWorkspaceSubtitle)),
|
||||
Command::new(t(L10nKey::CmdDeleteWorkspace), DeleteWorkspace)
|
||||
.with_subtitle(t(L10nKey::CmdDeleteWorkspaceSubtitle)),
|
||||
];
|
||||
|
||||
let view = [
|
||||
Command::new(
|
||||
if sidebar_hidden {
|
||||
"Show Left Sidebar"
|
||||
t(L10nKey::CmdShowLeftSidebar)
|
||||
} else {
|
||||
"Hide Left Sidebar"
|
||||
t(L10nKey::CmdHideLeftSidebar)
|
||||
},
|
||||
ToggleLeftPanel,
|
||||
),
|
||||
Command::new(
|
||||
if right_panel_open {
|
||||
"Hide Right Panel"
|
||||
t(L10nKey::CmdHideRightPanel)
|
||||
} else {
|
||||
"Show Right Panel"
|
||||
t(L10nKey::CmdShowRightPanel)
|
||||
},
|
||||
ToggleRightPanel,
|
||||
),
|
||||
Command::new("Show Code Panel", ToggleCodePanel),
|
||||
Command::new(t(L10nKey::CmdShowCodePanel), ToggleCodePanel),
|
||||
Command::new(
|
||||
if tab_bar_left {
|
||||
"Tab Bar: Move to Top"
|
||||
t(L10nKey::CmdTabBarMoveToTop)
|
||||
} else {
|
||||
"Tab Bar: Move to Left Sidebar"
|
||||
t(L10nKey::CmdTabBarMoveToLeftSidebar)
|
||||
},
|
||||
ToggleTabSidebar,
|
||||
),
|
||||
Command::new("Right Panel: Info", ShowRightPanel(RightPanelTab::Info)),
|
||||
Command::new(
|
||||
"Right Panel: Outline",
|
||||
t(L10nKey::CmdRightPanelInfo),
|
||||
ShowRightPanel(RightPanelTab::Info),
|
||||
),
|
||||
Command::new(
|
||||
t(L10nKey::CmdRightPanelOutline),
|
||||
ShowRightPanel(RightPanelTab::Outline),
|
||||
),
|
||||
Command::new(
|
||||
"Right Panel: Changes",
|
||||
t(L10nKey::CmdRightPanelChanges),
|
||||
ShowRightPanel(RightPanelTab::Changes),
|
||||
),
|
||||
Command::new("Right Panel: Files", ShowRightPanel(RightPanelTab::Files)),
|
||||
Command::new("Change Theme…", OpenThemePicker),
|
||||
Command::new("Reset Font Size", ResetFontSize),
|
||||
Command::new("Enter Full Screen", ToggleFullscreen),
|
||||
Command::new(
|
||||
t(L10nKey::CmdRightPanelFiles),
|
||||
ShowRightPanel(RightPanelTab::Files),
|
||||
),
|
||||
Command::new(t(L10nKey::CmdChangeTheme), OpenThemePicker),
|
||||
Command::new(t(L10nKey::CmdResetFontSize), ResetFontSize),
|
||||
Command::new(t(L10nKey::CmdEnterFullScreen), ToggleFullscreen),
|
||||
];
|
||||
|
||||
let terminal = [
|
||||
Command::new("Clear Scrollback", ClearTerminal),
|
||||
Command::new("Find in Terminal…", FindInTerminal),
|
||||
Command::new("Find Next", FindNext),
|
||||
Command::new("Find Previous", FindPrevious),
|
||||
Command::new("Copy", CopyText),
|
||||
Command::new("Cut", CutText),
|
||||
Command::new("Paste", PasteText),
|
||||
Command::new("Select All", SelectAllText),
|
||||
Command::new(t(L10nKey::CmdClearScrollback), ClearTerminal),
|
||||
Command::new(t(L10nKey::CmdFindInTerminal), FindInTerminal),
|
||||
Command::new(t(L10nKey::CmdFindNext), FindNext),
|
||||
Command::new(t(L10nKey::CmdFindPrevious), FindPrevious),
|
||||
Command::new(t(L10nKey::CmdCopy), CopyText),
|
||||
Command::new(t(L10nKey::CmdCut), CutText),
|
||||
Command::new(t(L10nKey::CmdPaste), PasteText),
|
||||
Command::new(t(L10nKey::CmdSelectAll), SelectAllText),
|
||||
];
|
||||
|
||||
let ssh = [
|
||||
Command::new("SSH: Add Connection…", OpenSshConnectInput),
|
||||
Command::new("SSH: Manage Profiles…", OpenSshProfiles),
|
||||
Command::new("SSH: Reconnect", RestartSshSession),
|
||||
Command::new("SSH: Remote Files", ToggleSftp),
|
||||
Command::new("SSH: Port Forwarding", ShowSshForwards),
|
||||
Command::new(t(L10nKey::CmdSshAddConnection), OpenSshConnectInput),
|
||||
Command::new(t(L10nKey::CmdSshManageProfiles), OpenSshProfiles),
|
||||
Command::new(t(L10nKey::CmdSshReconnect), RestartSshSession),
|
||||
Command::new(t(L10nKey::CmdSshRemoteFiles), ToggleSftp),
|
||||
Command::new(t(L10nKey::CmdSshPortForwarding), ShowSshForwards),
|
||||
];
|
||||
|
||||
let agents = [
|
||||
Command::new("Agent: Send Selection", SendSelectionToAgent)
|
||||
.with_subtitle("selection → running coding agent"),
|
||||
Command::new("Agent: Send Git Diff for Review", SendGitDiffToAgent)
|
||||
.with_subtitle("git diff → running coding agent"),
|
||||
Command::new(t(L10nKey::CmdAgentSendSelection), SendSelectionToAgent)
|
||||
.with_subtitle(t(L10nKey::CmdAgentSendSelectionSubtitle)),
|
||||
Command::new(t(L10nKey::CmdAgentSendGitDiffForReview), SendGitDiffToAgent)
|
||||
.with_subtitle(t(L10nKey::CmdAgentSendGitDiffSubtitle)),
|
||||
];
|
||||
|
||||
let application = [
|
||||
Command::new("Settings…", OpenSettings),
|
||||
Command::new("Keyboard Shortcuts", ShowKeyboardShortcuts),
|
||||
Command::new("About tty7", About),
|
||||
Command::new("Check for Updates…", CheckForUpdates),
|
||||
Command::new("Documentation", OpenDocumentation),
|
||||
Command::new("Join the Discord", OpenDiscord),
|
||||
Command::new("Report an Issue…", ReportIssue),
|
||||
Command::new("Restart Server…", RestartDaemon)
|
||||
.with_subtitle("ends every running shell; layout is kept"),
|
||||
Command::new("Quit tty7", Quit).with_subtitle("shells keep running"),
|
||||
Command::new(t(L10nKey::CmdSettings), OpenSettings),
|
||||
Command::new(t(L10nKey::CmdKeyboardShortcuts), ShowKeyboardShortcuts),
|
||||
Command::new(t(L10nKey::CmdAboutTty7), About),
|
||||
Command::new(t(L10nKey::CmdCheckForUpdates), CheckForUpdates),
|
||||
Command::new(t(L10nKey::CmdDocumentation), OpenDocumentation),
|
||||
Command::new(t(L10nKey::CmdJoinDiscord), OpenDiscord),
|
||||
Command::new(t(L10nKey::CmdReportIssue), ReportIssue),
|
||||
Command::new(t(L10nKey::CmdRestartServer), RestartDaemon)
|
||||
.with_subtitle(t(L10nKey::CmdRestartServerSubtitle)),
|
||||
Command::new(t(L10nKey::CmdQuitTty7), Quit)
|
||||
.with_subtitle(t(L10nKey::CmdQuitTty7Subtitle)),
|
||||
];
|
||||
|
||||
let mut out = Vec::new();
|
||||
@@ -504,10 +512,11 @@ impl Command {
|
||||
}
|
||||
|
||||
fn ssh_connect_command(input: &str) -> Command {
|
||||
let title = if input.trim().is_empty() {
|
||||
"SSH: Add Connection…".to_string()
|
||||
let trimmed = input.trim();
|
||||
let title = if trimmed.is_empty() {
|
||||
t(L10nKey::CmdSshAddConnection).to_string()
|
||||
} else {
|
||||
format!("SSH: Connect {}", input.trim())
|
||||
t_fmt(L10nKey::CmdSshConnectWithInput, &[("input", trimmed)])
|
||||
};
|
||||
Command::new(title, CommandKind::OpenSshConnect(input.to_string()))
|
||||
}
|
||||
@@ -644,7 +653,7 @@ impl PaletteDelegate {
|
||||
recent.truncate(RECENT_ROWS);
|
||||
if !recent.is_empty() {
|
||||
sections.push(Section {
|
||||
title: Some("Recent".into()),
|
||||
title: Some(t(L10nKey::CmdRecent).into()),
|
||||
commands: recent.into_iter().map(|(_, c)| c.clone()).collect(),
|
||||
});
|
||||
}
|
||||
@@ -675,11 +684,11 @@ impl PaletteDelegate {
|
||||
let target = query.trim().to_string();
|
||||
vec![
|
||||
Command::new(
|
||||
format!("Connect to \"{target}\""),
|
||||
t_fmt(L10nKey::CmdQuickConnect, &[("target", &target)]),
|
||||
CommandKind::QuickConnect(target.clone()),
|
||||
),
|
||||
Command::new(
|
||||
format!("Save \"{target}\" as profile…"),
|
||||
t_fmt(L10nKey::CmdQuickConnectSaveProfile, &[("target", &target)]),
|
||||
CommandKind::SaveQuickConnect(target),
|
||||
),
|
||||
]
|
||||
@@ -803,11 +812,13 @@ impl ListDelegate for PaletteDelegate {
|
||||
.items_center()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("No matching commands")
|
||||
.child(crate::ui::i18n::t(
|
||||
crate::ui::i18n::L10nKey::NoMatchingCommands,
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.child("Type user@host to connect over SSH instead."),
|
||||
.child(crate::ui::i18n::t(crate::ui::i18n::L10nKey::ConnectSshHint)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -840,7 +851,12 @@ impl ListDelegate for PaletteDelegate {
|
||||
.justify_between()
|
||||
.child(left);
|
||||
if cmd.kind.edit_variant().is_some() {
|
||||
row = row.child(div().text_xs().text_color(muted).child("→ edit"));
|
||||
row = row.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(muted)
|
||||
.child(crate::ui::i18n::t(crate::ui::i18n::L10nKey::EditHint)),
|
||||
);
|
||||
}
|
||||
if let Some(tokens) = keys {
|
||||
row = row.child(h_flex().gap_1().children(tokens.into_iter().map(move |t| {
|
||||
@@ -958,8 +974,8 @@ impl PaletteView {
|
||||
fn search_placeholder(&self) -> &'static str {
|
||||
match self.menu {
|
||||
PaletteMenu::SshConnect => "user@host [-p 2222 -J jump]",
|
||||
PaletteMenu::Root => "Search or type user@host to connect…",
|
||||
PaletteMenu::Theme => "Search…",
|
||||
PaletteMenu::Root => t(crate::ui::i18n::L10nKey::SearchCommandsOrHost),
|
||||
PaletteMenu::Theme => t(crate::ui::i18n::L10nKey::SearchTheme),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1094,6 +1110,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn host_like_queries_get_connect_and_save_rows() {
|
||||
crate::ui::i18n::set_locale("en");
|
||||
for q in [
|
||||
"deploy@10.0.0.5",
|
||||
"host.example.com",
|
||||
@@ -1105,8 +1122,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
titles,
|
||||
vec![
|
||||
format!("Connect to \"{q}\""),
|
||||
format!("Save \"{q}\" as profile…"),
|
||||
t_fmt(L10nKey::CmdQuickConnect, &[("target", q)]),
|
||||
t_fmt(L10nKey::CmdQuickConnectSaveProfile, &[("target", q)]),
|
||||
],
|
||||
"query {q:?}"
|
||||
);
|
||||
|
||||
+10
-13
@@ -9,6 +9,7 @@ use gpui_component::{ActiveTheme as _, Icon, IconName, Sizable as _, h_flex, v_f
|
||||
|
||||
use crate::daemon::protocol::ShellSpec;
|
||||
use crate::terminal::PaneWorkspace;
|
||||
use crate::ui::i18n::{L10nKey, t_fmt};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PendingSpawn {
|
||||
@@ -93,23 +94,19 @@ impl Render for PendingPane {
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(muted)
|
||||
.child(format!("Connecting to {}…", self.machine)),
|
||||
)
|
||||
.child(div().text_sm().text_color(muted).child(t_fmt(
|
||||
L10nKey::PendingConnecting,
|
||||
&[("machine", &self.machine)],
|
||||
)))
|
||||
.into_any_element(),
|
||||
PendingState::Failed(reason) => v_flex()
|
||||
.items_center()
|
||||
.gap(px(10.))
|
||||
.max_w(px(420.))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(theme.foreground)
|
||||
.child(format!("Couldn't reach {}", self.machine)),
|
||||
)
|
||||
.child(div().text_sm().text_color(theme.foreground).child(t_fmt(
|
||||
L10nKey::PendingUnreachable,
|
||||
&[("machine", &self.machine)],
|
||||
)))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
@@ -119,7 +116,7 @@ impl Render for PendingPane {
|
||||
)
|
||||
.child(
|
||||
Button::new("pending-pane-retry")
|
||||
.label("Try Again")
|
||||
.label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::TryAgain))
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
|
||||
@@ -499,6 +499,9 @@ pub fn fork_to_file(t: &Theme) -> std::io::Result<String> {
|
||||
n += 1;
|
||||
}
|
||||
let mut copy = t.clone();
|
||||
// The name lands in the YAML on disk and is matched back with
|
||||
// `trim_end_matches(" (custom)")`, so it stays English in every locale —
|
||||
// a translated suffix would survive a language switch and stack up.
|
||||
copy.name = format!("{} (custom)", t.name.trim_end_matches(" (custom)"));
|
||||
crate::core::config::write_atomic(
|
||||
&dir.join(format!("{stem}.yaml")),
|
||||
@@ -592,6 +595,8 @@ fn id_and_name(path: &std::path::Path) -> (String, String) {
|
||||
(
|
||||
stem,
|
||||
if name.is_empty() {
|
||||
// Theme names are data — they get written back to the YAML file,
|
||||
// so the fallback stays English rather than following the GUI.
|
||||
"Theme".into()
|
||||
} else {
|
||||
name
|
||||
@@ -1442,6 +1447,15 @@ ansi:
|
||||
assert_eq!(name, "Solarized Dark");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_names_stay_english_under_a_translated_gui() {
|
||||
crate::ui::i18n::set_locale("zh-CN");
|
||||
// A stem of only separators leaves nothing to title-case.
|
||||
let (_, name) = id_and_name(std::path::Path::new("/x/_.yaml"));
|
||||
assert_eq!(name, "Theme");
|
||||
crate::ui::i18n::set_locale("en");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mix_blends_channels() {
|
||||
assert_eq!(mix(0x000000, 0xffffff, 0.0), 0x000000);
|
||||
|
||||
+130
-69
@@ -15,6 +15,7 @@ use crate::daemon::install::{
|
||||
};
|
||||
use crate::daemon::protocol::{AuthPromptKind, AuthResponse, NativeSshSpec};
|
||||
use crate::daemon::router::RouteHeader;
|
||||
use crate::ui::i18n::{L10nKey, t, t_fmt};
|
||||
use tty7_core::host::remote::RemoteHost;
|
||||
use tty7_core::host::{Host as _, HostId};
|
||||
|
||||
@@ -106,12 +107,14 @@ fn local_stdio_host() -> Option<HostChoice> {
|
||||
};
|
||||
Some(HostChoice {
|
||||
label: format!("{target}"),
|
||||
detail: format!("{program} --stdio (this computer)"),
|
||||
detail: format!("{program} --stdio ({})", t(L10nKey::RemoteThisComputer)),
|
||||
target,
|
||||
})
|
||||
}
|
||||
|
||||
const WSL_DETAIL: &str = "WSL · this computer";
|
||||
fn wsl_detail() -> String {
|
||||
format!("WSL · {}", t(L10nKey::RemoteThisComputer))
|
||||
}
|
||||
|
||||
fn wsl_hosts(cx: &App) -> Vec<HostChoice> {
|
||||
let names = cx
|
||||
@@ -129,7 +132,7 @@ fn wsl_choices(names: &[String]) -> Vec<HostChoice> {
|
||||
distro: distro.clone(),
|
||||
},
|
||||
label: distro.clone(),
|
||||
detail: WSL_DETAIL.to_string(),
|
||||
detail: wsl_detail(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -197,7 +200,7 @@ pub fn spec_for(target: &RemoteTarget, cx: &App) -> Result<NativeSshSpec, String
|
||||
.ssh_profiles
|
||||
.iter()
|
||||
.find(|p| p.id == *id)
|
||||
.ok_or_else(|| "that saved SSH profile no longer exists".to_string())?;
|
||||
.ok_or_else(|| t(L10nKey::RemoteProfileMissing).to_string())?;
|
||||
Ok(crate::ui::ssh_connect::build_native_ssh_spec(
|
||||
profile,
|
||||
&cfg.ssh_profiles,
|
||||
@@ -207,7 +210,7 @@ pub fn spec_for(target: &RemoteTarget, cx: &App) -> Result<NativeSshSpec, String
|
||||
}
|
||||
RemoteTarget::Alias { alias } => {
|
||||
let resolved = crate::core::ssh_config::resolve_alias_to_profile(alias)
|
||||
.ok_or_else(|| format!("`{alias}` is no longer in ~/.ssh/config"))?;
|
||||
.ok_or_else(|| t_fmt(L10nKey::RemoteAliasMissing, &[("alias", alias)]))?;
|
||||
Ok(crate::ui::ssh_connect::native_spec_from_transient_profile(
|
||||
&resolved.profile,
|
||||
resolved.proxy_jump,
|
||||
@@ -228,10 +231,8 @@ pub fn spec_for(target: &RemoteTarget, cx: &App) -> Result<NativeSshSpec, String
|
||||
cfg.verify_host_keys,
|
||||
))
|
||||
}
|
||||
RemoteTarget::Wsl { .. } => Err("a WSL workspace has no SSH connection".to_string()),
|
||||
RemoteTarget::LocalStdio { .. } => {
|
||||
Err("a local --stdio workspace has no SSH connection".to_string())
|
||||
}
|
||||
RemoteTarget::Wsl { .. } => Err(t(L10nKey::RemoteWslNoSsh).to_string()),
|
||||
RemoteTarget::LocalStdio { .. } => Err(t(L10nKey::RemoteLocalStdioNoSsh).to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,22 +261,42 @@ pub fn connect_blocking(
|
||||
label: &str,
|
||||
) -> Result<Connected, String> {
|
||||
note_origin(&header.target, target);
|
||||
crate::daemon::spawn::ensure_running()
|
||||
.map_err(|e| format!("tty7's local server could not be started: {e}"))?;
|
||||
crate::daemon::spawn::ensure_running().map_err(|e| {
|
||||
t_fmt(
|
||||
L10nKey::RemoteDaemonStartFailed,
|
||||
&[("error", &e.to_string())],
|
||||
)
|
||||
})?;
|
||||
|
||||
let stream = crate::daemon::transport::connect()
|
||||
.map_err(|e| format!("could not reach tty7's local server: {e}"))?;
|
||||
let stream = crate::daemon::transport::connect().map_err(|e| {
|
||||
t_fmt(
|
||||
L10nKey::RemoteDaemonUnreachable,
|
||||
&[("error", &e.to_string())],
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut stream = stream;
|
||||
crate::daemon::router::negotiate(&mut stream, &header)
|
||||
.map_err(|e| format!("could not reach {label}: {e}"))?;
|
||||
crate::daemon::router::negotiate(&mut stream, &header).map_err(|e| {
|
||||
t_fmt(
|
||||
L10nKey::RemoteHostUnreachable,
|
||||
&[("machine", label), ("error", &e.to_string())],
|
||||
)
|
||||
})?;
|
||||
|
||||
let hello = ControlHello::host_rpc(new_session_token(), client_hostname());
|
||||
let host = handshake(stream, &target.connection_key(), &hello)
|
||||
.map_err(|e| format!("{label} answered, but not as a tty7 server: {e}"))?;
|
||||
let host = handshake(stream, &target.connection_key(), &hello).map_err(|e| {
|
||||
t_fmt(
|
||||
L10nKey::RemoteHostNotTty7,
|
||||
&[("machine", label), ("error", &e.to_string())],
|
||||
)
|
||||
})?;
|
||||
|
||||
let rows = list_workspaces(&host)
|
||||
.map_err(|e| format!("connected to {label}, but its workspace list failed: {e}"))?;
|
||||
let rows = list_workspaces(&host).map_err(|e| {
|
||||
t_fmt(
|
||||
L10nKey::RemoteWorkspaceListFailed,
|
||||
&[("machine", label), ("error", &e.to_string())],
|
||||
)
|
||||
})?;
|
||||
let home = host.home();
|
||||
refresh_agent_hooks_once(&host, &home);
|
||||
Ok(Connected { host, home, rows })
|
||||
@@ -321,7 +342,10 @@ pub fn list_workspaces(host: &Arc<RemoteHost>) -> io::Result<Vec<RemoteWorkspace
|
||||
ReplyOk::MachineTree(machine) => Ok(rows_from_machine(&machine)),
|
||||
other => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("the server answered a machine tree with {other:?}"),
|
||||
t_fmt(
|
||||
L10nKey::RemoteMachineTreeUnexpectedReply,
|
||||
&[("reply", &format!("{other:?}"))],
|
||||
),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -405,36 +429,37 @@ impl HostLinks {
|
||||
}
|
||||
|
||||
pub fn install_detail(request: &InstallRequest) -> String {
|
||||
format!(
|
||||
"tty7 will write its server binary to {machine} so this machine can host \
|
||||
workspaces there. Nothing else on {machine} is touched, and no sudo is used.\n\
|
||||
\n\
|
||||
Path\u{2003}{path}\n\
|
||||
Version\u{2003}{version} ({asset})\n\
|
||||
Size\u{2003}{size}\n\
|
||||
From\u{2003}{url}\n\
|
||||
SHA-256\u{2003}{sha}\n\
|
||||
\n\
|
||||
Later upgrades on this machine install silently.",
|
||||
machine = request.host,
|
||||
path = request.remote_path,
|
||||
version = request.version,
|
||||
asset = request.asset,
|
||||
size = human_bytes(request.size_bytes),
|
||||
url = request.source_url,
|
||||
sha = request.sha256,
|
||||
t_fmt(
|
||||
L10nKey::RemoteInstallDetail,
|
||||
&[
|
||||
("machine", &request.host),
|
||||
("path", &request.remote_path),
|
||||
(
|
||||
"version",
|
||||
&format!("{} ({})", request.version, request.asset),
|
||||
),
|
||||
("size", &human_bytes(request.size_bytes)),
|
||||
("from", &request.source_url),
|
||||
("sha256", &request.sha256),
|
||||
("path_label", t(L10nKey::RemoteInstallPathLabel)),
|
||||
("version_label", t(L10nKey::RemoteInstallVersionLabel)),
|
||||
("size_label", t(L10nKey::RemoteInstallSizeLabel)),
|
||||
("from_label", t(L10nKey::RemoteInstallFromLabel)),
|
||||
("sha_label", t(L10nKey::RemoteInstallShaLabel)),
|
||||
("silent_upgrades", t(L10nKey::RemoteInstallSilentUpgrades)),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn install_title(request: &InstallRequest) -> String {
|
||||
format!("Install tty7's server on \u{201c}{}\u{201d}?", request.host)
|
||||
t_fmt(L10nKey::RemoteInstallTitle, &[("machine", &request.host)])
|
||||
}
|
||||
|
||||
pub fn human_bytes(n: u64) -> String {
|
||||
const KIB: f64 = 1024.0;
|
||||
let n = n as f64;
|
||||
if n < KIB {
|
||||
return format!("{} bytes", n as u64);
|
||||
return format!("{} {}", n as u64, t(L10nKey::RemoteInstallBytes));
|
||||
}
|
||||
let units = ["KiB", "MiB", "GiB"];
|
||||
let mut value = n / KIB;
|
||||
@@ -615,29 +640,34 @@ pub(crate) fn claim_mailbox() -> std::sync::MutexGuard<'static, ()> {
|
||||
MAILBOX_TURN.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
pub const MISMATCH_ANSWERS: [&str; 2] = ["Cancel", "Restart Server"];
|
||||
pub fn mismatch_answers() -> [&'static str; 2] {
|
||||
[t(L10nKey::Cancel), t(L10nKey::RestartServer)]
|
||||
}
|
||||
|
||||
pub fn mismatch_detail(m: &MismatchedRemoteDaemon) -> String {
|
||||
let running = match (&m.running_version, &m.running_exe) {
|
||||
(Some(v), Some(exe)) => format!("{v} (from {exe})"),
|
||||
(Some(v), Some(exe)) => t_fmt(
|
||||
L10nKey::RemoteMismatchVersionFromExe,
|
||||
&[("version", v), ("exe", exe)],
|
||||
),
|
||||
(Some(v), None) => v.clone(),
|
||||
(None, Some(exe)) => format!("an unknown build (from {exe})"),
|
||||
(None, None) => "an unknown build".to_string(),
|
||||
(None, Some(exe)) => t_fmt(L10nKey::RemoteMismatchUnknownBuildFromExe, &[("exe", exe)]),
|
||||
(None, None) => t(L10nKey::RemoteMismatchUnknownBuild).to_string(),
|
||||
};
|
||||
format!(
|
||||
"{host} is serving tty7 sessions from {running}, which speaks a protocol \
|
||||
this client ({wanted}) cannot. tty7 has installed a matching server there, \
|
||||
but the one already running is the one your sessions are on.\n\
|
||||
\n\
|
||||
Restart Server\u{2003}starts {wanted} there and ends every session it is hosting.\n\
|
||||
Cancel\u{2003}leaves {host} exactly as it is. This window will not connect.",
|
||||
host = m.host,
|
||||
wanted = m.wanted_version,
|
||||
t_fmt(
|
||||
L10nKey::RemoteMismatchDetail,
|
||||
&[
|
||||
("machine", &m.host),
|
||||
("running", &running),
|
||||
("wanted", &m.wanted_version),
|
||||
("restart_server", t(L10nKey::RestartServer)),
|
||||
("cancel", t(L10nKey::Cancel)),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn mismatch_title(m: &MismatchedRemoteDaemon) -> String {
|
||||
format!("Restart tty7's server on \u{201c}{}\u{201d}?", m.host)
|
||||
t_fmt(L10nKey::RemoteMismatchTitle, &[("machine", &m.host)])
|
||||
}
|
||||
|
||||
pub fn mismatch_target(m: &MismatchedRemoteDaemon) -> Option<RemoteTarget> {
|
||||
@@ -646,17 +676,26 @@ pub fn mismatch_target(m: &MismatchedRemoteDaemon) -> Option<RemoteTarget> {
|
||||
|
||||
pub fn restart_server_blocking(header: RouteHeader, label: &str) -> Result<(), String> {
|
||||
let action = header.action;
|
||||
crate::daemon::spawn::ensure_running()
|
||||
.map_err(|e| format!("tty7's local server could not be started: {e}"))?;
|
||||
let mut stream = crate::daemon::transport::connect()
|
||||
.map_err(|e| format!("could not reach tty7's local server: {e}"))?;
|
||||
let ack = crate::daemon::router::negotiate(&mut stream, &header)
|
||||
.map_err(|e| format!("could not restart tty7's server on {label}: {e}"))?;
|
||||
crate::daemon::spawn::ensure_running().map_err(|e| {
|
||||
t_fmt(
|
||||
L10nKey::RemoteDaemonStartFailed,
|
||||
&[("error", &e.to_string())],
|
||||
)
|
||||
})?;
|
||||
let mut stream = crate::daemon::transport::connect().map_err(|e| {
|
||||
t_fmt(
|
||||
L10nKey::RemoteDaemonUnreachable,
|
||||
&[("error", &e.to_string())],
|
||||
)
|
||||
})?;
|
||||
let ack = crate::daemon::router::negotiate(&mut stream, &header).map_err(|e| {
|
||||
t_fmt(
|
||||
L10nKey::RemoteServerRestartFailed,
|
||||
&[("machine", label), ("error", &e.to_string())],
|
||||
)
|
||||
})?;
|
||||
if !ack.performed(action) {
|
||||
return Err(format!(
|
||||
"this machine's tty7 daemon is an older build and cannot restart the server on \
|
||||
{label}. Quit tty7 (which stops the daemon) and open it again, then retry."
|
||||
));
|
||||
return Err(t_fmt(L10nKey::RemoteDaemonTooOld, &[("machine", label)]));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -679,6 +718,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn the_install_prompt_states_every_field_of_the_request() {
|
||||
crate::ui::i18n::set_locale("en");
|
||||
let request = request();
|
||||
let detail = install_detail(&request);
|
||||
for needle in [
|
||||
@@ -694,12 +734,22 @@ mod tests {
|
||||
);
|
||||
}
|
||||
assert!(detail.contains("9.0 MiB"), "{detail}");
|
||||
assert!(install_title(&request).contains("me@build-box:22"));
|
||||
assert_eq!(
|
||||
install_title(&request),
|
||||
t_fmt(
|
||||
L10nKey::RemoteInstallTitle,
|
||||
&[("machine", "me@build-box:22")]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn human_bytes_reads_in_binary_units() {
|
||||
assert_eq!(human_bytes(512), "512 bytes");
|
||||
crate::ui::i18n::set_locale("en");
|
||||
assert_eq!(
|
||||
human_bytes(512),
|
||||
format!("512 {}", t(L10nKey::RemoteInstallBytes))
|
||||
);
|
||||
assert_eq!(human_bytes(1024), "1.0 KiB");
|
||||
assert_eq!(human_bytes(1_572_864), "1.5 MiB");
|
||||
assert_eq!(human_bytes(3 * 1024 * 1024 * 1024), "3.0 GiB");
|
||||
@@ -859,6 +909,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn the_mismatch_prompt_names_the_host_and_both_versions() {
|
||||
crate::ui::i18n::set_locale("en");
|
||||
let m = MismatchedRemoteDaemon {
|
||||
host: "me@build-box:22".into(),
|
||||
running_version: Some("0.8.0".into()),
|
||||
@@ -869,25 +920,35 @@ mod tests {
|
||||
assert!(detail.contains("0.8.0"), "{detail}");
|
||||
assert!(detail.contains("0.9.1"), "{detail}");
|
||||
assert!(detail.contains("me@build-box:22"), "{detail}");
|
||||
assert!(mismatch_title(&m).contains("me@build-box:22"));
|
||||
assert_eq!(
|
||||
mismatch_title(&m),
|
||||
t_fmt(
|
||||
L10nKey::RemoteMismatchTitle,
|
||||
&[("machine", "me@build-box:22")]
|
||||
)
|
||||
);
|
||||
|
||||
let unknown = MismatchedRemoteDaemon {
|
||||
running_version: None,
|
||||
running_exe: None,
|
||||
..m
|
||||
};
|
||||
assert!(mismatch_detail(&unknown).contains("an unknown build"));
|
||||
assert!(
|
||||
mismatch_detail(&unknown).contains(t(L10nKey::RemoteMismatchUnknownBuild)),
|
||||
"{detail}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_mismatch_detail_explains_every_answer_the_prompt_offers() {
|
||||
crate::ui::i18n::set_locale("en");
|
||||
let detail = mismatch_detail(&MismatchedRemoteDaemon {
|
||||
host: "me@build-box:22".into(),
|
||||
running_version: Some("0.8.0".into()),
|
||||
running_exe: None,
|
||||
wanted_version: "0.9.1".into(),
|
||||
});
|
||||
for answer in MISMATCH_ANSWERS {
|
||||
for answer in mismatch_answers() {
|
||||
assert!(detail.contains(answer), "{answer} is unexplained: {detail}");
|
||||
}
|
||||
}
|
||||
|
||||
+83
-78
@@ -11,6 +11,7 @@ use crate::core::session::{RemoteRef, RemoteTarget, WorkspaceId, WorkspaceStore}
|
||||
use crate::daemon::control::{ControlEvent, ControlRequest, ReplyOk};
|
||||
use crate::daemon::install::InstallDecision;
|
||||
use crate::ui::app::Tty7App;
|
||||
use crate::ui::i18n::{L10nKey, t, t_fmt};
|
||||
use crate::ui::remote_connect::{self, HostChoice, RemoteWorkspaceRow};
|
||||
|
||||
pub enum ConnectFlow {
|
||||
@@ -40,35 +41,47 @@ impl RemoteStatus {
|
||||
pub fn strip_message(&self, machine: &str) -> Option<String> {
|
||||
match self {
|
||||
RemoteStatus::Attached => None,
|
||||
RemoteStatus::Disconnected => Some(format!("Not connected to {machine}")),
|
||||
RemoteStatus::Connecting => Some(format!("Connecting to {machine}…")),
|
||||
RemoteStatus::Reconnecting { attempt: 0 } => {
|
||||
Some(format!("Reconnecting to {machine}…"))
|
||||
RemoteStatus::Disconnected => Some(t_fmt(
|
||||
L10nKey::RemoteStripDisconnected,
|
||||
&[("machine", machine)],
|
||||
)),
|
||||
RemoteStatus::Connecting => Some(t_fmt(
|
||||
L10nKey::RemoteStripConnecting,
|
||||
&[("machine", machine)],
|
||||
)),
|
||||
RemoteStatus::Reconnecting { attempt: 0 } => Some(t_fmt(
|
||||
L10nKey::RemoteStripReconnecting,
|
||||
&[("machine", machine)],
|
||||
)),
|
||||
RemoteStatus::Reconnecting { attempt } => Some(t_fmt(
|
||||
L10nKey::RemoteStripReconnectingAttempt,
|
||||
&[("machine", machine), ("count", &(attempt + 1).to_string())],
|
||||
)),
|
||||
RemoteStatus::Preempted { by } => {
|
||||
Some(t_fmt(L10nKey::RemoteStripPreempted, &[("by", by)]))
|
||||
}
|
||||
RemoteStatus::Reconnecting { attempt } => Some(format!(
|
||||
"Reconnecting to {machine}… (attempt {})",
|
||||
attempt + 1
|
||||
RemoteStatus::Failed(e) => Some(t_fmt(
|
||||
L10nKey::RemoteStripFailed,
|
||||
&[("machine", machine), ("error", e)],
|
||||
)),
|
||||
RemoteStatus::Preempted { by } => Some(format!("This workspace was opened on {by}")),
|
||||
RemoteStatus::Failed(e) => Some(format!("Not connected to {machine} — {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn input_notice(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
RemoteStatus::Attached => None,
|
||||
RemoteStatus::Preempted { .. } => Some("Opened elsewhere — typing has no effect"),
|
||||
_ => Some("Not connected — typing has no effect"),
|
||||
RemoteStatus::Preempted { .. } => Some(t(L10nKey::RemoteNoticePreempted)),
|
||||
_ => Some(t(L10nKey::RemoteNoticeDisconnected)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn action_label(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
RemoteStatus::Attached | RemoteStatus::Connecting => None,
|
||||
RemoteStatus::Reconnecting { .. } => Some("Retry Now"),
|
||||
RemoteStatus::Preempted { .. } => Some("Take Back"),
|
||||
RemoteStatus::Disconnected => Some("Connect"),
|
||||
RemoteStatus::Failed(_) => Some("Retry"),
|
||||
RemoteStatus::Reconnecting { .. } => Some(t(L10nKey::RemoteActionRetryNow)),
|
||||
RemoteStatus::Preempted { .. } => Some(t(L10nKey::RemoteActionTakeBack)),
|
||||
RemoteStatus::Disconnected => Some(t(L10nKey::RemoteActionConnect)),
|
||||
RemoteStatus::Failed(_) => Some(t(L10nKey::RemoteActionRetry)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,11 +197,7 @@ impl Tty7App {
|
||||
_ => {
|
||||
let machine = self.remote_machine_label(cx);
|
||||
window.push_notification(
|
||||
format!(
|
||||
"This window is a workspace on {machine}, but tty7 has no connection \
|
||||
details for it any more — check that its SSH profile or ~/.ssh/config \
|
||||
entry still exists."
|
||||
),
|
||||
t_fmt(L10nKey::RemoteNoConnectionDetails, &[("machine", &machine)]),
|
||||
cx,
|
||||
);
|
||||
false
|
||||
@@ -206,7 +215,7 @@ impl Tty7App {
|
||||
pub(crate) fn remote_machine_label(&self, cx: &gpui::App) -> String {
|
||||
match WorkspaceStore::remote_ref(cx, self.workspace) {
|
||||
Some(host) => host.target.to_string(),
|
||||
None => "this computer".to_string(),
|
||||
None => t(L10nKey::RemoteThisComputer).to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,7 +446,7 @@ impl Tty7App {
|
||||
PromptLevel::Warning,
|
||||
&title,
|
||||
Some(&detail),
|
||||
&["Cancel", "Install"],
|
||||
&[t(L10nKey::Cancel), t(L10nKey::SettingsInstall)],
|
||||
cx,
|
||||
);
|
||||
cx.spawn(async move |_, _| {
|
||||
@@ -458,7 +467,7 @@ impl Tty7App {
|
||||
PromptLevel::Warning,
|
||||
&title,
|
||||
Some(&detail),
|
||||
&remote_connect::MISMATCH_ANSWERS,
|
||||
&remote_connect::mismatch_answers(),
|
||||
cx,
|
||||
);
|
||||
cx.spawn(async move |this, cx| {
|
||||
@@ -481,7 +490,7 @@ impl Tty7App {
|
||||
) {
|
||||
let label = mismatch.host.clone();
|
||||
match remote_connect::mismatch_target(&mismatch)
|
||||
.ok_or_else(|| format!("tty7 no longer has a way to reach {label}"))
|
||||
.ok_or_else(|| t_fmt(L10nKey::RemoteNoRouteToHost, &[("machine", &label)]))
|
||||
{
|
||||
Ok(target) => self.restart_remote_server(target, label, window, cx),
|
||||
Err(e) => Tty7App::report_restart_failure(&label, &e, window, cx),
|
||||
@@ -497,13 +506,9 @@ impl Tty7App {
|
||||
) {
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Warning,
|
||||
&format!("Restart tty7's server on \u{201c}{label}\u{201d}?"),
|
||||
Some(&format!(
|
||||
"This stops every shell on {label} — anything still running in them \
|
||||
will be terminated, including shells this window is not showing. \
|
||||
Workspaces and layouts are kept and come back with fresh shells."
|
||||
)),
|
||||
&["Cancel", "Restart Server"],
|
||||
&t_fmt(L10nKey::RemoteRestartTitle, &[("machine", &label)]),
|
||||
Some(&t_fmt(L10nKey::RemoteRestartBody, &[("machine", &label)])),
|
||||
&[t(L10nKey::Cancel), t(L10nKey::RestartServer)],
|
||||
cx,
|
||||
);
|
||||
cx.spawn(async move |this, cx| {
|
||||
@@ -567,16 +572,9 @@ impl Tty7App {
|
||||
) {
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Warning,
|
||||
&format!("Restart tty7's server on \u{201c}{label}\u{201d}?"),
|
||||
Some(&format!(
|
||||
"The tty7-server running on {label} speaks a protocol this client cannot. \
|
||||
tty7 will restart the service there onto one that does, installing it \
|
||||
first if {label} does not already have it.\n\
|
||||
\n\
|
||||
Every session running on {label} ends, including any this window is not \
|
||||
connected to."
|
||||
)),
|
||||
&["Cancel", "Restart Server"],
|
||||
&t_fmt(L10nKey::RemoteRestartTitle, &[("machine", &label)]),
|
||||
Some(&t_fmt(L10nKey::RemoteReplaceBody, &[("machine", &label)])),
|
||||
&[t(L10nKey::Cancel), t(L10nKey::RestartServer)],
|
||||
cx,
|
||||
);
|
||||
cx.spawn(async move |this, cx| {
|
||||
@@ -640,12 +638,12 @@ impl Tty7App {
|
||||
) {
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Warning,
|
||||
&format!("tty7's server on \u{201c}{label}\u{201d} was not restarted"),
|
||||
Some(&format!(
|
||||
"{error}\n\nSessions still running there are on the older build. If they are \
|
||||
gone, reconnecting starts this build's server."
|
||||
&t_fmt(L10nKey::RemoteRestartFailedTitle, &[("machine", label)]),
|
||||
Some(&t_fmt(
|
||||
L10nKey::RemoteRestartFailedBody,
|
||||
&[("error", error)],
|
||||
)),
|
||||
&["OK"],
|
||||
&[t(L10nKey::Ok)],
|
||||
cx,
|
||||
);
|
||||
cx.spawn(async move |_, _| {
|
||||
@@ -1441,24 +1439,28 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn the_status_strip_speaks_unless_everything_is_working() {
|
||||
crate::ui::i18n::set_locale("en");
|
||||
assert_eq!(RemoteStatus::Attached.strip_message("build-box"), None);
|
||||
assert_eq!(
|
||||
RemoteStatus::Disconnected
|
||||
.strip_message("build-box")
|
||||
.as_deref(),
|
||||
Some("Not connected to build-box")
|
||||
RemoteStatus::Disconnected.strip_message("build-box"),
|
||||
Some(t_fmt(
|
||||
L10nKey::RemoteStripDisconnected,
|
||||
&[("machine", "build-box")]
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
RemoteStatus::Connecting
|
||||
.strip_message("build-box")
|
||||
.as_deref(),
|
||||
Some("Connecting to build-box…")
|
||||
RemoteStatus::Connecting.strip_message("build-box"),
|
||||
Some(t_fmt(
|
||||
L10nKey::RemoteStripConnecting,
|
||||
&[("machine", "build-box")]
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
RemoteStatus::Failed("connection refused".into())
|
||||
.strip_message("build-box")
|
||||
.as_deref(),
|
||||
Some("Not connected to build-box — connection refused")
|
||||
RemoteStatus::Failed("connection refused".into()).strip_message("build-box"),
|
||||
Some(t_fmt(
|
||||
L10nKey::RemoteStripFailed,
|
||||
&[("machine", "build-box"), ("error", "connection refused")]
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1626,39 +1628,40 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn every_state_says_what_it_means_for_the_keyboard() {
|
||||
crate::ui::i18n::set_locale("en");
|
||||
let cases = [
|
||||
(RemoteStatus::Attached, true, None, None),
|
||||
(
|
||||
RemoteStatus::Disconnected,
|
||||
false,
|
||||
Some("Not connected — typing has no effect"),
|
||||
Some("Connect"),
|
||||
Some(t(L10nKey::RemoteNoticeDisconnected)),
|
||||
Some(t(L10nKey::RemoteActionConnect)),
|
||||
),
|
||||
(
|
||||
RemoteStatus::Connecting,
|
||||
false,
|
||||
Some("Not connected — typing has no effect"),
|
||||
Some(t(L10nKey::RemoteNoticeDisconnected)),
|
||||
None,
|
||||
),
|
||||
(
|
||||
RemoteStatus::Reconnecting { attempt: 2 },
|
||||
false,
|
||||
Some("Not connected — typing has no effect"),
|
||||
Some("Retry Now"),
|
||||
Some(t(L10nKey::RemoteNoticeDisconnected)),
|
||||
Some(t(L10nKey::RemoteActionRetryNow)),
|
||||
),
|
||||
(
|
||||
RemoteStatus::Preempted {
|
||||
by: "desktop".into(),
|
||||
},
|
||||
false,
|
||||
Some("Opened elsewhere — typing has no effect"),
|
||||
Some("Take Back"),
|
||||
Some(t(L10nKey::RemoteNoticePreempted)),
|
||||
Some(t(L10nKey::RemoteActionTakeBack)),
|
||||
),
|
||||
(
|
||||
RemoteStatus::Failed("no route to host".into()),
|
||||
false,
|
||||
Some("Not connected — typing has no effect"),
|
||||
Some("Retry"),
|
||||
Some(t(L10nKey::RemoteNoticeDisconnected)),
|
||||
Some(t(L10nKey::RemoteActionRetry)),
|
||||
),
|
||||
];
|
||||
for (status, accepts, notice, action) in cases {
|
||||
@@ -1670,26 +1673,28 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn the_new_states_name_what_happened() {
|
||||
crate::ui::i18n::set_locale("en");
|
||||
assert_eq!(
|
||||
RemoteStatus::Reconnecting { attempt: 0 }
|
||||
.strip_message("build-box")
|
||||
.as_deref(),
|
||||
Some("Reconnecting to build-box…"),
|
||||
RemoteStatus::Reconnecting { attempt: 0 }.strip_message("build-box"),
|
||||
Some(t_fmt(
|
||||
L10nKey::RemoteStripReconnecting,
|
||||
&[("machine", "build-box")]
|
||||
)),
|
||||
"the first attempt does not need a count"
|
||||
);
|
||||
assert_eq!(
|
||||
RemoteStatus::Reconnecting { attempt: 3 }
|
||||
.strip_message("build-box")
|
||||
.as_deref(),
|
||||
Some("Reconnecting to build-box… (attempt 4)")
|
||||
RemoteStatus::Reconnecting { attempt: 3 }.strip_message("build-box"),
|
||||
Some(t_fmt(
|
||||
L10nKey::RemoteStripReconnectingAttempt,
|
||||
&[("machine", "build-box"), ("count", "4")]
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
RemoteStatus::Preempted {
|
||||
by: "desktop".into()
|
||||
}
|
||||
.strip_message("build-box")
|
||||
.as_deref(),
|
||||
Some("This workspace was opened on desktop")
|
||||
.strip_message("build-box"),
|
||||
Some(t_fmt(L10nKey::RemoteStripPreempted, &[("by", "desktop")]))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+48
-41
@@ -14,6 +14,7 @@ use crate::ui::app::{
|
||||
CONTENT_INSET, TILE_GLYPH_SM, TILE_SIZE_SM, Tty7App, tile_trailing_inset,
|
||||
tile_trailing_inset_sm,
|
||||
};
|
||||
use crate::ui::i18n::{L10nKey, t, t_plural};
|
||||
use crate::ui::scrollbar::with_vertical_scrollbar;
|
||||
|
||||
pub(crate) const MIN_WIDTH: f32 = 216.;
|
||||
@@ -355,7 +356,7 @@ impl Tty7App {
|
||||
}
|
||||
|
||||
fn render_panel_info(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let title = self.panel_title("Info", None, None, window, cx);
|
||||
let title = self.panel_title(t(L10nKey::PanelInfoTitle), None, None, window, cx);
|
||||
let mut rows: Vec<(&'static str, String)> = Vec::new();
|
||||
let mut cwd_for_actions: Option<PathBuf> = None;
|
||||
let mut pane_id: Option<u64> = None;
|
||||
@@ -370,16 +371,16 @@ impl Tty7App {
|
||||
.map(|p| p.to_path_buf())
|
||||
.or_else(|| view.cwd())
|
||||
{
|
||||
rows.push(("cwd", compact_path(&cwd)));
|
||||
rows.push((t(L10nKey::PanelCwd), compact_path(&cwd)));
|
||||
cwd_for_actions = Some(cwd);
|
||||
}
|
||||
let shell = match view.shell_spec().map(|s| s.program.clone()) {
|
||||
Some(program) => crate::core::shells::default_shell_name(Some(&program)),
|
||||
None => self.default_shell_label(cx),
|
||||
};
|
||||
rows.push(("shell", shell));
|
||||
rows.push((t(L10nKey::PanelShell), shell));
|
||||
if let Some(ssh) = view.ssh_spec() {
|
||||
rows.push(("ssh", ssh.host.clone()));
|
||||
rows.push((t(L10nKey::PanelSsh), ssh.host.clone()));
|
||||
}
|
||||
let connected_ssh = view
|
||||
.remote_context()
|
||||
@@ -393,8 +394,11 @@ impl Tty7App {
|
||||
}
|
||||
}
|
||||
if let Some(git) = tab.git_status(Some(window), cx) {
|
||||
rows.push(("branch", git.branch.clone()));
|
||||
rows.push(("changes", format!("+{} −{}", git.added, git.removed)));
|
||||
rows.push((t(L10nKey::PanelBranch), git.branch.clone()));
|
||||
rows.push((
|
||||
t(L10nKey::PanelChangesRow),
|
||||
format!("+{} −{}", git.added, git.removed),
|
||||
));
|
||||
}
|
||||
if let Some(agent) = tab.agent(cx) {
|
||||
let name = agent.display_name();
|
||||
@@ -402,15 +406,15 @@ impl Tty7App {
|
||||
Some(s) => format!("{name} · {}", agent_status_label(s)),
|
||||
None => name.to_string(),
|
||||
};
|
||||
rows.push(("agent", status));
|
||||
rows.push((t(L10nKey::PanelAgent), status));
|
||||
}
|
||||
}
|
||||
|
||||
if rows.is_empty() {
|
||||
return self.panel_scroll(
|
||||
self.panel_empty(
|
||||
"No active session.",
|
||||
Some("Open a tab to see its shell, directory, and processes here."),
|
||||
t(L10nKey::PanelNoSession),
|
||||
Some(t(L10nKey::PanelNoSessionHint)),
|
||||
cx,
|
||||
),
|
||||
title,
|
||||
@@ -449,7 +453,7 @@ impl Tty7App {
|
||||
}
|
||||
|
||||
let inner = v_flex()
|
||||
.child(self.panel_subtitle("Session", false, None, cx))
|
||||
.child(self.panel_subtitle(t(L10nKey::PanelSessionSubtitle), false, None, cx))
|
||||
.child(list)
|
||||
.when_some(cwd_for_actions, |this, cwd| {
|
||||
this.child(self.cwd_actions(cwd, cx))
|
||||
@@ -491,7 +495,7 @@ impl Tty7App {
|
||||
cx,
|
||||
)
|
||||
.rounded_md()
|
||||
.tooltip("Copy Path")
|
||||
.tooltip(t(L10nKey::FileTreeContextCopyPath))
|
||||
.on_click(move |_, _window, cx| {
|
||||
cx.write_to_clipboard(gpui::ClipboardItem::new_string(
|
||||
cwd.display().to_string(),
|
||||
@@ -575,7 +579,7 @@ impl Tty7App {
|
||||
}
|
||||
Some(
|
||||
v_flex()
|
||||
.child(self.panel_subtitle("Processes", true, None, cx))
|
||||
.child(self.panel_subtitle(t(L10nKey::PanelProcessesSubtitle), true, None, cx))
|
||||
.child(list)
|
||||
.into_any_element(),
|
||||
)
|
||||
@@ -613,7 +617,7 @@ impl Tty7App {
|
||||
}
|
||||
Some(
|
||||
v_flex()
|
||||
.child(self.panel_subtitle("Ports", true, None, cx))
|
||||
.child(self.panel_subtitle(t(L10nKey::PanelPortsSubtitle), true, None, cx))
|
||||
.child(list)
|
||||
.into_any_element(),
|
||||
)
|
||||
@@ -708,11 +712,11 @@ impl Tty7App {
|
||||
.get(self.active)
|
||||
.and_then(|t| t.detail_pane(window, cx))
|
||||
else {
|
||||
let title = self.panel_title("Outline", None, None, window, cx);
|
||||
let title = self.panel_title(t(L10nKey::PanelOutlineTitle), None, None, window, cx);
|
||||
return self.panel_scroll(
|
||||
self.panel_empty(
|
||||
"No active session.",
|
||||
Some("Open a tab to see its shell, directory, and processes here."),
|
||||
t(L10nKey::PanelNoSession),
|
||||
Some(t(L10nKey::PanelNoSessionHint)),
|
||||
cx,
|
||||
),
|
||||
title,
|
||||
@@ -720,17 +724,23 @@ impl Tty7App {
|
||||
};
|
||||
let count = leaf.read(cx).command_marks().len();
|
||||
if count == 0 {
|
||||
let title = self.panel_title("Outline", None, None, window, cx);
|
||||
let title = self.panel_title(t(L10nKey::PanelOutlineTitle), None, None, window, cx);
|
||||
return self.panel_scroll(
|
||||
self.panel_empty(
|
||||
"No commands recorded for this pane.",
|
||||
Some("Run a command — shell integration marks each one so you can jump back to it."),
|
||||
t(L10nKey::PanelNoCommands),
|
||||
Some(t(L10nKey::PanelNoCommandsHint)),
|
||||
cx,
|
||||
),
|
||||
title,
|
||||
);
|
||||
}
|
||||
let title = self.panel_title("Outline", Some(count.to_string()), None, window, cx);
|
||||
let title = self.panel_title(
|
||||
t(L10nKey::PanelOutlineTitle),
|
||||
Some(count.to_string()),
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let mono = cx.theme().mono_font_family.clone();
|
||||
let mut list = v_flex().px(px(CONTENT_INSET - 4.)).py(px(2.)).gap(px(1.));
|
||||
@@ -812,11 +822,11 @@ impl Tty7App {
|
||||
});
|
||||
|
||||
let Some((host, cwd)) = target else {
|
||||
let title = self.panel_title("Changes", None, None, window, cx);
|
||||
let title = self.panel_title(t(L10nKey::PanelChangesTitle), None, None, window, cx);
|
||||
return self.panel_scroll(
|
||||
self.panel_empty(
|
||||
"No working directory.",
|
||||
Some("This pane has not reported one yet."),
|
||||
t(L10nKey::PanelNoWorkingDirectory),
|
||||
Some(t(L10nKey::PanelNoWorkingDirectoryHint)),
|
||||
cx,
|
||||
),
|
||||
title,
|
||||
@@ -838,20 +848,20 @@ impl Tty7App {
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let title = self.panel_title("Changes", count, None, window, cx);
|
||||
let title = self.panel_title(t(L10nKey::PanelChangesTitle), count, None, window, cx);
|
||||
let mono = cx.theme().mono_font_family.clone();
|
||||
|
||||
let inner = match &self.right_panel.diff {
|
||||
None => self.panel_empty("Loading…", None, cx),
|
||||
None => self.panel_empty(t(L10nKey::PanelLoading), None, cx),
|
||||
Some(None) => self.panel_empty(
|
||||
"Not a git repository.",
|
||||
Some("cd into one and this tab lists its uncommitted changes."),
|
||||
t(L10nKey::PanelNotAGitRepo),
|
||||
Some(t(L10nKey::PanelNotAGitRepoHint)),
|
||||
cx,
|
||||
),
|
||||
Some(Some(snap)) if snap.files.is_empty() && snap.untracked.is_empty() => self
|
||||
.panel_empty(
|
||||
"No uncommitted changes.",
|
||||
Some("The working tree is clean."),
|
||||
t(L10nKey::PanelNoChanges),
|
||||
Some(t(L10nKey::PanelNoChangesHint)),
|
||||
cx,
|
||||
),
|
||||
Some(Some(snap)) => {
|
||||
@@ -930,10 +940,7 @@ impl Tty7App {
|
||||
.py(px(3.))
|
||||
.text_size(px(11.5))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(format!(
|
||||
"… and {rest} more changed file{} — run `git diff` to see them.",
|
||||
if rest == 1 { "" } else { "s" }
|
||||
)),
|
||||
.child(t_plural(L10nKey::PanelMoreChangedFiles, rest, &[])),
|
||||
);
|
||||
}
|
||||
if untracked > 0 {
|
||||
@@ -952,7 +959,7 @@ impl Tty7App {
|
||||
div()
|
||||
.text_size(px(11.5))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(format!("{untracked} untracked")),
|
||||
.child(t_plural(L10nKey::PanelUntracked, untracked, &[])),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1007,7 +1014,7 @@ impl Tty7App {
|
||||
return self.render_panel_sftp(host.unwrap_or_default(), window, cx);
|
||||
}
|
||||
|
||||
let title = self.panel_title("Files", None, None, window, cx);
|
||||
let title = self.panel_title(t(L10nKey::PanelFilesTitle), None, None, window, cx);
|
||||
let search = self.panel_search(&self.file_search.clone(), cx);
|
||||
let rows = self.render_file_tree_rows(window, cx);
|
||||
v_flex()
|
||||
@@ -1071,19 +1078,19 @@ pub(crate) fn info_chip(
|
||||
|
||||
pub fn reveal_label() -> &'static str {
|
||||
if cfg!(target_os = "macos") {
|
||||
"Reveal in Finder"
|
||||
t(L10nKey::PanelRevealInFinder)
|
||||
} else {
|
||||
"Open Folder"
|
||||
t(L10nKey::PanelOpenFolder)
|
||||
}
|
||||
}
|
||||
|
||||
fn agent_status_label(status: crate::core::cli_agent::AgentStatus) -> &'static str {
|
||||
use crate::core::cli_agent::AgentStatus::*;
|
||||
match status {
|
||||
Idle => "idle",
|
||||
Working => "working",
|
||||
Waiting => "waiting",
|
||||
Done => "done",
|
||||
Idle => t(L10nKey::PanelAgentIdle),
|
||||
Working => t(L10nKey::PanelAgentWorking),
|
||||
Waiting => t(L10nKey::PanelAgentWaiting),
|
||||
Done => t(L10nKey::PanelAgentDone),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+785
-592
File diff suppressed because it is too large
Load Diff
+115
-57
@@ -20,6 +20,7 @@ use crate::daemon::protocol::{
|
||||
use crate::daemon::ssh::sftp::{remote_basename, remote_join, remote_parent, safe_local_name};
|
||||
use crate::terminal::RemoteTerminal;
|
||||
use crate::ui::app::{CONTENT_INSET, Tty7App};
|
||||
use crate::ui::i18n::{L10nKey, t, t_fmt};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum SftpMenuAction {
|
||||
@@ -70,7 +71,10 @@ impl SftpRoute {
|
||||
};
|
||||
match RemoteTerminal::on_workspace(req) {
|
||||
Ok(crate::daemon::protocol::DaemonMsg::SftpEntries(e)) => Ok(e),
|
||||
Ok(other) => Err(format!("unexpected reply: {other:?}")),
|
||||
Ok(other) => Err(t_fmt(
|
||||
L10nKey::SftpErrorUnexpectedReply,
|
||||
&[("reply", &format!("{other:?}"))],
|
||||
)),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -83,7 +87,10 @@ impl SftpRoute {
|
||||
};
|
||||
match RemoteTerminal::on_workspace(req) {
|
||||
Ok(crate::daemon::protocol::DaemonMsg::SftpOpResult(r)) => r,
|
||||
Ok(other) => SftpOpResult::Error(format!("unexpected reply: {other:?}")),
|
||||
Ok(other) => SftpOpResult::Error(t_fmt(
|
||||
L10nKey::SftpErrorUnexpectedReply,
|
||||
&[("reply", &format!("{other:?}"))],
|
||||
)),
|
||||
Err(e) => SftpOpResult::Error(e.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -98,7 +105,10 @@ impl SftpRoute {
|
||||
};
|
||||
match RemoteTerminal::on_workspace(req) {
|
||||
Ok(crate::daemon::protocol::DaemonMsg::SftpTransferStarted { job_id }) => Ok(job_id),
|
||||
Ok(other) => Err(format!("unexpected reply: {other:?}")),
|
||||
Ok(other) => Err(t_fmt(
|
||||
L10nKey::SftpErrorUnexpectedReply,
|
||||
&[("reply", &format!("{other:?}"))],
|
||||
)),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -146,7 +156,10 @@ pub(crate) struct SftpPanelState {
|
||||
|
||||
impl SftpPanelState {
|
||||
pub(crate) fn new(window: &mut Window, cx: &mut Context<Tty7App>) -> Self {
|
||||
let filter_input = cx.new(|cx| InputState::new(window, cx).placeholder("Search"));
|
||||
let filter_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Search))
|
||||
});
|
||||
let sub = cx.subscribe_in(&filter_input, window, |_this, _input, ev, _w, cx| {
|
||||
if matches!(ev, gpui_component::input::InputEvent::Change) {
|
||||
cx.notify();
|
||||
@@ -496,7 +509,10 @@ impl Tty7App {
|
||||
return;
|
||||
};
|
||||
if !safe_local_name(&entry.name) {
|
||||
self.sftp_panel.error = Some(format!("refusing unsafe remote name {:?}", entry.name));
|
||||
self.sftp_panel.error = Some(t_fmt(
|
||||
L10nKey::SftpErrorUnsafeRemoteName,
|
||||
&[("name", &format!("{:?}", entry.name))],
|
||||
));
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
@@ -595,13 +611,19 @@ impl Tty7App {
|
||||
}
|
||||
|
||||
pub(crate) fn sftp_begin_new_folder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder("New folder name"));
|
||||
let input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder(crate::ui::i18n::t(crate::ui::i18n::L10nKey::NewFolderName))
|
||||
});
|
||||
self.sftp_panel.editing = Some(SftpEdit::NewFolder(input));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn sftp_begin_new_file(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder("New file name"));
|
||||
let input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder(crate::ui::i18n::t(crate::ui::i18n::L10nKey::NewFileName))
|
||||
});
|
||||
self.sftp_panel.editing = Some(SftpEdit::NewFile(input));
|
||||
cx.notify();
|
||||
}
|
||||
@@ -685,7 +707,8 @@ impl Tty7App {
|
||||
mode,
|
||||
}),
|
||||
Err(_) => {
|
||||
self.sftp_panel.error = Some("invalid octal mode".to_string());
|
||||
self.sftp_panel.error =
|
||||
Some(t(L10nKey::SftpErrorInvalidOctalMode).to_string());
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
@@ -832,7 +855,13 @@ impl Tty7App {
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let controls = self.sftp_controls(cx);
|
||||
let title = self.panel_title("Files", Some(host), Some(controls), window, cx);
|
||||
let title = self.panel_title(
|
||||
t(L10nKey::SftpPanelTitleFiles),
|
||||
Some(host),
|
||||
Some(controls),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let breadcrumb = self.render_sftp_breadcrumb(cx);
|
||||
let filter = div()
|
||||
.id("panel-sftp-filter")
|
||||
@@ -885,7 +914,7 @@ impl Tty7App {
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
.tooltip("Refresh")
|
||||
.tooltip(t(L10nKey::SftpTooltipRefresh))
|
||||
.on_click(cx.listener(|this, _, _w, cx| this.sftp_refresh(cx))),
|
||||
),
|
||||
)
|
||||
@@ -897,16 +926,19 @@ impl Tty7App {
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
.tooltip("More")
|
||||
.tooltip(t(L10nKey::SftpTooltipMore))
|
||||
.dropdown_menu_with_anchor(gpui::Anchor::TopRight, {
|
||||
let app = cx.entity().downgrade();
|
||||
move |menu, _window, _cx| {
|
||||
let mut menu = menu.min_w(px(190.));
|
||||
for (label, action) in [
|
||||
("New folder", SftpMenuAction::NewFolder),
|
||||
("New file", SftpMenuAction::NewFile),
|
||||
("Upload…", SftpMenuAction::Upload),
|
||||
("Go to shell directory", SftpMenuAction::GotoShellCwd),
|
||||
(t(L10nKey::SftpMenuNewFolder), SftpMenuAction::NewFolder),
|
||||
(t(L10nKey::SftpMenuNewFile), SftpMenuAction::NewFile),
|
||||
(t(L10nKey::SftpMenuUpload), SftpMenuAction::Upload),
|
||||
(
|
||||
t(L10nKey::SftpMenuGotoShellCwd),
|
||||
SftpMenuAction::GotoShellCwd,
|
||||
),
|
||||
] {
|
||||
menu = menu.item(PopupMenuItem::new(label).on_click({
|
||||
let app = app.clone();
|
||||
@@ -919,9 +951,9 @@ impl Tty7App {
|
||||
}
|
||||
menu.separator().item(
|
||||
PopupMenuItem::new(if history {
|
||||
"Hide transfer history"
|
||||
t(L10nKey::SftpMenuHideTransferHistory)
|
||||
} else {
|
||||
"Transfer history"
|
||||
t(L10nKey::SftpMenuTransferHistory)
|
||||
})
|
||||
.on_click({
|
||||
let app = app.clone();
|
||||
@@ -1027,12 +1059,15 @@ impl Tty7App {
|
||||
let border = cx.theme().border;
|
||||
let foreground = cx.theme().foreground;
|
||||
let (title, input): (String, _) = match self.sftp_panel.editing.as_ref()? {
|
||||
SftpEdit::NewFolder(input) => ("New folder".to_string(), input),
|
||||
SftpEdit::NewFile(input) => ("New file".to_string(), input),
|
||||
SftpEdit::Rename { input, .. } => ("Rename".to_string(), input),
|
||||
SftpEdit::NewFolder(input) => (t(L10nKey::SftpEditNewFolder).to_string(), input),
|
||||
SftpEdit::NewFile(input) => (t(L10nKey::SftpEditNewFile).to_string(), input),
|
||||
SftpEdit::Rename { input, .. } => (t(L10nKey::SftpEditRename).to_string(), input),
|
||||
SftpEdit::Chmod {
|
||||
readable, input, ..
|
||||
} => (format!("Permissions · {readable}"), input),
|
||||
} => (
|
||||
t_fmt(L10nKey::SftpEditPermissions, &[("mode", readable)]),
|
||||
input,
|
||||
),
|
||||
};
|
||||
Some(
|
||||
v_flex()
|
||||
@@ -1058,14 +1093,14 @@ impl Tty7App {
|
||||
.justify_end()
|
||||
.child(
|
||||
Button::new("sftp-edit-cancel")
|
||||
.label("Cancel")
|
||||
.label(t(L10nKey::Cancel))
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(|this, _, _w, cx| this.sftp_cancel_edit(cx))),
|
||||
)
|
||||
.child(
|
||||
Button::new("sftp-edit-ok")
|
||||
.label("OK")
|
||||
.label(t(L10nKey::Ok))
|
||||
.xsmall()
|
||||
.primary()
|
||||
.on_click(cx.listener(|this, _, _w, cx| this.sftp_commit_edit(cx))),
|
||||
@@ -1105,12 +1140,12 @@ impl Tty7App {
|
||||
let show_go_up = self.sftp_panel.cwd != "/" && filter.trim().is_empty();
|
||||
|
||||
if entries.is_empty() && !show_go_up {
|
||||
let text = if self.sftp_panel.loading {
|
||||
"Loading…"
|
||||
let text: gpui::SharedString = if self.sftp_panel.loading {
|
||||
t(L10nKey::SftpLoading).into()
|
||||
} else {
|
||||
"Empty directory."
|
||||
t(L10nKey::SftpEmptyDirectory).into()
|
||||
};
|
||||
return container.child(note(text.into(), muted));
|
||||
return container.child(note(text, muted));
|
||||
}
|
||||
|
||||
let mut list = v_flex().gap(px(1.)).py(px(2.));
|
||||
@@ -1220,7 +1255,11 @@ impl Tty7App {
|
||||
) -> gpui_component::menu::PopupMenu {
|
||||
let mut menu = menu.min_w(px(180.));
|
||||
|
||||
let primary_label = if dir_like { "Open" } else { "Download" };
|
||||
let primary_label = if dir_like {
|
||||
t(L10nKey::SftpContextOpen)
|
||||
} else {
|
||||
t(L10nKey::Download)
|
||||
};
|
||||
menu = menu.item(PopupMenuItem::new(primary_label).on_click({
|
||||
let app = app.clone();
|
||||
let entry = entry.clone();
|
||||
@@ -1231,18 +1270,20 @@ impl Tty7App {
|
||||
}));
|
||||
|
||||
if is_symlink {
|
||||
menu = menu.item(PopupMenuItem::new("Follow symlink").on_click({
|
||||
let app = app.clone();
|
||||
let entry = entry.clone();
|
||||
move |_, _window, cx| {
|
||||
menu = menu.item(
|
||||
PopupMenuItem::new(t(L10nKey::SftpContextFollowSymlink)).on_click({
|
||||
let app = app.clone();
|
||||
let entry = entry.clone();
|
||||
let _ = app.update(cx, |this, cx| this.sftp_follow_symlink(entry, cx));
|
||||
}
|
||||
}));
|
||||
move |_, _window, cx| {
|
||||
let entry = entry.clone();
|
||||
let _ = app.update(cx, |this, cx| this.sftp_follow_symlink(entry, cx));
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
menu = menu
|
||||
.item(PopupMenuItem::new("Rename").on_click({
|
||||
.item(PopupMenuItem::new(t(L10nKey::SftpContextRename)).on_click({
|
||||
let app = app.clone();
|
||||
let name = entry.name.clone();
|
||||
move |_, window, cx| {
|
||||
@@ -1250,7 +1291,7 @@ impl Tty7App {
|
||||
let _ = app.update(cx, |this, cx| this.sftp_begin_rename(name, window, cx));
|
||||
}
|
||||
}))
|
||||
.item(PopupMenuItem::new("chmod…").on_click({
|
||||
.item(PopupMenuItem::new(t(L10nKey::SftpContextChmod)).on_click({
|
||||
let app = app.clone();
|
||||
let entry = entry.clone();
|
||||
move |_, window, cx| {
|
||||
@@ -1261,15 +1302,17 @@ impl Tty7App {
|
||||
.separator();
|
||||
|
||||
menu.item(
|
||||
PopupMenuItem::element(move |_window, _cx| div().text_color(danger).child("Delete"))
|
||||
.on_click({
|
||||
let app = app.clone();
|
||||
PopupMenuItem::element(move |_window, _cx| {
|
||||
div().text_color(danger).child(t(L10nKey::Delete))
|
||||
})
|
||||
.on_click({
|
||||
let app = app.clone();
|
||||
let entry = entry.clone();
|
||||
move |_, _window, cx| {
|
||||
let entry = entry.clone();
|
||||
move |_, _window, cx| {
|
||||
let entry = entry.clone();
|
||||
let _ = app.update(cx, |this, cx| this.sftp_delete_entry(entry, cx));
|
||||
}
|
||||
}),
|
||||
let _ = app.update(cx, |this, cx| this.sftp_delete_entry(entry, cx));
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1312,11 +1355,20 @@ impl Tty7App {
|
||||
0.0
|
||||
};
|
||||
let summary = if running > 0 {
|
||||
format!("{running} transferring · {pct:.0}%")
|
||||
t_fmt(
|
||||
L10nKey::SftpTransferSummaryRunning,
|
||||
&[
|
||||
("count", &running.to_string()),
|
||||
("pct", &format!("{pct:.0}")),
|
||||
],
|
||||
)
|
||||
} else if failed > 0 {
|
||||
format!("{failed} failed")
|
||||
t_fmt(
|
||||
L10nKey::SftpTransferSummaryFailed,
|
||||
&[("count", &failed.to_string())],
|
||||
)
|
||||
} else {
|
||||
"Transfers".to_string()
|
||||
t(L10nKey::SftpTransferSummaryIdle).to_string()
|
||||
};
|
||||
let summary_color = if running == 0 && failed > 0 {
|
||||
danger
|
||||
@@ -1363,7 +1415,7 @@ impl Tty7App {
|
||||
.w(px(18.))
|
||||
.h(px(18.))
|
||||
.rounded(px(4.))
|
||||
.tooltip("Dismiss")
|
||||
.tooltip(t(L10nKey::Dismiss))
|
||||
.on_click(cx.listener(|this, _, _w, cx| this.sftp_dismiss_tray(cx))),
|
||||
),
|
||||
);
|
||||
@@ -1383,7 +1435,7 @@ impl Tty7App {
|
||||
.py(px(3.))
|
||||
.text_size(px(11.5))
|
||||
.text_color(muted)
|
||||
.child("No transfers yet."),
|
||||
.child(t(L10nKey::SftpNoTransfers)),
|
||||
)
|
||||
} else {
|
||||
let mut list = v_flex().px(px(CONTENT_INSET)).pb(px(6.)).gap(px(6.));
|
||||
@@ -1430,14 +1482,20 @@ impl Tty7App {
|
||||
0.0
|
||||
};
|
||||
let status = match job.state {
|
||||
SftpJobState::Running => format!(
|
||||
"{} / {} ({pct:.0}%)",
|
||||
human_size(job.bytes_done),
|
||||
human_size(job.bytes_total)
|
||||
SftpJobState::Running => t_fmt(
|
||||
L10nKey::SftpTransferProgress,
|
||||
&[
|
||||
("done", &human_size(job.bytes_done)),
|
||||
("total", &human_size(job.bytes_total)),
|
||||
("pct", &format!("{pct:.0}")),
|
||||
],
|
||||
),
|
||||
SftpJobState::Done => "done".to_string(),
|
||||
SftpJobState::Cancelled => "cancelled".to_string(),
|
||||
SftpJobState::Error => job.error.clone().unwrap_or_else(|| "error".to_string()),
|
||||
SftpJobState::Done => t(L10nKey::SftpTransferDone).to_string(),
|
||||
SftpJobState::Cancelled => t(L10nKey::SftpTransferCancelled).to_string(),
|
||||
SftpJobState::Error => job
|
||||
.error
|
||||
.clone()
|
||||
.unwrap_or_else(|| t(L10nKey::SftpTransferError).to_string()),
|
||||
};
|
||||
let status_color = match job.state {
|
||||
SftpJobState::Error => danger,
|
||||
|
||||
+73
-39
@@ -579,7 +579,7 @@ impl Tty7App {
|
||||
.child(div().flex_1().text_sm().child(text.to_string()))
|
||||
.child(
|
||||
Button::new(("ssh-banner-dismiss", ix))
|
||||
.label("Dismiss")
|
||||
.label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Dismiss))
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click(cx.listener(move |this, _, _w, cx| this.dismiss_ssh_banner(ix, cx))),
|
||||
@@ -590,23 +590,37 @@ impl Tty7App {
|
||||
fn render_ssh_sheet(&self, model: &PromptModel, cx: &mut Context<Self>) -> AnyElement {
|
||||
let danger = cx.theme().danger;
|
||||
let (title, danger_sheet) = match model {
|
||||
PromptModel::Password { user, host, .. } => {
|
||||
(format!("Password for {user}@{host}"), false)
|
||||
}
|
||||
PromptModel::KeyPassphrase { key_path, .. } => {
|
||||
(format!("Passphrase for {key_path}"), false)
|
||||
}
|
||||
PromptModel::Password { user, host, .. } => (
|
||||
crate::ui::i18n::t_fmt(
|
||||
crate::ui::i18n::L10nKey::SshPromptPasswordFor,
|
||||
&[("user", user), ("host", host)],
|
||||
),
|
||||
false,
|
||||
),
|
||||
PromptModel::KeyPassphrase { key_path, .. } => (
|
||||
crate::ui::i18n::t_fmt(
|
||||
crate::ui::i18n::L10nKey::SshPromptPassphraseFor,
|
||||
&[("key_path", key_path)],
|
||||
),
|
||||
false,
|
||||
),
|
||||
PromptModel::KeyboardInteractive { name, .. } => {
|
||||
let label = if name.is_empty() {
|
||||
"Two-factor authentication".to_string()
|
||||
crate::ui::i18n::t(crate::ui::i18n::L10nKey::SshPromptTwoFactor).to_string()
|
||||
} else {
|
||||
name.clone()
|
||||
};
|
||||
(label, false)
|
||||
}
|
||||
PromptModel::HostKeyUnknown { host, .. } => (format!("Unknown host {host}"), false),
|
||||
PromptModel::HostKeyUnknown { host, .. } => (
|
||||
crate::ui::i18n::t_fmt(
|
||||
crate::ui::i18n::L10nKey::SshPromptUnknownHost,
|
||||
&[("host", host)],
|
||||
),
|
||||
false,
|
||||
),
|
||||
PromptModel::HostKeyChanged { .. } => (
|
||||
"Host key CHANGED — possible man-in-the-middle".to_string(),
|
||||
crate::ui::i18n::t(crate::ui::i18n::L10nKey::SshPromptHostKeyChanged).to_string(),
|
||||
true,
|
||||
),
|
||||
};
|
||||
@@ -645,16 +659,16 @@ impl Tty7App {
|
||||
PromptModel::Password { rejected, .. } => {
|
||||
let mut c = card;
|
||||
if *rejected {
|
||||
c = c.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(danger)
|
||||
.child("The stored password was rejected. Enter a new one."),
|
||||
);
|
||||
c = c.child(div().text_xs().text_color(danger).child(crate::ui::i18n::t(
|
||||
crate::ui::i18n::L10nKey::StoredPasswordRejected,
|
||||
)));
|
||||
}
|
||||
c.child(self.render_ssh_input(0))
|
||||
.child(self.render_ssh_remember(cx))
|
||||
.child(self.render_ssh_actions("Connect", cx))
|
||||
.child(self.render_ssh_actions(
|
||||
crate::ui::i18n::t(crate::ui::i18n::L10nKey::SshPromptConnect),
|
||||
cx,
|
||||
))
|
||||
}
|
||||
PromptModel::KeyPassphrase { comment, .. } => {
|
||||
let mut c = card;
|
||||
@@ -668,7 +682,10 @@ impl Tty7App {
|
||||
}
|
||||
c.child(self.render_ssh_input(0))
|
||||
.child(self.render_ssh_remember(cx))
|
||||
.child(self.render_ssh_actions("Unlock", cx))
|
||||
.child(self.render_ssh_actions(
|
||||
crate::ui::i18n::t(crate::ui::i18n::L10nKey::SshPromptUnlock),
|
||||
cx,
|
||||
))
|
||||
}
|
||||
PromptModel::KeyboardInteractive {
|
||||
instructions,
|
||||
@@ -683,7 +700,10 @@ impl Tty7App {
|
||||
c = c.child(div().text_xs().child(row.text.clone()));
|
||||
c = c.child(self.render_ssh_input(i));
|
||||
}
|
||||
c.child(self.render_ssh_actions("Submit", cx))
|
||||
c.child(self.render_ssh_actions(
|
||||
crate::ui::i18n::t(crate::ui::i18n::L10nKey::SshPromptSubmit),
|
||||
cx,
|
||||
))
|
||||
}
|
||||
PromptModel::HostKeyUnknown {
|
||||
algorithm,
|
||||
@@ -703,16 +723,21 @@ impl Tty7App {
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("ssh-hk-trust")
|
||||
.label("Trust")
|
||||
.label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Trust))
|
||||
.small()
|
||||
.primary()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.trust_ssh_host_key(window, cx)
|
||||
})),
|
||||
)
|
||||
.child(Button::new("ssh-hk-abort").label("Abort").small().on_click(
|
||||
cx.listener(|this, _, window, cx| this.cancel_ssh_prompt(window, cx)),
|
||||
)),
|
||||
.child(
|
||||
Button::new("ssh-hk-abort")
|
||||
.label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Abort))
|
||||
.small()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.cancel_ssh_prompt(window, cx)
|
||||
})),
|
||||
),
|
||||
),
|
||||
PromptModel::HostKeyChanged {
|
||||
algorithm,
|
||||
@@ -721,35 +746,39 @@ impl Tty7App {
|
||||
port,
|
||||
host,
|
||||
} => card
|
||||
.child(div().text_xs().text_color(danger).child(
|
||||
"The host key differs from the one previously trusted. This may be an attack.",
|
||||
))
|
||||
.child(div().text_xs().text_color(danger).child(crate::ui::i18n::t(
|
||||
crate::ui::i18n::L10nKey::SshPromptHostKeyChangedBody,
|
||||
)))
|
||||
.child(div().text_xs().child(format!("{host}:{port} {algorithm}")))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.font_family("monospace")
|
||||
.child(format!("new {fingerprint}")),
|
||||
.child(crate::ui::i18n::t_fmt(
|
||||
crate::ui::i18n::L10nKey::SshPromptNewKey,
|
||||
&[("fingerprint", &fingerprint)],
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.font_family("monospace")
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(format!("old {old_fingerprint}")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.child("Type \"yes\" to override and trust the new key, or Esc to abort."),
|
||||
.child(crate::ui::i18n::t_fmt(
|
||||
crate::ui::i18n::L10nKey::SshPromptOldKey,
|
||||
&[("old_fingerprint", &old_fingerprint)],
|
||||
)),
|
||||
)
|
||||
.child(div().text_xs().child(crate::ui::i18n::t(
|
||||
crate::ui::i18n::L10nKey::HostKeyOverrideMessage,
|
||||
)))
|
||||
.child(self.render_ssh_input(0))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("ssh-hkc-abort")
|
||||
.label("Abort")
|
||||
.label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Abort))
|
||||
.small()
|
||||
.primary()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
@@ -758,7 +787,7 @@ impl Tty7App {
|
||||
)
|
||||
.child(
|
||||
Button::new("ssh-hkc-override")
|
||||
.label("Override")
|
||||
.label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Override))
|
||||
.small()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.submit_ssh_prompt(window, cx)
|
||||
@@ -781,7 +810,9 @@ impl Tty7App {
|
||||
h_flex()
|
||||
.child(
|
||||
Checkbox::new("ssh-remember")
|
||||
.label("Remember (keychain)")
|
||||
.label(crate::ui::i18n::t(
|
||||
crate::ui::i18n::L10nKey::RememberKeychain,
|
||||
))
|
||||
.checked(self.ssh_prompt.remember)
|
||||
.on_click(cx.listener(|this, _, _w, cx| this.toggle_ssh_remember(cx))),
|
||||
)
|
||||
@@ -802,9 +833,12 @@ impl Tty7App {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("ssh-cancel").label("Cancel").small().on_click(
|
||||
cx.listener(|this, _, window, cx| this.cancel_ssh_prompt(window, cx)),
|
||||
),
|
||||
Button::new("ssh-cancel")
|
||||
.label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Cancel))
|
||||
.small()
|
||||
.on_click(
|
||||
cx.listener(|this, _, window, cx| this.cancel_ssh_prompt(window, cx)),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
+61
-39
@@ -16,6 +16,7 @@ use crate::core::session::WorkspaceStore;
|
||||
use crate::daemon::install::InstallPhase;
|
||||
use crate::terminal::pane_liveness::Liveness;
|
||||
use crate::ui::app::Tty7App;
|
||||
use crate::ui::i18n::{L10nKey, t, t_fmt};
|
||||
use crate::ui::remote_connect::{self, HostChoice, RemoteWorkspaceRow, human_bytes};
|
||||
use crate::ui::remote_workspace::ConnectFlow;
|
||||
|
||||
@@ -108,8 +109,11 @@ impl Tty7App {
|
||||
pub(crate) fn open_switcher(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
remote_connect::register(cx);
|
||||
remote_connect::sweep_wsl(cx);
|
||||
let query =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("Search workspaces and machines"));
|
||||
let query = cx.new(|cx| {
|
||||
InputState::new(window, cx).placeholder(crate::ui::i18n::t(
|
||||
crate::ui::i18n::L10nKey::SearchWorkspacesAndMachines,
|
||||
))
|
||||
});
|
||||
query.update(cx, |state, cx| state.focus(window, cx));
|
||||
let subs = vec![cx.subscribe_in(
|
||||
&query,
|
||||
@@ -152,7 +156,11 @@ impl Tty7App {
|
||||
let store = WorkspaceStore::all(app);
|
||||
for w in &store.views {
|
||||
let (key, label, target) = match w.host.as_ref() {
|
||||
None => (String::new(), "This Computer".to_string(), None),
|
||||
None => (
|
||||
String::new(),
|
||||
t(L10nKey::SwitcherThisComputer).to_string(),
|
||||
None,
|
||||
),
|
||||
Some(r) => {
|
||||
let key = r.target.to_string();
|
||||
(key.clone(), key, Some(r.target.clone()))
|
||||
@@ -175,7 +183,7 @@ impl Tty7App {
|
||||
groups[slot].rows.push(Row {
|
||||
id: w.id,
|
||||
name: crate::ui::machine_mirror::display_name(app, w)
|
||||
.unwrap_or_else(|| "Untitled".to_string()),
|
||||
.unwrap_or_else(|| t(L10nKey::WindowUntitled).to_string()),
|
||||
path: crate::ui::machine_mirror::subject_path(app, w)
|
||||
.map(|p| crate::ui::home::display_path(std::path::Path::new(&p)))
|
||||
.unwrap_or_default(),
|
||||
@@ -213,7 +221,7 @@ impl Tty7App {
|
||||
0,
|
||||
Group {
|
||||
key: String::new(),
|
||||
label: "This Computer".to_string(),
|
||||
label: t(L10nKey::SwitcherThisComputer).to_string(),
|
||||
endpoint: String::new(),
|
||||
target: None,
|
||||
link: Link::Offline,
|
||||
@@ -436,7 +444,7 @@ impl Tty7App {
|
||||
.py(px(14.))
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("No workspace or machine matches."),
|
||||
.child(t(L10nKey::SwitcherNoMatch)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -537,7 +545,7 @@ impl Tty7App {
|
||||
GUTTER,
|
||||
Icon::new(IconName::Plus).size(px(ICON)).text_color(dim),
|
||||
))
|
||||
.child("Add SSH Host…")
|
||||
.child(t(L10nKey::AddSshHost))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.close_switcher(window, cx);
|
||||
this.open_settings_section(
|
||||
@@ -563,7 +571,7 @@ impl Tty7App {
|
||||
.border_color(border)
|
||||
.child("⌘"),
|
||||
)
|
||||
.child("click for a new window"),
|
||||
.child(t(L10nKey::ClickForNewWindow)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -630,7 +638,7 @@ impl Tty7App {
|
||||
"switcher-retry:{}",
|
||||
group.key
|
||||
)))
|
||||
.label("Try Again")
|
||||
.label(t(L10nKey::TryAgain))
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
@@ -655,7 +663,7 @@ impl Tty7App {
|
||||
"switcher-replace:{}",
|
||||
group.key
|
||||
)))
|
||||
.label("Restart Server")
|
||||
.label(t(L10nKey::RestartServer))
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
@@ -693,19 +701,20 @@ impl Tty7App {
|
||||
let accent = theme.warning;
|
||||
let fraction = phase.fraction().unwrap_or(0.0);
|
||||
let caption = match phase {
|
||||
InstallPhase::Restarting => "Restarting tty7's server\u{2026}".to_string(),
|
||||
InstallPhase::Restarting => t(L10nKey::SwitcherRestartingServer).to_string(),
|
||||
InstallPhase::Downloading { done, total } => match total {
|
||||
Some(total) => format!(
|
||||
"Downloading tty7's server\u{2026} {} / {}",
|
||||
human_bytes(done),
|
||||
human_bytes(total)
|
||||
Some(total) => t_fmt(
|
||||
L10nKey::SwitcherDownloadingServerWithTotal,
|
||||
&[("done", &human_bytes(done)), ("total", &human_bytes(total))],
|
||||
),
|
||||
None => t_fmt(
|
||||
L10nKey::SwitcherDownloadingServerNoTotal,
|
||||
&[("done", &human_bytes(done))],
|
||||
),
|
||||
None => format!("Downloading tty7's server\u{2026} {}", human_bytes(done)),
|
||||
},
|
||||
InstallPhase::Uploading { done, total } => format!(
|
||||
"Copying tty7's server\u{2026} {} / {}",
|
||||
human_bytes(done),
|
||||
human_bytes(total)
|
||||
InstallPhase::Uploading { done, total } => t_fmt(
|
||||
L10nKey::SwitcherCopyingServer,
|
||||
&[("done", &human_bytes(done)), ("total", &human_bytes(total))],
|
||||
),
|
||||
};
|
||||
|
||||
@@ -784,17 +793,25 @@ impl Tty7App {
|
||||
let (dot, word): (Option<gpui::Hsla>, Option<&'static str>) = match group.link {
|
||||
Link::Local => (None, None),
|
||||
Link::Connected => (Some(gpui::rgb(crate::ui::tab_strip::LIVE_DOT).into()), None),
|
||||
Link::Connecting if matches!(group.installing, Some(InstallPhase::Restarting)) => {
|
||||
(Some(theme.warning), Some("restarting…"))
|
||||
}
|
||||
Link::Connecting if group.installing.is_some() => {
|
||||
(Some(theme.warning), Some("installing…"))
|
||||
}
|
||||
Link::Connecting => (Some(theme.warning), Some("connecting…")),
|
||||
Link::Failed => (Some(theme.danger), Some("couldn't connect")),
|
||||
Link::Connecting if matches!(group.installing, Some(InstallPhase::Restarting)) => (
|
||||
Some(theme.warning),
|
||||
Some(t(L10nKey::SwitcherStatusRestarting)),
|
||||
),
|
||||
Link::Connecting if group.installing.is_some() => (
|
||||
Some(theme.warning),
|
||||
Some(t(L10nKey::SwitcherStatusInstalling)),
|
||||
),
|
||||
Link::Connecting => (
|
||||
Some(theme.warning),
|
||||
Some(t(L10nKey::SwitcherStatusConnecting)),
|
||||
),
|
||||
Link::Failed => (
|
||||
Some(theme.danger),
|
||||
Some(t(L10nKey::SwitcherStatusConnectFailed)),
|
||||
),
|
||||
Link::Offline => (
|
||||
Some(gpui::rgb(crate::ui::tab_strip::UNKNOWN_DOT).into()),
|
||||
Some("not connected"),
|
||||
Some(t(L10nKey::SwitcherStatusNotConnected)),
|
||||
),
|
||||
};
|
||||
let word_color = match group.link {
|
||||
@@ -927,9 +944,9 @@ impl Tty7App {
|
||||
let key = row.id.element_key() as usize;
|
||||
|
||||
let badge = if row.current {
|
||||
Some(("this window", true))
|
||||
Some((t(L10nKey::SwitcherThisWindow), true))
|
||||
} else if row.open {
|
||||
Some(("open", false))
|
||||
Some((t(L10nKey::SwitcherOpen), false))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1054,7 +1071,12 @@ impl Tty7App {
|
||||
GUTTER,
|
||||
Icon::new(IconName::Globe).size(px(ICON)).text_color(dim),
|
||||
))
|
||||
.child(div().text_sm().text_color(muted).child("Other Machines"))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(muted)
|
||||
.child(t(L10nKey::OtherMachines)),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
div()
|
||||
@@ -1202,7 +1224,7 @@ fn group_menu(
|
||||
let gref = group.clone();
|
||||
let can_create = group.target.is_none() || group.home.is_some();
|
||||
let menu = menu.item(
|
||||
PopupMenuItem::new("New Workspace")
|
||||
PopupMenuItem::new(t(L10nKey::AppMenuNewWorkspace))
|
||||
.disabled(!can_create)
|
||||
.on_click(move |_, window, cx| {
|
||||
let _ = a1.update(cx, |this, cx| this.switcher_new(&gref, window, cx));
|
||||
@@ -1215,7 +1237,7 @@ fn group_menu(
|
||||
let restartable = target.is_ssh();
|
||||
let (label, for_restart) = (group.label.clone(), target.clone());
|
||||
let menu = menu.separator().item(
|
||||
PopupMenuItem::new("Disconnect")
|
||||
PopupMenuItem::new(t(L10nKey::SwitcherDisconnect))
|
||||
.disabled(!connected)
|
||||
.on_click(move |_, _window, cx| {
|
||||
let _ = a2.update(cx, |this, cx| this.switcher_disconnect(&target, cx));
|
||||
@@ -1225,7 +1247,7 @@ fn group_menu(
|
||||
return menu;
|
||||
}
|
||||
menu.item(
|
||||
PopupMenuItem::new("Restart Server…").on_click(move |_, window, cx| {
|
||||
PopupMenuItem::new(t(L10nKey::AppMenuRestartServer)).on_click(move |_, window, cx| {
|
||||
let _ = a3.update(cx, |this, cx| {
|
||||
this.confirm_restart_remote_server(for_restart.clone(), label.clone(), window, cx);
|
||||
});
|
||||
@@ -1242,14 +1264,14 @@ fn row_menu(
|
||||
let (id, adopt) = (row.id, row.adopt.is_some());
|
||||
let stoppable = row.live;
|
||||
menu.item(
|
||||
PopupMenuItem::new("Rename…")
|
||||
PopupMenuItem::new(t(L10nKey::SwitcherRename))
|
||||
.disabled(adopt)
|
||||
.on_click(move |_, window, cx| {
|
||||
let _ = a1.update(cx, |this, cx| this.switcher_rename(id, window, cx));
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
PopupMenuItem::new("Open in New Window")
|
||||
PopupMenuItem::new(t(L10nKey::SwitcherOpenInNewWindow))
|
||||
.disabled(adopt)
|
||||
.on_click(move |_, window, cx| {
|
||||
let _ = a2.update(cx, |this, cx| {
|
||||
@@ -1260,7 +1282,7 @@ fn row_menu(
|
||||
)
|
||||
.separator()
|
||||
.item(
|
||||
PopupMenuItem::new("Stop Workspace…")
|
||||
PopupMenuItem::new(t(L10nKey::AppMenuStopWorkspace))
|
||||
.disabled(adopt || !stoppable)
|
||||
.on_click(move |_, window, cx| {
|
||||
let _ = a3.update(cx, |this, cx| {
|
||||
@@ -1270,7 +1292,7 @@ fn row_menu(
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
PopupMenuItem::new("Delete Workspace…")
|
||||
PopupMenuItem::new(t(L10nKey::AppMenuDeleteWorkspace))
|
||||
.disabled(adopt)
|
||||
.on_click(move |_, window, cx| {
|
||||
let _ = a4.update(cx, |this, cx| {
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::core::config::{Config, SidebarGrouping};
|
||||
use crate::terminal::git_status::GitStatusCache;
|
||||
use crate::ui::app::{TITLE_BAR_HEIGHT, Tty7App};
|
||||
use crate::ui::hints::tab_badge_label;
|
||||
use crate::ui::i18n::{L10nKey, t};
|
||||
use crate::ui::reorder::{self, Reorder, Surface};
|
||||
use crate::ui::tab_strip::{DragTab, REORDER_SLIDE_MS};
|
||||
|
||||
@@ -554,7 +555,7 @@ impl Tty7App {
|
||||
cx,
|
||||
)
|
||||
.rounded_lg()
|
||||
.tooltip("Hide Sidebar")
|
||||
.tooltip(t(L10nKey::TabTooltipHideSidebar))
|
||||
.on_click(cx.listener(|this, _, _window, cx| this.toggle_left_panel(cx))),
|
||||
),
|
||||
);
|
||||
@@ -784,7 +785,7 @@ fn sidebar_sections(keys: &[Option<PathBuf>]) -> Vec<Section> {
|
||||
if !scratch.is_empty() {
|
||||
sections.push(Section {
|
||||
key: None,
|
||||
name: Some("Scratch".into()),
|
||||
name: Some(t(L10nKey::SidebarScratchGroup).to_string()),
|
||||
tabs: scratch,
|
||||
});
|
||||
}
|
||||
|
||||
+44
-38
@@ -20,6 +20,7 @@ use crate::core::shells::DetectedShell;
|
||||
use crate::daemon::protocol::ShellSpec;
|
||||
use crate::ui::app::{TILE_GLYPH, TILE_GLYPH_LINE, TILE_SIZE, Tab, Tty7App, tile_trailing_inset};
|
||||
use crate::ui::hints::tab_badge_label;
|
||||
use crate::ui::i18n::{L10nKey, t, t_fmt};
|
||||
use crate::ui::reorder::{self, Reorder, Surface};
|
||||
|
||||
pub(crate) const REORDER_SLIDE_MS: u64 = 140;
|
||||
@@ -327,14 +328,14 @@ impl Tty7App {
|
||||
cx,
|
||||
)
|
||||
.rounded_lg()
|
||||
.tooltip("More")
|
||||
.tooltip(t(L10nKey::TabTooltipMore))
|
||||
.dropdown_menu_with_anchor(
|
||||
gpui::Anchor::TopRight,
|
||||
move |menu, _window, _cx| {
|
||||
menu.min_w(px(200.))
|
||||
.action_context(action_ctx.clone())
|
||||
.menu("Command Palette…", Box::new(TogglePalette))
|
||||
.menu("Settings…", Box::new(OpenSettings))
|
||||
.menu(t(L10nKey::AppMenuCommandPalette), Box::new(TogglePalette))
|
||||
.menu(t(L10nKey::AppMenuSettings), Box::new(OpenSettings))
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -362,9 +363,9 @@ impl Tty7App {
|
||||
)
|
||||
.rounded_lg()
|
||||
.tooltip(if panel_open {
|
||||
"Hide Detail Panel"
|
||||
t(L10nKey::TabTooltipHideDetailPanel)
|
||||
} else {
|
||||
"Show Detail Panel"
|
||||
t(L10nKey::TabTooltipShowDetailPanel)
|
||||
})
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.toggle_right_panel(cx);
|
||||
@@ -387,26 +388,26 @@ impl Tty7App {
|
||||
(
|
||||
RightPanelTab::Info,
|
||||
Icon::empty().path("icons/info.svg"),
|
||||
"Info",
|
||||
L10nKey::PanelInfoTitle,
|
||||
),
|
||||
(
|
||||
RightPanelTab::Outline,
|
||||
Icon::empty().path("icons/list.svg"),
|
||||
"Outline",
|
||||
L10nKey::PanelOutlineTitle,
|
||||
),
|
||||
(
|
||||
RightPanelTab::Changes,
|
||||
Icon::empty().path("icons/git-branch.svg"),
|
||||
"Changes",
|
||||
L10nKey::PanelChangesTitle,
|
||||
),
|
||||
(
|
||||
RightPanelTab::Files,
|
||||
Icon::new(IconName::FolderClosed),
|
||||
"Files",
|
||||
L10nKey::PanelFilesTitle,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(tab, icon, label)| {
|
||||
.map(|(tab, icon, label_key)| {
|
||||
div()
|
||||
.occlude()
|
||||
.flex_shrink_0()
|
||||
@@ -419,9 +420,9 @@ impl Tty7App {
|
||||
.rounded_lg()
|
||||
.tooltip(match (tab, changed) {
|
||||
(RightPanelTab::Changes, Some(n)) => {
|
||||
SharedString::from(format!("{label} · {n}"))
|
||||
SharedString::from(format!("{} · {n}", t(label_key)))
|
||||
}
|
||||
_ => SharedString::from(label),
|
||||
_ => SharedString::from(t(label_key)),
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
this.set_right_panel_tab(tab, cx);
|
||||
@@ -534,7 +535,10 @@ impl Tty7App {
|
||||
let raw = tab.leaf_title(window, cx);
|
||||
let label = short_title(&raw);
|
||||
if label.trim().is_empty() {
|
||||
format!("Shell {}", index + 1)
|
||||
t_fmt(
|
||||
L10nKey::TabUnnamedShell,
|
||||
&[("n", &((index + 1).to_string()))],
|
||||
)
|
||||
} else {
|
||||
label
|
||||
}
|
||||
@@ -565,7 +569,7 @@ impl Tty7App {
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("default"),
|
||||
.child(t(L10nKey::ShellDefault)),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
@@ -581,13 +585,13 @@ impl Tty7App {
|
||||
}
|
||||
if shells.is_empty() {
|
||||
let open_default = app.clone();
|
||||
menu = menu.item(
|
||||
PopupMenuItem::new("New Tab").on_click(move |_, window, cx| {
|
||||
menu = menu.item(PopupMenuItem::new(t(L10nKey::AppMenuNewTab)).on_click(
|
||||
move |_, window, cx| {
|
||||
if let Some(app) = open_default.upgrade() {
|
||||
app.update(cx, |this, cx| this.new_tab(window, cx));
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
menu
|
||||
})
|
||||
@@ -610,7 +614,7 @@ impl Tty7App {
|
||||
let has_cwd = cwd.is_some();
|
||||
let mut menu = menu.min_w(px(200.));
|
||||
|
||||
menu = menu.item(PopupMenuItem::new("Rename Tab").on_click({
|
||||
menu = menu.item(PopupMenuItem::new(t(L10nKey::AppMenuRenameTab)).on_click({
|
||||
let app = app.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = app.update(cx, |this, cx| this.start_rename(index, window, cx));
|
||||
@@ -622,7 +626,7 @@ impl Tty7App {
|
||||
let done = tab.and_then(|t| t.agent_status(cx))
|
||||
== Some(crate::core::cli_agent::AgentStatus::Done);
|
||||
menu = menu.item(
|
||||
PopupMenuItem::new("Mark as Unread")
|
||||
PopupMenuItem::new(t(L10nKey::TabContextMarkUnread))
|
||||
.disabled(!done)
|
||||
.on_click({
|
||||
let app = app.clone();
|
||||
@@ -635,14 +639,14 @@ impl Tty7App {
|
||||
|
||||
let in_repo = this.tab_is_in_repo(index, window, cx);
|
||||
if in_repo {
|
||||
menu = menu
|
||||
.separator()
|
||||
.item(PopupMenuItem::new("New Worktree Tab").on_click({
|
||||
menu = menu.separator().item(
|
||||
PopupMenuItem::new(t(L10nKey::AppMenuNewWorktreeTab)).on_click({
|
||||
let app = app.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = app.update(cx, |this, cx| this.new_worktree_tab(index, window, cx));
|
||||
}
|
||||
}));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let agent_session = this.tab_agent_session(index, window, cx);
|
||||
@@ -673,7 +677,7 @@ impl Tty7App {
|
||||
|
||||
menu = menu
|
||||
.separator()
|
||||
.item(PopupMenuItem::new("Split Right").on_click({
|
||||
.item(PopupMenuItem::new(t(L10nKey::AppMenuSplitRight)).on_click({
|
||||
let app = app.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = app.update(cx, |this, cx| {
|
||||
@@ -682,7 +686,7 @@ impl Tty7App {
|
||||
});
|
||||
}
|
||||
}))
|
||||
.item(PopupMenuItem::new("Split Down").on_click({
|
||||
.item(PopupMenuItem::new(t(L10nKey::AppMenuSplitDown)).on_click({
|
||||
let app = app.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = app.update(cx, |this, cx| {
|
||||
@@ -693,7 +697,7 @@ impl Tty7App {
|
||||
}));
|
||||
|
||||
menu = menu.separator().item(
|
||||
PopupMenuItem::new("Copy Working Directory")
|
||||
PopupMenuItem::new(t(L10nKey::AppMenuCopyWorkingDirectory))
|
||||
.disabled(!has_cwd)
|
||||
.on_click(move |_, _window, cx| {
|
||||
if let Some(cwd) = cwd.as_ref() {
|
||||
@@ -706,7 +710,7 @@ impl Tty7App {
|
||||
|
||||
if let Some(session_id) = agent_session.map(|(_, s)| s.session_id) {
|
||||
menu = menu.item(
|
||||
PopupMenuItem::new("Copy Session ID")
|
||||
PopupMenuItem::new(t(L10nKey::AppMenuCopySessionId))
|
||||
.disabled(session_id.is_none())
|
||||
.on_click(move |_, _window, cx| {
|
||||
if let Some(id) = session_id.as_ref() {
|
||||
@@ -717,14 +721,16 @@ impl Tty7App {
|
||||
}
|
||||
|
||||
menu.separator()
|
||||
.item(PopupMenuItem::new("Close Tab").on_click({
|
||||
let app = app.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = app.update(cx, |this, cx| this.close_tab(index, window, cx));
|
||||
}
|
||||
}))
|
||||
.item(
|
||||
PopupMenuItem::new("Close Other Tabs")
|
||||
PopupMenuItem::new(t(L10nKey::TabContextCloseTab)).on_click({
|
||||
let app = app.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = app.update(cx, |this, cx| this.close_tab(index, window, cx));
|
||||
}
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
PopupMenuItem::new(t(L10nKey::AppMenuCloseOtherTabs))
|
||||
.disabled(tab_count <= 1)
|
||||
.on_click({
|
||||
let app = app.clone();
|
||||
@@ -736,9 +742,9 @@ impl Tty7App {
|
||||
)
|
||||
.item(
|
||||
PopupMenuItem::new(if below_wording {
|
||||
"Close Tabs Below"
|
||||
t(L10nKey::TabContextCloseTabsBelow)
|
||||
} else {
|
||||
"Close Tabs to the Right"
|
||||
t(L10nKey::AppMenuCloseTabsRight)
|
||||
})
|
||||
.disabled(index + 1 >= tab_count)
|
||||
.on_click({
|
||||
@@ -1054,7 +1060,7 @@ impl Tty7App {
|
||||
cx,
|
||||
)
|
||||
.rounded_lg()
|
||||
.tooltip("Show Sidebar")
|
||||
.tooltip(t(L10nKey::TabTooltipShowSidebar))
|
||||
.on_click(cx.listener(|this, _, _window, cx| this.toggle_left_panel(cx))),
|
||||
),
|
||||
)
|
||||
|
||||
+67
-60
@@ -11,6 +11,7 @@ use crate::terminal::view::{
|
||||
ClearScrollback, CopyText, CutText, FindInTerminal, FindNext, FindPrevious, PasteText,
|
||||
RedoEdit, SelectAll, UndoEdit,
|
||||
};
|
||||
use crate::ui::i18n::{L10nKey, t};
|
||||
use crate::ui::presets;
|
||||
use crate::ui::presets::Fill;
|
||||
|
||||
@@ -21,84 +22,87 @@ pub(crate) fn traffic_light_position() -> Point<Pixels> {
|
||||
pub(crate) fn set_menus(cx: &mut App) {
|
||||
cx.set_menus([
|
||||
Menu::new("tty7").items([
|
||||
MenuItem::action("About tty7", About),
|
||||
MenuItem::action("Check for Updates…", CheckForUpdates),
|
||||
MenuItem::action(t(L10nKey::AppMenuAbout), About),
|
||||
MenuItem::action(t(L10nKey::AppMenuCheckForUpdates), CheckForUpdates),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Settings…", OpenSettings),
|
||||
MenuItem::action(t(L10nKey::AppMenuSettings), OpenSettings),
|
||||
MenuItem::separator(),
|
||||
MenuItem::os_submenu("Services", SystemMenuType::Services),
|
||||
MenuItem::os_submenu(t(L10nKey::AppMenuServices), SystemMenuType::Services),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Hide tty7", HideApp),
|
||||
MenuItem::action("Hide Others", HideOthers),
|
||||
MenuItem::action("Show All", ShowAll),
|
||||
MenuItem::action(t(L10nKey::AppMenuHideApp), HideApp),
|
||||
MenuItem::action(t(L10nKey::AppMenuHideOthers), HideOthers),
|
||||
MenuItem::action(t(L10nKey::AppMenuShowAll), ShowAll),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Quit tty7", Quit),
|
||||
MenuItem::action(t(L10nKey::AppMenuQuit), Quit),
|
||||
]),
|
||||
Menu::new("File").items([
|
||||
MenuItem::action("New Tab", NewTab),
|
||||
MenuItem::action("New Workspace", NewWorkspace),
|
||||
MenuItem::action("New Worktree Tab", NewWorktreeTab),
|
||||
Menu::new(t(L10nKey::AppMenuFile)).items([
|
||||
MenuItem::action(t(L10nKey::AppMenuNewTab), NewTab),
|
||||
MenuItem::action(t(L10nKey::AppMenuNewWorkspace), NewWorkspace),
|
||||
MenuItem::action(t(L10nKey::AppMenuNewWorktreeTab), NewWorktreeTab),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Split Right", SplitRight),
|
||||
MenuItem::action("Split Down", SplitDown),
|
||||
MenuItem::action(t(L10nKey::AppMenuSplitRight), SplitRight),
|
||||
MenuItem::action(t(L10nKey::AppMenuSplitDown), SplitDown),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Rename Tab…", RenameTab),
|
||||
MenuItem::action("Copy Working Directory", CopyWorkingDirectory),
|
||||
MenuItem::action("Copy Session ID", CopyAgentSessionId),
|
||||
MenuItem::action("Fork Session", ForkAgentSession),
|
||||
MenuItem::action(t(L10nKey::AppMenuRenameTab), RenameTab),
|
||||
MenuItem::action(
|
||||
t(L10nKey::AppMenuCopyWorkingDirectory),
|
||||
CopyWorkingDirectory,
|
||||
),
|
||||
MenuItem::action(t(L10nKey::AppMenuCopySessionId), CopyAgentSessionId),
|
||||
MenuItem::action(t(L10nKey::AppMenuForkSession), ForkAgentSession),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Close Pane / Tab", CloseActiveTab),
|
||||
MenuItem::action("Close Other Tabs", CloseOtherTabs),
|
||||
MenuItem::action("Close Tabs to the Right", CloseTabsToTheRight),
|
||||
MenuItem::action("Reopen Closed Tab", ReopenClosedTab),
|
||||
MenuItem::action(t(L10nKey::AppMenuClosePaneTab), CloseActiveTab),
|
||||
MenuItem::action(t(L10nKey::AppMenuCloseOtherTabs), CloseOtherTabs),
|
||||
MenuItem::action(t(L10nKey::AppMenuCloseTabsRight), CloseTabsToTheRight),
|
||||
MenuItem::action(t(L10nKey::AppMenuReopenClosedTab), ReopenClosedTab),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Rename Workspace…", RenameWorkspace),
|
||||
MenuItem::action("Stop Workspace…", StopWorkspace),
|
||||
MenuItem::action(t(L10nKey::AppMenuRenameWorkspace), RenameWorkspace),
|
||||
MenuItem::action(t(L10nKey::AppMenuStopWorkspace), StopWorkspace),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Delete Workspace…", DeleteWorkspace),
|
||||
MenuItem::action(t(L10nKey::AppMenuDeleteWorkspace), DeleteWorkspace),
|
||||
]),
|
||||
Menu::new("Edit").items([
|
||||
MenuItem::os_action("Undo", UndoEdit, OsAction::Undo),
|
||||
MenuItem::os_action("Redo", RedoEdit, OsAction::Redo),
|
||||
Menu::new(t(L10nKey::AppMenuEdit)).items([
|
||||
MenuItem::os_action(t(L10nKey::AppMenuUndo), UndoEdit, OsAction::Undo),
|
||||
MenuItem::os_action(t(L10nKey::AppMenuRedo), RedoEdit, OsAction::Redo),
|
||||
MenuItem::separator(),
|
||||
MenuItem::os_action("Cut", CutText, OsAction::Cut),
|
||||
MenuItem::os_action("Copy", CopyText, OsAction::Copy),
|
||||
MenuItem::os_action("Paste", PasteText, OsAction::Paste),
|
||||
MenuItem::os_action("Select All", SelectAll, OsAction::SelectAll),
|
||||
MenuItem::os_action(t(L10nKey::AppMenuCut), CutText, OsAction::Cut),
|
||||
MenuItem::os_action(t(L10nKey::AppMenuCopy), CopyText, OsAction::Copy),
|
||||
MenuItem::os_action(t(L10nKey::AppMenuPaste), PasteText, OsAction::Paste),
|
||||
MenuItem::os_action(t(L10nKey::AppMenuSelectAll), SelectAll, OsAction::SelectAll),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Find…", FindInTerminal),
|
||||
MenuItem::action("Find Next", FindNext),
|
||||
MenuItem::action("Find Previous", FindPrevious),
|
||||
MenuItem::action(t(L10nKey::AppMenuFind), FindInTerminal),
|
||||
MenuItem::action(t(L10nKey::AppMenuFindNext), FindNext),
|
||||
MenuItem::action(t(L10nKey::AppMenuFindPrevious), FindPrevious),
|
||||
]),
|
||||
Menu::new("View").items([
|
||||
MenuItem::action("Command Palette…", TogglePalette),
|
||||
Menu::new(t(L10nKey::AppMenuView)).items([
|
||||
MenuItem::action(t(L10nKey::AppMenuCommandPalette), TogglePalette),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Increase Font Size", IncreaseFontSize),
|
||||
MenuItem::action("Decrease Font Size", DecreaseFontSize),
|
||||
MenuItem::action("Reset Font Size", ResetFontSize),
|
||||
MenuItem::action(t(L10nKey::AppMenuIncreaseFontSize), IncreaseFontSize),
|
||||
MenuItem::action(t(L10nKey::AppMenuDecreaseFontSize), DecreaseFontSize),
|
||||
MenuItem::action(t(L10nKey::AppMenuResetFontSize), ResetFontSize),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Left Sidebar", ToggleLeftPanel),
|
||||
MenuItem::action("Right Panel", ToggleRightPanel),
|
||||
MenuItem::action("Code Panel", ToggleCodePanel),
|
||||
MenuItem::action("Tab Bar Position", ToggleTabSidebar),
|
||||
MenuItem::action(t(L10nKey::AppMenuLeftSidebar), ToggleLeftPanel),
|
||||
MenuItem::action(t(L10nKey::AppMenuRightPanel), ToggleRightPanel),
|
||||
MenuItem::action(t(L10nKey::AppMenuCodePanel), ToggleCodePanel),
|
||||
MenuItem::action(t(L10nKey::AppMenuTabBarPosition), ToggleTabSidebar),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Focus Next Pane", FocusNextPane),
|
||||
MenuItem::action("Focus Previous Pane", FocusPrevPane),
|
||||
MenuItem::action("Zoom Pane", ToggleMaximizePane),
|
||||
MenuItem::action(t(L10nKey::AppMenuFocusNextPane), FocusNextPane),
|
||||
MenuItem::action(t(L10nKey::AppMenuFocusPreviousPane), FocusPrevPane),
|
||||
MenuItem::action(t(L10nKey::AppMenuZoomPane), ToggleMaximizePane),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Clear Scrollback", ClearScrollback),
|
||||
MenuItem::action(t(L10nKey::AppMenuClearScrollback), ClearScrollback),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Enter Full Screen", ToggleFullscreen),
|
||||
MenuItem::action(t(L10nKey::AppMenuEnterFullscreen), ToggleFullscreen),
|
||||
]),
|
||||
Menu::new("Window").items(window_menu_items(cx)),
|
||||
Menu::new("Help").items([
|
||||
MenuItem::action("tty7 Documentation", OpenDocumentation),
|
||||
MenuItem::action("Keyboard Shortcuts", ShowKeyboardShortcuts),
|
||||
Menu::new(t(L10nKey::AppMenuWindow)).items(window_menu_items(cx)),
|
||||
Menu::new(t(L10nKey::AppMenuHelp)).items([
|
||||
MenuItem::action(t(L10nKey::AppMenuDocumentation), OpenDocumentation),
|
||||
MenuItem::action(t(L10nKey::AppMenuKeyboardShortcuts), ShowKeyboardShortcuts),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Join the Discord", OpenDiscord),
|
||||
MenuItem::action("Report an Issue…", ReportIssue),
|
||||
MenuItem::action(t(L10nKey::AppMenuJoinDiscord), OpenDiscord),
|
||||
MenuItem::action(t(L10nKey::AppMenuReportIssue), ReportIssue),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Restart Server…", RestartDaemon),
|
||||
MenuItem::action(t(L10nKey::AppMenuRestartServer), RestartDaemon),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -114,8 +118,8 @@ fn window_menu_items(cx: &App) -> Vec<MenuItem> {
|
||||
let slot_action = crate::ui::tab_strip::select_workspace_action;
|
||||
|
||||
let mut items = vec![
|
||||
MenuItem::action("Minimize", MinimizeWindow),
|
||||
MenuItem::action("Zoom", ZoomWindow),
|
||||
MenuItem::action(t(L10nKey::AppMenuMinimize), MinimizeWindow),
|
||||
MenuItem::action(t(L10nKey::AppMenuZoom), ZoomWindow),
|
||||
MenuItem::separator(),
|
||||
];
|
||||
let workspace_start = items.len();
|
||||
@@ -132,7 +136,7 @@ fn window_menu_items(cx: &App) -> Vec<MenuItem> {
|
||||
}
|
||||
}
|
||||
let name = crate::ui::machine_mirror::display_name(cx, workspace)
|
||||
.unwrap_or_else(|| "Untitled".to_string());
|
||||
.unwrap_or_else(|| t(L10nKey::WindowUntitled).to_string());
|
||||
let label = if *open {
|
||||
name
|
||||
} else {
|
||||
@@ -151,7 +155,10 @@ fn window_menu_items(cx: &App) -> Vec<MenuItem> {
|
||||
});
|
||||
}
|
||||
if items.len() == workspace_start {
|
||||
items.push(MenuItem::action("New Workspace", NewWorkspace));
|
||||
items.push(MenuItem::action(
|
||||
t(L10nKey::AppMenuNewWorkspace),
|
||||
NewWorkspace,
|
||||
));
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
+59
-25
@@ -11,6 +11,7 @@ use sni::Backend;
|
||||
|
||||
use crate::core::cli_agent::AgentStatus;
|
||||
use crate::core::config::{Config, NotifyMode};
|
||||
use crate::ui::i18n::{L10nKey, t};
|
||||
use gpui::App;
|
||||
|
||||
const POLL: std::time::Duration = std::time::Duration::from_millis(1000);
|
||||
@@ -59,9 +60,18 @@ impl TraySnapshot {
|
||||
let count = |s: AgentStatus| self.agents.iter().filter(|a| a.status == s).count();
|
||||
let mut parts = Vec::new();
|
||||
for (n, word) in [
|
||||
(count(AgentStatus::Waiting), "waiting"),
|
||||
(count(AgentStatus::Working), "working"),
|
||||
(count(AgentStatus::Done), "done"),
|
||||
(
|
||||
count(AgentStatus::Waiting),
|
||||
t(crate::ui::i18n::L10nKey::PanelAgentWaiting),
|
||||
),
|
||||
(
|
||||
count(AgentStatus::Working),
|
||||
t(crate::ui::i18n::L10nKey::PanelAgentWorking),
|
||||
),
|
||||
(
|
||||
count(AgentStatus::Done),
|
||||
t(crate::ui::i18n::L10nKey::PanelAgentDone),
|
||||
),
|
||||
] {
|
||||
if n > 0 {
|
||||
parts.push(format!("{n} {word}"));
|
||||
@@ -90,19 +100,23 @@ pub(crate) enum SpecItem {
|
||||
}
|
||||
|
||||
pub(crate) fn menu_spec(snap: &TraySnapshot) -> Vec<SpecItem> {
|
||||
let item = |id: &str, label: String| SpecItem::Item {
|
||||
let item = |id: &str, label: &str| SpecItem::Item {
|
||||
id: id.to_string(),
|
||||
label,
|
||||
label: label.to_string(),
|
||||
checked: None,
|
||||
avatar: None,
|
||||
};
|
||||
let mut items = vec![item("show", "Show tty7".into()), SpecItem::Separator];
|
||||
let mut items = vec![item("show", t(L10nKey::TrayShowTty7)), SpecItem::Separator];
|
||||
for a in &snap.agents {
|
||||
let state = match a.status {
|
||||
AgentStatus::Waiting => " — needs input",
|
||||
AgentStatus::Working => " — working",
|
||||
AgentStatus::Done => " — done",
|
||||
AgentStatus::Idle => "",
|
||||
AgentStatus::Waiting => {
|
||||
format!(" — {}", t(crate::ui::i18n::L10nKey::TrayAgentNeedsInput))
|
||||
}
|
||||
AgentStatus::Working => {
|
||||
format!(" — {}", t(crate::ui::i18n::L10nKey::PanelAgentWorking))
|
||||
}
|
||||
AgentStatus::Done => format!(" — {}", t(crate::ui::i18n::L10nKey::PanelAgentDone)),
|
||||
AgentStatus::Idle => String::new(),
|
||||
};
|
||||
items.push(SpecItem::Item {
|
||||
id: format!("agent:{}", a.leaf_id),
|
||||
@@ -121,18 +135,33 @@ pub(crate) fn menu_spec(snap: &TraySnapshot) -> Vec<SpecItem> {
|
||||
avatar: None,
|
||||
};
|
||||
items.push(SpecItem::Submenu {
|
||||
label: "Notifications".into(),
|
||||
label: t(L10nKey::TrayNotifications).to_string(),
|
||||
items: vec![
|
||||
notify("notify:never", "Never", NotifyMode::Never),
|
||||
notify("notify:unfocused", "When Unfocused", NotifyMode::Unfocused),
|
||||
notify("notify:always", "Always", NotifyMode::Always),
|
||||
notify(
|
||||
"notify:never",
|
||||
t(L10nKey::NotifyModeNever),
|
||||
NotifyMode::Never,
|
||||
),
|
||||
notify(
|
||||
"notify:unfocused",
|
||||
t(L10nKey::NotifyModeUnfocused),
|
||||
NotifyMode::Unfocused,
|
||||
),
|
||||
notify(
|
||||
"notify:always",
|
||||
t(L10nKey::NotifyModeAlways),
|
||||
NotifyMode::Always,
|
||||
),
|
||||
],
|
||||
});
|
||||
items.push(item("settings", "Settings…".into()));
|
||||
items.push(item("updates", "Check for Updates…".into()));
|
||||
items.push(item("settings", t(L10nKey::AppMenuSettings)));
|
||||
items.push(item("updates", t(L10nKey::AppMenuCheckForUpdates)));
|
||||
items.push(SpecItem::Separator);
|
||||
items.push(item("quit", "Quit tty7".into()));
|
||||
items.push(item("quit-stop", "Quit and Stop Server…".into()));
|
||||
items.push(item("quit", t(L10nKey::AppMenuQuit)));
|
||||
items.push(item(
|
||||
"quit-stop",
|
||||
crate::ui::i18n::t(crate::ui::i18n::L10nKey::TrayQuitStopServer),
|
||||
));
|
||||
items
|
||||
}
|
||||
|
||||
@@ -199,18 +228,23 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn attention_follows_waiting_and_tooltip_counts() {
|
||||
crate::ui::i18n::set_locale("en");
|
||||
assert!(snapshot_with_agent(AgentStatus::Waiting).attention());
|
||||
assert!(!snapshot_with_agent(AgentStatus::Working).attention());
|
||||
assert!(!snapshot_with_agent(AgentStatus::Done).attention());
|
||||
assert_eq!(
|
||||
snapshot_with_agent(AgentStatus::Waiting).tooltip(),
|
||||
"tty7 — 1 waiting"
|
||||
format!(
|
||||
"tty7 — 1 {}",
|
||||
t(crate::ui::i18n::L10nKey::PanelAgentWaiting)
|
||||
)
|
||||
);
|
||||
assert_eq!(TraySnapshot::default().tooltip(), "tty7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn menu_spec_shape() {
|
||||
crate::ui::i18n::set_locale("en");
|
||||
let empty = menu_spec(&TraySnapshot::default());
|
||||
let labels: Vec<_> = empty
|
||||
.iter()
|
||||
@@ -223,12 +257,12 @@ mod tests {
|
||||
assert_eq!(
|
||||
labels,
|
||||
[
|
||||
"Show tty7",
|
||||
"Notifications",
|
||||
"Settings…",
|
||||
"Check for Updates…",
|
||||
"Quit tty7",
|
||||
"Quit and Stop Server…"
|
||||
t(L10nKey::TrayShowTty7),
|
||||
t(L10nKey::TrayNotifications),
|
||||
t(L10nKey::AppMenuSettings),
|
||||
t(L10nKey::AppMenuCheckForUpdates),
|
||||
t(L10nKey::AppMenuQuit),
|
||||
t(L10nKey::TrayQuitStopServer),
|
||||
]
|
||||
);
|
||||
assert!(
|
||||
|
||||
+43
-24
@@ -8,6 +8,7 @@ use crate::core::config::{Config, StartupMode};
|
||||
use crate::core::session::{WorkspaceId, WorkspaceStore};
|
||||
use crate::core::window_state::{WindowGeometry as _, WindowState};
|
||||
use crate::ui::app::Tty7App;
|
||||
use crate::ui::i18n::{L10nKey, t, t_fmt, t_plural};
|
||||
|
||||
const CASCADE_STEP: f32 = 28.0;
|
||||
|
||||
@@ -105,6 +106,23 @@ impl WindowRegistry {
|
||||
.map(|w| w.app.clone())
|
||||
}
|
||||
|
||||
pub fn refresh_locale(cx: &mut App, except: Option<WorkspaceId>) {
|
||||
Self::sweep(cx);
|
||||
let windows: Vec<_> = cx
|
||||
.global::<Self>()
|
||||
.windows
|
||||
.iter()
|
||||
.filter(|entry| Some(entry.workspace) != except)
|
||||
.map(|entry| (entry.handle, entry.app.clone()))
|
||||
.collect();
|
||||
for (handle, app) in windows {
|
||||
let _ = handle.update(cx, |_, window, cx| {
|
||||
let _ = app.update(cx, |app, cx| app.refresh_locale_state(window, cx));
|
||||
window.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn register(
|
||||
cx: &mut App,
|
||||
workspace: WorkspaceId,
|
||||
@@ -303,22 +321,11 @@ pub fn confirm_and_delete(cx: &mut App, window: &mut Window, workspace: Workspac
|
||||
|
||||
fn destructive_detail(live: Option<usize>, verb: &str) -> String {
|
||||
match (live, verb) {
|
||||
(None, "Delete") => "Its machine could not be reached. Any shells still running there \
|
||||
will be ended, and the layout forgotten."
|
||||
.to_string(),
|
||||
(None, _) => {
|
||||
"Its machine could not be reached. Any shells still running there will be ended."
|
||||
.to_string()
|
||||
}
|
||||
(Some(0), _) => "Its layout and working directories will be forgotten.".to_string(),
|
||||
(Some(1), "Delete") => {
|
||||
"1 running shell will be ended and its layout forgotten.".to_string()
|
||||
}
|
||||
(Some(n), "Delete") => {
|
||||
format!("{n} running shells will be ended and the layout forgotten.")
|
||||
}
|
||||
(Some(1), _) => "1 running shell will be ended.".to_string(),
|
||||
(Some(n), _) => format!("{n} running shells will be ended."),
|
||||
(None, "Delete") => t(L10nKey::WindowDeleteUnreachable).to_string(),
|
||||
(None, _) => t(L10nKey::WindowStopUnreachable).to_string(),
|
||||
(Some(0), _) => t_plural(L10nKey::WindowStopShells, 0, &[]),
|
||||
(Some(n), "Delete") => t_plural(L10nKey::WindowDeleteShells, n, &[]),
|
||||
(Some(n), _) => t_plural(L10nKey::WindowStopShells, n, &[]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,7 +337,7 @@ fn confirm_destructive(
|
||||
act: fn(&mut App, WorkspaceId),
|
||||
) {
|
||||
let name = crate::ui::machine_mirror::display_name_for(cx, workspace)
|
||||
.unwrap_or_else(|| "this workspace".to_string());
|
||||
.unwrap_or_else(|| t(L10nKey::WindowThisWorkspace).to_string());
|
||||
let query = pane_count_query(cx, workspace);
|
||||
let handle = window.window_handle();
|
||||
|
||||
@@ -349,12 +356,22 @@ fn confirm_destructive(
|
||||
}
|
||||
|
||||
let detail = destructive_detail(live, verb);
|
||||
let verb_key = if verb == "Delete" {
|
||||
L10nKey::WindowDelete
|
||||
} else {
|
||||
L10nKey::WindowStop
|
||||
};
|
||||
let verb_label = t(verb_key);
|
||||
let title = t_fmt(
|
||||
L10nKey::WindowConfirmTitle,
|
||||
&[("verb", verb_label), ("name", &name)],
|
||||
);
|
||||
let Ok(answer) = handle.update(cx, |_, window, cx| {
|
||||
window.prompt(
|
||||
gpui::PromptLevel::Warning,
|
||||
&format!("{verb} Workspace \u{201c}{name}\u{201d}?"),
|
||||
&title,
|
||||
Some(&detail),
|
||||
&["Cancel", verb],
|
||||
&[t(L10nKey::Cancel), verb_label],
|
||||
cx,
|
||||
)
|
||||
}) else {
|
||||
@@ -537,6 +554,7 @@ fn cascade(bounds: Bounds<gpui::Pixels>, existing: usize) -> Bounds<gpui::Pixels
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::core::session::{WindowView, WindowViews};
|
||||
use crate::ui::i18n::{L10nKey, set_locale, t_plural};
|
||||
|
||||
fn bounds_at(x: f32, y: f32) -> Bounds<gpui::Pixels> {
|
||||
Bounds {
|
||||
@@ -614,25 +632,26 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn the_confirmation_says_which_of_the_three_answers_it_got() {
|
||||
set_locale("en");
|
||||
assert_eq!(
|
||||
destructive_detail(Some(1), "Stop"),
|
||||
"1 running shell will be ended."
|
||||
t_plural(L10nKey::WindowStopShells, 1, &[])
|
||||
);
|
||||
assert_eq!(
|
||||
destructive_detail(Some(3), "Stop"),
|
||||
"3 running shells will be ended."
|
||||
t_plural(L10nKey::WindowStopShells, 3, &[])
|
||||
);
|
||||
assert_eq!(
|
||||
destructive_detail(Some(1), "Delete"),
|
||||
"1 running shell will be ended and its layout forgotten."
|
||||
t_plural(L10nKey::WindowDeleteShells, 1, &[])
|
||||
);
|
||||
assert_eq!(
|
||||
destructive_detail(Some(3), "Delete"),
|
||||
"3 running shells will be ended and the layout forgotten."
|
||||
t_plural(L10nKey::WindowDeleteShells, 3, &[])
|
||||
);
|
||||
assert_eq!(
|
||||
destructive_detail(Some(0), "Delete"),
|
||||
"Its layout and working directories will be forgotten."
|
||||
t_plural(L10nKey::WindowStopShells, 0, &[])
|
||||
);
|
||||
|
||||
for verb in ["Stop", "Delete"] {
|
||||
|
||||
@@ -7,6 +7,7 @@ use gpui_component::{
|
||||
|
||||
use crate::core::worktree::{WorktreeDefaults, WorktreeRequest};
|
||||
use crate::ui::app::Tty7App;
|
||||
use crate::ui::i18n::{L10nKey, t, t_fmt};
|
||||
|
||||
pub(crate) struct WorktreePrompt {
|
||||
host: crate::ui::host_ops::SharedHost,
|
||||
@@ -78,7 +79,7 @@ impl Tty7App {
|
||||
let base = p.base.read(cx).value().trim().to_string();
|
||||
let (name, branch) = match (name.is_empty(), branch.is_empty()) {
|
||||
(true, true) => {
|
||||
window.push_notification("The worktree needs a name", cx);
|
||||
window.push_notification(t(L10nKey::WorktreePromptNeedsName), cx);
|
||||
return;
|
||||
}
|
||||
(true, false) => (branch.clone(), branch),
|
||||
@@ -113,7 +114,10 @@ impl Tty7App {
|
||||
if let Some(p) = this.worktree_prompt.as_mut() {
|
||||
p.busy = false;
|
||||
}
|
||||
window.push_notification(format!("New worktree failed: {e}"), cx);
|
||||
window.push_notification(
|
||||
t_fmt(L10nKey::AppNewWorktreeFailed, &[("error", &e.to_string())]),
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
},
|
||||
@@ -162,9 +166,9 @@ impl Tty7App {
|
||||
div()
|
||||
.text_sm()
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.child("New Worktree Tab"),
|
||||
.child(t(L10nKey::WorktreePromptTitle)),
|
||||
)
|
||||
.child(field("Worktree Name", &p.name))
|
||||
.child(field(t(L10nKey::WorktreePromptName), &p.name))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
@@ -172,14 +176,18 @@ impl Tty7App {
|
||||
.text_color(muted)
|
||||
.child(preview),
|
||||
)
|
||||
.child(field("New Branch", &p.branch))
|
||||
.child(field("Start From", &p.base))
|
||||
.child(field(t(L10nKey::WorktreePromptBranch), &p.branch))
|
||||
.child(field(t(L10nKey::WorktreePromptBase), &p.base))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("worktree-create")
|
||||
.label(if p.busy { "Creating…" } else { "Create" })
|
||||
.label(if p.busy {
|
||||
t(L10nKey::WorktreePromptCreating)
|
||||
} else {
|
||||
t(L10nKey::WorktreePromptCreate)
|
||||
})
|
||||
.small()
|
||||
.primary()
|
||||
.disabled(p.busy)
|
||||
@@ -189,7 +197,7 @@ impl Tty7App {
|
||||
)
|
||||
.child(
|
||||
Button::new("worktree-cancel")
|
||||
.label("Cancel")
|
||||
.label(t(L10nKey::Cancel))
|
||||
.small()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.cancel_worktree_prompt(window, cx)
|
||||
|
||||
Reference in New Issue
Block a user