Skip to content

Commit a689e71

Browse files
mizu-junclaudehappy-otter
committed
feat(client): add "Open config.toml" link to the settings footer
Windows Terminal's "Open JSON file" equivalent: a right-aligned accent link in the settings-panel footer bar opens config.toml with the OS default editor. - `platform::open_config_file` follows the `open_releases_url` pattern (open / xdg-open / `cmd /c start "" <path>` with CREATE_NO_WINDOW; the empty title argument keeps paths with spaces intact) and creates the file first when it does not exist yet. - Hit-test mirrors the footer geometry (`bottom_y` formula) and only the label region is clickable; the link colour goes through `ensure_readable` for the 4.5:1 contrast rule. - i18n: new `settings-open-config-file` key in all 8 locales. Note: the per-field "restore default" item originally planned for this phase moves into the P2 row-refactor, which introduces the per-row metadata it needs. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
1 parent 9444040 commit a689e71

13 files changed

Lines changed: 96 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- Settings panel: an "Open config.toml" link in the footer bar opens the
12+
configuration file with the OS default editor (Windows Terminal's
13+
"Open JSON file" equivalent). Creates the file first when it does not
14+
exist yet.
15+
1016
## [1.14.0] - 2026-07-12
1117

1218
Windows console-flash fix and a Windows-Terminal-inspired settings-panel

nexterm-client-gpu/src/platform.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,41 @@ pub(crate) fn cursor_screen_pos() -> Option<(i32, i32)> {
7070
}
7171
}
7272

73+
/// Open `config.toml` with the OS default handler (P4, WT's "Open JSON
74+
/// file" equivalent). Creates an empty file first when it does not exist
75+
/// yet, so the editor does not error out on a missing path.
76+
pub(crate) fn open_config_file() {
77+
let path = nexterm_config::toml_path();
78+
if !path.exists() {
79+
if let Some(parent) = path.parent() {
80+
let _ = std::fs::create_dir_all(parent);
81+
}
82+
if let Err(e) = std::fs::write(&path, "") {
83+
tracing::warn!("failed to create {}: {}", path.display(), e);
84+
return;
85+
}
86+
}
87+
let path_str = path.to_string_lossy().to_string();
88+
#[cfg(target_os = "macos")]
89+
let _ = std::process::Command::new("open").arg(&path_str).spawn();
90+
#[cfg(target_os = "linux")]
91+
let _ = std::process::Command::new("xdg-open")
92+
.arg(&path_str)
93+
.spawn();
94+
#[cfg(windows)]
95+
{
96+
use std::os::windows::process::CommandExt;
97+
use windows_sys::Win32::System::Threading::CREATE_NO_WINDOW;
98+
// Without CREATE_NO_WINDOW a GUI-subsystem process spawning a console
99+
// app briefly flashes a black console window. `start ""` keeps paths
100+
// with spaces from being consumed as the window title.
101+
let _ = std::process::Command::new("cmd")
102+
.args(["/c", "start", "", &path_str])
103+
.creation_flags(CREATE_NO_WINDOW)
104+
.spawn();
105+
}
106+
}
107+
73108
/// Open the GitHub releases page in the default browser.
74109
pub(crate) fn open_releases_url() {
75110
let url = "https://github.com/mizu-jun/nexterm/releases/latest";

nexterm-client-gpu/src/renderer/event_handler/mouse.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,11 @@ impl EventHandler {
634634
// Click outside the panel → close the panel.
635635
self.app.state.settings_panel.close();
636636
}
637+
SettingsPanelHit::OpenConfigFile => {
638+
// P4 (WT-like UX): open config.toml with the OS
639+
// default editor (WT's "Open JSON file" equivalent).
640+
crate::platform::open_config_file();
641+
}
637642
SettingsPanelHit::Category(idx) => {
638643
// Click on a sidebar category → switch category. With
639644
// a non-empty Phase 4 search, the rendered list is the

nexterm-client-gpu/src/renderer/event_handler/settings_panel_hit.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ pub(super) enum SettingsPanelHit {
4343
/// cyclers (click cycles forward), 4..=6 are byte-cap fields (click focuses
4444
/// and starts editing). The footer note row has no hit zone.
4545
SecurityRow(u8),
46+
/// P4 (WT-like UX): the "Open config.toml" link in the footer bar.
47+
OpenConfigFile,
4648
/// Empty area inside the panel (no-op).
4749
PanelBackground,
4850
/// Phase 4 (UI/UX v2): click landed on the sidebar search input.
@@ -106,6 +108,21 @@ impl EventHandler {
106108
return SettingsPanelHit::TitleBar;
107109
}
108110

111+
// Footer bar — mirrors the `bottom_y` formula in
112+
// `overlay/settings/mod.rs`. The right-aligned "Open config.toml"
113+
// link is the only clickable region; the rest is a no-op. Checked
114+
// before the sidebar branch because the footer spans the full panel
115+
// width below the sidebar.
116+
let footer_y = py + panel_h - cell_h * 1.5;
117+
if cy >= footer_y {
118+
let label = format!("↗ {}", nexterm_i18n::fl!("settings-open-config-file"));
119+
let label_w = crate::vertex_util::visual_width(&label) as f32 * cell_w;
120+
if cx >= px + panel_w - label_w - cell_w {
121+
return SettingsPanelHit::OpenConfigFile;
122+
}
123+
return SettingsPanelHit::PanelBackground;
124+
}
125+
109126
// Sidebar — first the Phase 4 search input (reserved strip at the
110127
// top), then the category list below it.
111128
let sidebar_top = py + title_h;

nexterm-client-gpu/src/renderer/overlay/settings/mod.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,31 @@ impl WgpuState {
619619
text_idx,
620620
);
621621

622+
// P4 (WT-like UX): right-aligned "Open config.toml" link (WT's
623+
// "Open JSON file" equivalent). The hit-test in
624+
// `settings_panel_hit.rs` mirrors this exact geometry.
625+
let open_label = format!("↗ {}", nexterm_i18n::fl!("settings-open-config-file"));
626+
let open_label_w = crate::vertex_util::visual_width(&open_label) as f32 * cell_w;
627+
add_string_verts(
628+
&open_label,
629+
px + panel_w - open_label_w - cell_w,
630+
bottom_y + cell_h * 0.3,
631+
row::ensure_readable(
632+
tokens.accent_primary,
633+
tokens.surface_0,
634+
row::MIN_TEXT_CONTRAST,
635+
),
636+
false,
637+
sw,
638+
sh,
639+
cell_w,
640+
font,
641+
atlas,
642+
&self.queue,
643+
text_verts,
644+
text_idx,
645+
);
646+
622647
// Phase B3: the open/close animation is expressed purely through
623648
// `slide_offset` (position) above — there used to also be a
624649
// translucent "fade-in" wash drawn over the entire panel here,

nexterm-i18n/locales/de.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"settings-category-security": "Sicherheit",
9898
"settings-panel-title": " * Nexterm-Einstellungen",
9999
"settings-panel-footer-hint": " Enter=Speichern Esc=Abbrechen Tab=Nächste Kategorie",
100+
"settings-open-config-file": "config.toml öffnen",
100101
"settings-search-placeholder-typing": "/ Zum Filtern tippen…",
101102
"settings-search-placeholder-idle": "/ Kategorien durchsuchen",
102103
"settings-startup-language": "Sprache",

nexterm-i18n/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"settings-category-security": "Security",
9898
"settings-panel-title": " * Nexterm Settings",
9999
"settings-panel-footer-hint": " Enter=Save Esc=Cancel Tab=Next category",
100+
"settings-open-config-file": "Open config.toml",
100101
"settings-search-placeholder-typing": "/ Type to filter…",
101102
"settings-search-placeholder-idle": "/ Search categories",
102103
"settings-startup-language": "Language",

nexterm-i18n/locales/es.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"settings-category-security": "Seguridad",
9898
"settings-panel-title": " * Ajustes de Nexterm",
9999
"settings-panel-footer-hint": " Intro=Guardar Esc=Cancelar Tab=Siguiente categoría",
100+
"settings-open-config-file": "Abrir config.toml",
100101
"settings-search-placeholder-typing": "/ Escriba para filtrar…",
101102
"settings-search-placeholder-idle": "/ Buscar categorías",
102103
"settings-startup-language": "Idioma",

nexterm-i18n/locales/fr.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"settings-category-security": "Sécurité",
9898
"settings-panel-title": " * Paramètres Nexterm",
9999
"settings-panel-footer-hint": " Entrée=Enregistrer Échap=Annuler Tab=Catégorie suivante",
100+
"settings-open-config-file": "Ouvrir config.toml",
100101
"settings-search-placeholder-typing": "/ Tapez pour filtrer…",
101102
"settings-search-placeholder-idle": "/ Rechercher des catégories",
102103
"settings-startup-language": "Langue",

nexterm-i18n/locales/it.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"settings-category-security": "Sicurezza",
9898
"settings-panel-title": " * Impostazioni di Nexterm",
9999
"settings-panel-footer-hint": " Invio=Salva Esc=Annulla Tab=Categoria successiva",
100+
"settings-open-config-file": "Apri config.toml",
100101
"settings-search-placeholder-typing": "/ Digita per filtrare…",
101102
"settings-search-placeholder-idle": "/ Cerca categorie",
102103
"settings-startup-language": "Lingua",

0 commit comments

Comments
 (0)