From 53fb14696691f57c924e47ed8be9ef4d5d80717b Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Mon, 20 Jul 2026 05:30:48 +0000 Subject: [PATCH 59/71] feat(macos): persistent settings via NSUserDefaults (font/size/theme/renderer/shell/ssh) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read defaults from NSUserDefaults (org.contourterminal.Contour) so settings persist across launches and are editable now with the `defaults` CLI, e.g. defaults write org.contourterminal.Contour fontFamily Monaco defaults write org.contourterminal.Contour fontSize -float 13 defaults write org.contourterminal.Contour theme light defaults write org.contourterminal.Contour renderer gl defaults write org.contourterminal.Contour shell /bin/zsh Resolution order per setting: environment variable (CONTOUR_RENDER/THEME/SSH, kept for quick testing) > NSUserDefaults > built-in default. This is the persistence layer a future preferences UI (#9) and config file (#24) build on — same keys, no parser needed. font family/size were previously hardcoded (Menlo/14) — now read from defaults. Shell was always the account login shell (pw_shell; the engine's loginShell() does not consult $SHELL) — now overridable: threaded a shellOverride through bridge_create -> TerminalSession -> makeLocalPty, used when non-empty. Explicit ternaries, not the `?:` GNU shorthand (not standard C++, and the .mm is built at gnu++98 by gcc-4.2). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AeG2jwYMX5gdvvtUnx3PJa --- src/contour_macos/SessionBridge.cpp | 8 +++- src/contour_macos/SessionBridge.h | 3 +- src/contour_macos/TerminalSession.cpp | 17 +++++--- src/contour_macos/TerminalSession.h | 3 +- src/contour_macos/TerminalView.h | 3 +- src/contour_macos/TerminalView.mm | 4 +- src/contour_macos/main.mm | 58 +++++++++++++++++++-------- 7 files changed, 68 insertions(+), 28 deletions(-) diff --git a/src/contour_macos/SessionBridge.cpp b/src/contour_macos/SessionBridge.cpp index 1b4e23c5..bbbfc577 100644 --- a/src/contour_macos/SessionBridge.cpp +++ b/src/contour_macos/SessionBridge.cpp @@ -73,7 +73,8 @@ TerminalSession* bridge_create(int widthPx, BridgeCallbacks const& cb, int renderMode, char const* themeName, - BridgeSshConfig const& ssh) + BridgeSshConfig const& ssh, + char const* shellOverride) { auto fonts = vtrasterizer::FontDescriptions {}; fonts.dpi = text::DPI { font.dpiX, font.dpiY }; @@ -144,6 +145,8 @@ TerminalSession* bridge_create(int widthPx, sshOptions.knownHostsFile = ssh.knownHostsFile ? ssh.knownHostsFile : ""; } + std::string const shell = shellOverride ? shellOverride : ""; + auto const pageSize = vtbackend::PageSize { vtbackend::LineCount(24), vtbackend::ColumnCount(80) }; return new TerminalSession(pageSize, std::move(fonts), @@ -151,7 +154,8 @@ TerminalSession* bridge_create(int widthPx, std::move(callbacks), backend, palette, - std::move(sshOptions)); + std::move(sshOptions), + shell); } void bridge_destroy(TerminalSession* session) diff --git a/src/contour_macos/SessionBridge.h b/src/contour_macos/SessionBridge.h index e3c34fde..f5a85562 100644 --- a/src/contour_macos/SessionBridge.h +++ b/src/contour_macos/SessionBridge.h @@ -86,7 +86,8 @@ TerminalSession* bridge_create(int widthPx, BridgeCallbacks const& callbacks, int renderMode, char const* themeName, - BridgeSshConfig const& ssh); + BridgeSshConfig const& ssh, + char const* shellOverride); /// Returns the render backend the session was created with (a BridgeRenderMode value). The view /// uses this to choose its presentation path (CGImage blit vs GL context present). diff --git a/src/contour_macos/TerminalSession.cpp b/src/contour_macos/TerminalSession.cpp index 238e9197..7533c393 100644 --- a/src/contour_macos/TerminalSession.cpp +++ b/src/contour_macos/TerminalSession.cpp @@ -47,9 +47,12 @@ namespace return settings; } - [[nodiscard]] std::unique_ptr makeLocalPty(vtbackend::PageSize pageSize) + [[nodiscard]] std::unique_ptr makeLocalPty(vtbackend::PageSize pageSize, + std::string const& shellOverride) { - auto const shell = vtpty::Process::loginShell(false).front(); + // Use the configured shell if one was set, else the account's login shell (pw_shell — the + // engine's loginShell() does not consult $SHELL). + auto const shell = !shellOverride.empty() ? shellOverride : vtpty::Process::loginShell(false).front(); auto exe = vtpty::Process::ExecInfo {}; exe.program = shell; exe.workingDirectory = vtpty::Process::homeDirectory(); @@ -60,10 +63,11 @@ namespace [[nodiscard]] std::unique_ptr makePty( vtbackend::PageSize pageSize, TerminalSession::SshOptions const& ssh, + std::string const& shellOverride, std::function const& verifyHostkey) { if (ssh.host.empty()) - return makeLocalPty(pageSize); + return makeLocalPty(pageSize, shellOverride); #if defined(VTPTY_LIBSSH2) auto const home = vtpty::Process::homeDirectory(); @@ -89,7 +93,7 @@ namespace return std::make_unique(config, onHostkey); #else (void) verifyHostkey; - return makeLocalPty(pageSize); + return makeLocalPty(pageSize, shellOverride); #endif } @@ -161,7 +165,8 @@ TerminalSession::TerminalSession(vtbackend::PageSize pageSize, Callbacks callbacks, RenderBackend backend, vtbackend::ColorPalette colorPalette, - SshOptions ssh): + SshOptions ssh, + std::string shellOverride): _callbacks { std::move(callbacks) }, _pageSize { pageSize }, _surfaceSize { surfaceSize }, @@ -205,7 +210,7 @@ TerminalSession::TerminalSession(vtbackend::PageSize pageSize, _terminal = std::make_unique( *this, - makePty(_pageSize, ssh, _callbacks.verifySshHostkey), + makePty(_pageSize, ssh, shellOverride, _callbacks.verifySshHostkey), makeSettings(_pageSize), steady_clock::now()); diff --git a/src/contour_macos/TerminalSession.h b/src/contour_macos/TerminalSession.h index 5a0339db..415d74e2 100644 --- a/src/contour_macos/TerminalSession.h +++ b/src/contour_macos/TerminalSession.h @@ -77,7 +77,8 @@ class TerminalSession: public vtbackend::Terminal::Events Callbacks callbacks, RenderBackend backend, vtbackend::ColorPalette colorPalette, - SshOptions ssh); + SshOptions ssh, + std::string shellOverride); ~TerminalSession() override; /// Spawns the shell and starts the read/parse thread. diff --git a/src/contour_macos/TerminalView.h b/src/contour_macos/TerminalView.h index 3f9c9c29..b99d65b9 100644 --- a/src/contour_macos/TerminalView.h +++ b/src/contour_macos/TerminalView.h @@ -29,7 +29,8 @@ fontSize:(double)fontSize renderMode:(int)renderMode theme:(NSString*)theme - sshDestination:(NSString*)sshDestination; + sshDestination:(NSString*)sshDestination + shell:(NSString*)shell; // Requests a repaint on the main thread (safe to call from any thread). - (void)requestRedraw; diff --git a/src/contour_macos/TerminalView.mm b/src/contour_macos/TerminalView.mm index b291cfc6..49f83b1b 100644 --- a/src/contour_macos/TerminalView.mm +++ b/src/contour_macos/TerminalView.mm @@ -186,6 +186,7 @@ int cbVerifySshHostkey(void* userData, char const* host, int port, char const* f renderMode:(int)renderMode theme:(NSString*)theme sshDestination:(NSString*)sshDestination + shell:(NSString*)shell { self = [super initWithFrame:frame]; if (!self) @@ -270,7 +271,8 @@ int cbVerifySshHostkey(void* userData, char const* host, int port, char const* f callbacks, renderMode, [theme UTF8String], - ssh); + ssh, + [shell length] ? [shell UTF8String] : NULL); if (!_session) { [self release]; diff --git a/src/contour_macos/main.mm b/src/contour_macos/main.mm index d08b0259..fc74a3b1 100644 --- a/src/contour_macos/main.mm +++ b/src/contour_macos/main.mm @@ -32,29 +32,55 @@ [window setTitle:@"contour"]; [window setReleasedWhenClosed:YES]; - // Render backend selector: CONTOUR_RENDER=gl selects the OpenGL backend, anything else (or - // unset) the CPU backend. A single env var is enough until the preferences UI exists; it lets - // the GL path be exercised without a rebuild. BridgeRender_CPU/_OpenGL are 0/1. - int renderMode = 0; // BridgeRender_CPU - char const* renderEnv = getenv("CONTOUR_RENDER"); - if (renderEnv && (strcmp(renderEnv, "gl") == 0 || strcmp(renderEnv, "opengl") == 0)) - renderMode = 1; // BridgeRender_OpenGL - - // Color theme selector: CONTOUR_THEME=light selects the light theme, anything else (or unset) - // the dark theme. Like CONTOUR_RENDER, a single env var suffices until the preferences UI exists. + // Settings resolution order (highest priority first): environment variable, then the persisted + // NSUserDefaults value, then the built-in default. NSUserDefaults lives in + // ~/Library/Preferences/org.contourterminal.Contour.plist and is editable with the `defaults` + // CLI (e.g. `defaults write org.contourterminal.Contour fontFamily Monaco`) — the persistence + // backing a future preferences UI, with the env vars kept as an override for quick testing. + NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; + + // Theme: env CONTOUR_THEME, else defaults "theme", else "dark". + // (Explicit ternaries rather than the `?:` GNU shorthand, which is not standard C++.) char const* themeEnv = getenv("CONTOUR_THEME"); - NSString* theme = (themeEnv && strcmp(themeEnv, "light") == 0) ? @"light" : @"dark"; + NSString* themeDefault = [defaults stringForKey:@"theme"]; + NSString* theme = themeEnv ? [NSString stringWithUTF8String:themeEnv] + : (themeDefault ? themeDefault : @"dark"); + if (![theme isEqualToString:@"light"]) + theme = @"dark"; - // SSH destination: CONTOUR_SSH=user@host[:port] opens an SSH session instead of a local shell. + // Render backend: env CONTOUR_RENDER, else defaults "renderer", else "cpu". + char const* renderEnv = getenv("CONTOUR_RENDER"); + NSString* rendererDefault = [defaults stringForKey:@"renderer"]; + NSString* renderStr = renderEnv ? [NSString stringWithUTF8String:renderEnv] + : (rendererDefault ? rendererDefault : @"cpu"); + int renderMode = + ([renderStr isEqualToString:@"gl"] || [renderStr isEqualToString:@"opengl"]) ? 1 : 0; + + // Font family/size: defaults "fontFamily"/"fontSize", else Menlo/14. (No env override needed.) + NSString* fontFamilyDefault = [defaults stringForKey:@"fontFamily"]; + NSString* fontFamily = fontFamilyDefault ? fontFamilyDefault : @"Menlo"; + double fontSize = [defaults doubleForKey:@"fontSize"]; + if (fontSize <= 0.0) + fontSize = 14.0; + + // SSH destination: env CONTOUR_SSH, else defaults "ssh". Empty/absent = local shell. char const* sshEnv = getenv("CONTOUR_SSH"); - NSString* sshDestination = (sshEnv && sshEnv[0] != '\0') ? [NSString stringWithUTF8String:sshEnv] : nil; + NSString* sshDestination = sshEnv ? [NSString stringWithUTF8String:sshEnv] + : [defaults stringForKey:@"ssh"]; + if (![sshDestination length]) + sshDestination = nil; + + // Shell: defaults "shell", else nil = the account's login shell. (env $SHELL is not consulted — + // set defaults "shell" or leave it for the login shell.) + NSString* shell = [defaults stringForKey:@"shell"]; TerminalView* view = [[TerminalView alloc] initWithFrame:frame - fontFamily:@"Menlo" - fontSize:14.0 + fontFamily:fontFamily + fontSize:fontSize renderMode:renderMode theme:theme - sshDestination:sshDestination]; + sshDestination:sshDestination + shell:shell]; if (view) { [window setContentView:view];