From 61da15dc1b72ea5ad1ca45dbc2e331d5e37fbb98 Mon Sep 17 00:00:00 2001 From: l0ng-ai Date: Mon, 13 Jul 2026 20:06:53 +0800 Subject: [PATCH] feat(settings): add bell, notify-threshold, mouse-reporting, and session-restore controls (#68) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose four terminal preferences in Settings that previously had no knob (or were hardcoded): - Terminal → Bell: Off / Visual / Audible. Audible rings the system bell (NSBeep on macOS), falling back to the visual flash where no system bell exists so an opted-in bell is never silent. - Terminal → Notifications: configurable "long command" threshold (5s/10s/30s/1m), replacing the hardcoded 10s floor. - Terminal → Mouse: "Report mouse to apps" toggle. Off keeps the mouse local (native selection + scrollback) regardless of what a full-screen app requests; Shift still bypasses per gesture. Cached per view and pushed on config hot-reload. - Window & Tabs: "Restore previous session" toggle. When off, the daemon is restarted on launch so the previous session's shells are hung up instead of left running orphaned (this launch never re-attaches to them). Config gains a BellMode enum plus bell / notify_threshold_secs / mouse_reporting / restore_session fields, each defaulting to the prior behavior. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> --- Cargo.toml | 2 +- src/core/config.rs | 107 +++++++++++++++++++++++++++++++++ src/main.rs | 15 ++++- src/terminal/view.rs | 138 ++++++++++++++++++++++++++++++------------- src/ui/app.rs | 63 +++++++++++++++++++- src/ui/settings.rs | 83 +++++++++++++++++++++++++- 6 files changed, 361 insertions(+), 47 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c45e241a..db582bd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,7 +101,7 @@ winresource = "0.1" # so the native traffic-light buttons render in the right light/dark style. [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6" -objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSResponder", "NSAppearance"] } +objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSResponder", "NSAppearance", "NSGraphics"] } # x11/wayland are the Linux windowing backends; only pull them on Linux. The # Windows backend (`gpui_windows`) and macOS backend are selected by gpui_platform diff --git a/src/core/config.rs b/src/core/config.rs index 13d0cbf9..9266c761 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -91,6 +91,22 @@ pub struct Config { /// self-updates — it only links to the Releases page. On by default; set to /// `false` to skip the network call entirely (offline / privacy). pub check_for_updates: bool, + /// Seconds a foreground command must run before it's eligible for a + /// "command finished" notification (further gated by + /// `notify_on_command_finish`). Defaults to 10; clamped in `sanitize` so a + /// hand-edit can't set a degenerate value. + #[serde(default = "default_notify_threshold_secs")] + pub notify_threshold_secs: u64, + /// Restore the previous session (tab/split layout + each pane's cwd) on + /// launch. On by default; when off, every launch starts with a single fresh + /// terminal instead of the last window's layout. The session is still saved + /// on quit — it's just ignored at startup. + #[serde(default = "default_true")] + pub restore_session: bool, + /// How the terminal bell (BEL / `^G`) is signalled. Defaults to a brief + /// visual flash (the current behavior). + #[serde(default, deserialize_with = "de_lenient")] + pub bell: BellMode, // ── Appearance ────────────────────────────────────────────────────────── /// The shape drawn for the terminal cursor. @@ -114,6 +130,13 @@ pub struct Config { /// Multiplier applied to mouse-wheel scroll distance. 1.0 = one row per wheel /// line (the raw amount). Clamped to a sane band in `sanitize`. pub mouse_scroll_multiplier: f32, + /// Report mouse events (click / drag / wheel) to full-screen apps that ask + /// for them (vim, tmux, htop). On by default. When off, the mouse always + /// stays local — native selection and scrollback — regardless of what the + /// app requested. Holding Shift already forces local behavior for a single + /// gesture even while this is on. + #[serde(default = "default_true")] + pub mouse_reporting: bool, /// Drop trailing whitespace from each copied line. Off by default. pub clipboard_trim_trailing_spaces: bool, /// Copy a mouse selection to the clipboard as soon as the gesture ends, @@ -215,6 +238,20 @@ pub enum NotifyMode { Always, } +/// How the terminal bell (BEL / `^G`) is signalled (see [`Config::bell`]). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum BellMode { + /// Ignore the bell entirely — no flash, no sound. + None, + /// A brief visual flash of the terminal (the current behavior). + #[default] + Visual, + /// Ring the system bell. On platforms without one, falls back to a flash so + /// an opted-in bell is never silent. + Audible, +} + /// A shell program plus its launch arguments. Mirrors `alacritty_terminal`'s /// `tty::Shell`, but lives here so config has no dependency on the PTY crate and /// the daemon can read it straight from `config.json`. @@ -276,6 +313,11 @@ impl Default for Config { // Opt-out, not opt-in: a stale terminal that never tells you it's // outdated is the status quo we're fixing. One cheap GET at startup. check_for_updates: true, + notify_threshold_secs: default_notify_threshold_secs(), + restore_session: true, + // Visual flash preserves the pre-config behavior (the bell always + // flashed); opting into None/Audible is a deliberate change. + bell: BellMode::Visual, cursor_style: CursorStyle::Block, // Input/mouse defaults preserve today's behavior: Option composes // characters as macOS ships it (opt into Option-as-Meta); GPUI @@ -286,6 +328,7 @@ impl Default for Config { mouse_hide_while_typing: true, focus_follows_mouse: false, mouse_scroll_multiplier: 1.0, + mouse_reporting: true, clipboard_trim_trailing_spaces: false, copy_on_select: false, startup_mode: StartupMode::Normal, @@ -344,6 +387,10 @@ impl Config { self.mouse_scroll_multiplier = Config::default().mouse_scroll_multiplier; } self.mouse_scroll_multiplier = self.mouse_scroll_multiplier.clamp(0.1, 10.0); + // Keep the notify threshold in a usable band: a 1s floor so it can't fire + // on every trivial command, and a 1-hour ceiling above which "long + // command" stops meaning anything. + self.notify_threshold_secs = self.notify_threshold_secs.clamp(1, 3600); } /// Write the current config back to disk, creating the parent directory if @@ -509,6 +556,19 @@ fn default_preset() -> String { "default".to_string() } +/// Serde default for the several `bool` fields that default to `true` (so a +/// config predating them, or one omitting them, keeps the on-by-default +/// behavior instead of deserializing to `false`). +fn default_true() -> bool { + true +} + +/// Serde default for [`Config::notify_threshold_secs`]: the 10-second floor a +/// command had to cross before this was configurable. +fn default_notify_threshold_secs() -> u64 { + 10 +} + /// Serde default for [`Config::prefix`]: tmux's classic `C-b`. fn default_prefix() -> String { "ctrl-b".to_string() @@ -709,6 +769,53 @@ mod tests { assert_eq!(clamp(usize::MAX), MAX_SCROLLBACK); // ceiling } + #[test] + fn new_terminal_prefs_default_and_parse_leniently() { + // Defaults preserve the pre-config behavior: restore on, mouse reporting + // on, a 10s notify floor, and a visual bell. + let cfg = Config::default(); + assert!(cfg.restore_session); + assert!(cfg.mouse_reporting); + assert_eq!(cfg.notify_threshold_secs, 10); + assert_eq!(cfg.bell, BellMode::Visual); + + // A config predating these fields keeps the on-by-default booleans (not + // `false`) and the 10s floor. + let cfg: Config = serde_json::from_str(r#"{"font_size": 15.0}"#).unwrap(); + assert!(cfg.restore_session); + assert!(cfg.mouse_reporting); + assert_eq!(cfg.notify_threshold_secs, 10); + assert_eq!(cfg.bell, BellMode::Visual); + + // Valid values round-trip; a bad bell string falls back without failing + // the whole parse. + let cfg: Config = serde_json::from_str( + r#"{"restore_session": false, "mouse_reporting": false, "bell": "audible"}"#, + ) + .unwrap(); + assert!(!cfg.restore_session); + assert!(!cfg.mouse_reporting); + assert_eq!(cfg.bell, BellMode::Audible); + + let cfg: Config = serde_json::from_str(r#"{"bell": "loud"}"#).unwrap(); + assert_eq!(cfg.bell, BellMode::Visual); + } + + #[test] + fn sanitize_clamps_notify_threshold_into_band() { + let clamp = |n: u64| { + let mut cfg = Config { + notify_threshold_secs: n, + ..Config::default() + }; + cfg.sanitize(); + cfg.notify_threshold_secs + }; + assert_eq!(clamp(0), 1); // floor + assert_eq!(clamp(10), 10); // untouched in-band + assert_eq!(clamp(100_000), 3600); // ceiling + } + #[test] fn keybinding_preset_and_prefix_default_and_round_trip() { // Missing fields fall back to the no-op preset and the tmux-classic prefix. diff --git a/src/main.rs b/src/main.rs index d960f752..96f13585 100644 --- a/src/main.rs +++ b/src/main.rs @@ -271,7 +271,20 @@ fn main() { // daemon if none is running (sharing our config dir). Failure is non-fatal — // we log and continue; a still-absent daemon will surface later when a // RemoteTerminal fails to connect, rather than blocking startup here. - if let Err(e) = crate::daemon::spawn::ensure_running() { + // + // When session restore is off, start the daemon *fresh* instead of reusing a + // live one: this launch won't re-attach to the previous session's panes, so + // reusing the daemon would leave those shells running orphaned (unreachable, + // never hung up). `restart()` hangs up every old shell then spawns a clean + // daemon — and is safe (equivalent to a plain spawn) when none is running. + // Read straight off disk; the `Config` global isn't set until inside `run`. + let restore_session = crate::core::config::Config::load().restore_session; + let daemon_result = if restore_session { + crate::daemon::spawn::ensure_running() + } else { + crate::daemon::spawn::restart() + }; + if let Err(e) = daemon_result { log::error!("failed to ensure daemon is running: {e}"); } diff --git a/src/terminal/view.rs b/src/terminal/view.rs index 6ab795f5..08470989 100644 --- a/src/terminal/view.rs +++ b/src/terminal/view.rs @@ -28,7 +28,7 @@ use super::typeahead::{RawInput, Typeahead}; use crate::core::actions::{ CloseActiveTab, NewTab, SendBackTab, SendTab, SplitDown, SplitRight, ToggleMaximizePane, }; -use crate::core::config::{Config, NotifyMode}; +use crate::core::config::{BellMode, Config, NotifyMode}; use crate::daemon::protocol::{RemoteContext, ShellSpec}; /// Inset (px) between the terminal-surface edge and the cell grid. The prompt @@ -165,6 +165,13 @@ pub struct TerminalView { /// True for a brief window after a bell event; drives a momentary visual /// flash painted in place of an audible beep. pub bell_flash: bool, + /// Whether mouse events are reported to full-screen apps that request it + /// (`Config::mouse_reporting`). Cached from the global at construction and + /// refreshed on config hot-reload (`Tty7App::reload_from_config`) so the + /// mouse-report gates — which run in `&self`/`&mut self` methods without a + /// `cx` — can consult it. When `false`, every mouse-tracking mode reads as + /// clear, keeping the mouse local (selection + scrollback). + pub report_mouse: bool, /// Last observed "shell is idle at its prompt" state, tracked so a change can /// trigger a redraw (showing/hiding the line editor) even when the shell /// produced no output to repaint on its own. @@ -336,9 +343,9 @@ enum CmdKey { FallThrough, } -/// A foreground command must run at least this long for its completion to be -/// worth a background notification. -const LONG_COMMAND: std::time::Duration = std::time::Duration::from_secs(10); +// The minimum foreground-command duration worth a "finished" notification is +// configurable (`Config::notify_threshold_secs`, default 10s); read live where +// the notification is posted rather than pinned to a const here. /// How long gap input may be held client-side before it must be released to /// the PTY (see the `hold` module). Long enough for a fast command's full @@ -399,6 +406,24 @@ fn notify_command_finished(label: &str, elapsed: std::time::Duration) { super::remote::notify_desktop(Some("tty7"), &body); } +/// Ring the OS system bell for the `Audible` bell mode. Returns `true` if a +/// sound was actually requested, `false` on platforms without a portable beep +/// (the caller then falls back to the visual flash so the bell is never silent). +fn ring_system_bell() -> bool { + #[cfg(target_os = "macos")] + { + // A parameter-less AppKit call that just asks the system to play the + // user's alert sound; invoked on the main (gpui app) thread, where every + // `AlacEvent` is handled. + objc2_app_kit::NSBeep(); + true + } + #[cfg(not(target_os = "macos"))] + { + false + } +} + /// Build the byte sequence written to the PTY for a paste. Under bracketed paste /// the content is wrapped in the `ESC[200~` / `ESC[201~` markers, and every ESC /// (`0x1b`) byte is stripped from the content first. Without that strip, clipboard @@ -590,6 +615,7 @@ impl TerminalView { let font_size = px(config.font_size); let line_height_mul = config.line_height; let font_features = config.font_features.clone(); + let report_mouse = config.mouse_reporting; let mut font = gpui::font(font_family); font.fallbacks = Some(gpui::FontFallbacks::from_fonts(fallbacks.clone())); if let Some(features) = &font_features { @@ -762,6 +788,7 @@ impl TerminalView { title: "tty7".to_string(), marked_text: String::new(), last_mouse_cell: None, + report_mouse, last_hover_cell: None, link_modifier_down: false, scroll_debt: 0., @@ -887,22 +914,20 @@ impl TerminalView { }; self.terminal.write(fmt(rgb).into_bytes()); } - AlacEvent::Bell => { - // Visual bell: a brief flash instead of an audible beep. Turn it - // on now, then schedule a one-shot task to clear it ~150ms later. - self.bell_flash = true; - cx.notify(); - cx.spawn(async move |this, cx| { - cx.background_executor() - .timer(std::time::Duration::from_millis(150)) - .await; - let _ = this.update(cx, |view, cx| { - view.bell_flash = false; - cx.notify(); - }); - }) - .detach(); - } + AlacEvent::Bell => match cx.global::().bell { + // Silenced: neither flash nor sound. + BellMode::None => {} + // Visual bell: a brief flash instead of an audible beep. + BellMode::Visual => self.flash_bell(cx), + // Audible bell: ring the system bell. Where none exists (non-mac + // today), fall back to the flash so an opted-in bell is never + // silent. + BellMode::Audible => { + if !ring_system_bell() { + self.flash_bell(cx); + } + } + }, AlacEvent::TextAreaSizeRequest(fmt) => { // CSI 14 t: the text area size in pixels. Image-preview TUIs // (yazi, ranger's chafa/sixel backends) size their graphics @@ -1693,12 +1718,32 @@ impl TerminalView { // ---- Mouse tracking (so vim / tmux / zellij get clicks & drags) ---- /// True when the application has enabled any mouse-reporting mode. + /// Drive the momentary visual bell flash: turn it on now, then schedule a + /// one-shot task to clear it ~150ms later. Shared by the `Visual` bell mode + /// and the `Audible` fallback on platforms without a system bell. + fn flash_bell(&mut self, cx: &mut Context) { + self.bell_flash = true; + cx.notify(); + cx.spawn(async move |this, cx| { + cx.background_executor() + .timer(std::time::Duration::from_millis(150)) + .await; + let _ = this.update(cx, |view, cx| { + view.bell_flash = false; + cx.notify(); + }); + }) + .detach(); + } + pub fn mouse_mode(&self) -> bool { - self.terminal - .term - .lock() - .mode() - .intersects(TermMode::MOUSE_MODE) + self.report_mouse + && self + .terminal + .term + .lock() + .mode() + .intersects(TermMode::MOUSE_MODE) } /// Encode and send a single mouse event to the PTY. `base` is the raw button @@ -1743,12 +1788,13 @@ impl TerminalView { if self.last_mouse_cell == Some((col, row)) { return; } - let wants = self - .terminal - .term - .lock() - .mode() - .intersects(TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION); + let wants = self.report_mouse + && self + .terminal + .term + .lock() + .mode() + .intersects(TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION); if !wants { return; } @@ -1771,12 +1817,13 @@ impl TerminalView { if self.last_mouse_cell == Some((col, row)) { return; } - if !self - .terminal - .term - .lock() - .mode() - .contains(TermMode::MOUSE_MOTION) + if !self.report_mouse + || !self + .terminal + .term + .lock() + .mode() + .contains(TermMode::MOUSE_MOTION) { return; } @@ -1790,7 +1837,13 @@ impl TerminalView { if lines == 0 { return; } - let mode = *self.terminal.term.lock().mode(); + let mut mode = *self.terminal.term.lock().mode(); + // "Mouse reporting off" also silences the wheel: drop the report mode so + // the tick falls through to alternate-scroll / local scrollback, exactly + // as if the app had never asked for wheel reporting. + if !self.report_mouse { + mode.remove(TermMode::MOUSE_MODE); + } match wheel_route(mode, mods.shift, lines > 0) { // Mouse-wheel reporting: one report per line, at the last mouse cell. WheelRoute::Report { base } => { @@ -2005,13 +2058,16 @@ impl TerminalView { let title = std::mem::take(&mut self.running_title); self.running_since = None; // Gate on the configured policy: never / only-when-unfocused / - // always. The long-command floor still applies regardless. - let notify = match cx.global::().notify_on_command_finish { + // always. The configured long-command floor still applies + // regardless. + let cfg = cx.global::(); + let notify = match cfg.notify_on_command_finish { NotifyMode::Never => false, NotifyMode::Unfocused => !window.is_window_active(), NotifyMode::Always => true, }; - if elapsed >= LONG_COMMAND && notify { + let threshold = std::time::Duration::from_secs(cfg.notify_threshold_secs); + if elapsed >= threshold && notify { notify_command_finished(&title, elapsed); } } diff --git a/src/ui/app.rs b/src/ui/app.rs index 60a362e8..d4399f09 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -204,8 +204,15 @@ pub struct Tty7App { impl Tty7App { pub fn new(window: &mut Window, cx: &mut Context) -> Self { - // Restore the previous session (tab/split layout + each pane's cwd). - Self::with_session(Session::load(), window, cx) + // Restore the previous session (tab/split layout + each pane's cwd), + // unless the user turned restore off — then start fresh. `None` takes the + // first-run path in `with_session`, spawning a single default terminal. + let session = if cx.global::().restore_session { + Session::load() + } else { + None + }; + Self::with_session(session, window, cx) } /// The whole constructor behind `new`, with the saved session injected @@ -990,6 +997,29 @@ impl Tty7App { self.update_config(cx, |cfg| cfg.notify_on_command_finish = mode); } + /// Set the "long command" floor (seconds) a foreground command must exceed + /// to be eligible for a completion notification. Read live where the alert + /// is posted, so nothing needs pushing to open panes. + pub(crate) fn set_notify_threshold(&mut self, secs: u64, cx: &mut Context) { + self.update_config(cx, |cfg| cfg.notify_threshold_secs = secs.clamp(1, 3600)); + } + + /// Switch how the terminal bell is signalled. Read live in each pane's bell + /// handler, so there's nothing to push. + pub(crate) fn set_bell_mode( + &mut self, + mode: crate::core::config::BellMode, + cx: &mut Context, + ) { + self.update_config(cx, |cfg| cfg.bell = mode); + } + + /// Toggle session restore. Takes effect on the next launch (this only + /// persists the preference); the current window is untouched. + pub(crate) fn set_restore_session(&mut self, on: bool, cx: &mut Context) { + self.update_config(cx, |cfg| cfg.restore_session = on); + } + // ── Input / Mouse setters ─────────────────────────────────────────────── /// Takes effect on the next keystroke — the terminal reads the flag per @@ -1008,6 +1038,21 @@ impl Tty7App { self.update_config(cx, |cfg| cfg.focus_follows_mouse = on); } + /// Toggle whether mouse events reach full-screen apps. The gates are cached + /// per view, so this pushes the new value into every open pane (like the + /// font setters) in addition to persisting it. + pub(crate) fn set_mouse_reporting(&mut self, on: bool, cx: &mut Context) { + self.update_config(cx, |cfg| cfg.mouse_reporting = on); + for tab in &self.tabs { + for leaf in tab.pane.leaves() { + leaf.update(cx, |v, cx| { + v.report_mouse = on; + cx.notify(); + }); + } + } + } + pub(crate) fn set_mouse_scroll_multiplier(&mut self, mult: f32, cx: &mut Context) { self.update_config(cx, |cfg| { cfg.mouse_scroll_multiplier = mult.clamp(0.1, 10.0) @@ -1992,6 +2037,20 @@ impl Tty7App { } } } + // Mouse-reporting is cached per view (the gates run without a `cx`), so a + // hot-reload must push it into every open pane. Diffed per leaf so an + // unrelated config edit doesn't churn panes that already agree. + let report_mouse = cx.global::().mouse_reporting; + for tab in &self.tabs { + for leaf in tab.pane.leaves() { + leaf.update(cx, |v, cx| { + if v.report_mouse != report_mouse { + v.report_mouse = report_mouse; + cx.notify(); + } + }); + } + } cx.notify(); } diff --git a/src/ui/settings.rs b/src/ui/settings.rs index d348e9fb..6391d4d5 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -20,7 +20,7 @@ use gpui_component::switch::Switch; use gpui_component::{ActiveTheme as _, Icon, IconName, Sizable as _, h_flex, v_flex}; use std::sync::Arc; -use crate::core::config::{Config, CursorStyle, NewTabPosition, NotifyMode}; +use crate::core::config::{BellMode, Config, CursorStyle, NewTabPosition, NotifyMode}; use crate::ui::app::{FONT_SIZE_STEP, LINE_HEIGHT_STEP, ThemeEdit, Tty7App}; use crate::ui::presets; @@ -832,6 +832,16 @@ impl Tty7App { let scroll_mult = cfg.mouse_scroll_multiplier; let clip_trim = cfg.clipboard_trim_trailing_spaces; let copy_on_select = cfg.copy_on_select; + let mouse_reporting = cfg.mouse_reporting; + let bell = cfg.bell; + // Map the persisted threshold onto its preset radio index (nearest slot + // for any off-preset value a hand-edit might leave). + let threshold_idx = match cfg.notify_threshold_secs { + n if n <= 5 => 0, + n if n <= 10 => 1, + n if n <= 30 => 2, + _ => 3, + }; // Map the persisted scrollback depth onto its preset radio index (default // to 10k's slot for any off-preset value a hand-edit might leave). let scrollback_idx = match cfg.scrollback_limit { @@ -904,6 +914,44 @@ impl Tty7App { .checked(copy_on_select) .on_click(cx.listener(|this, on: &bool, _w, cx| this.set_copy_on_select(*on, cx))) .into_any_element(); + let mouse_report_switch = Switch::new("term-mouse-report") + .checked(mouse_reporting) + .on_click(cx.listener(|this, on: &bool, _w, cx| this.set_mouse_reporting(*on, cx))) + .into_any_element(); + let bell_idx = match bell { + BellMode::None => 0, + BellMode::Visual => 1, + BellMode::Audible => 2, + }; + let bell_control = self.segmented( + "term-bell", + &["Off", "Visual", "Audible"], + bell_idx, + cx, + |this, ix, _w, cx| { + let mode = match ix { + 0 => BellMode::None, + 1 => BellMode::Visual, + _ => BellMode::Audible, + }; + this.set_bell_mode(mode, cx); + }, + ); + let threshold_radio = self.segmented( + "term-notify-threshold", + &["5s", "10s", "30s", "1m"], + threshold_idx, + cx, + |this, ix, _w, cx| { + let secs = match ix { + 0 => 5, + 1 => 10, + 2 => 30, + _ => 60, + }; + this.set_notify_threshold(secs, cx); + }, + ); // macOS only: the Option/special-character split this toggle resolves // doesn't exist on other platforms, where Alt always carries Meta. let option_alt_row = cfg!(target_os = "macos").then(|| { @@ -964,6 +1012,12 @@ impl Tty7App { mouse_hide_switch, cx, )) + .child(self.settings_row( + "Report mouse to apps", + "Let full-screen apps (vim, tmux) handle clicks and scrolling. Off keeps the mouse local; Shift always does for one gesture.", + mouse_report_switch, + cx, + )) .when_some(option_alt_row, |v, row| { v.child(self.section_rule(cx)) .child(self.section_header("Keyboard", cx)) @@ -998,13 +1052,27 @@ impl Tty7App { cx, )) .child(self.section_rule(cx)) + .child(self.section_header("Bell", cx)) + .child(self.settings_row( + "Terminal bell", + "How a bell (^G) is signalled: silenced, a brief flash, or the system sound.", + bell_control, + cx, + )) + .child(self.section_rule(cx)) .child(self.section_header("Notifications", cx)) .child(self.settings_row( "Notify on command finish", - "Desktop alert after a long (≥10s) command completes.", + "Desktop alert after a long foreground command completes.", notify_radio, cx, )) + .child(self.settings_row( + "Notify threshold", + "How long a command must run to qualify as \"long\".", + threshold_radio, + cx, + )) .into_any_element() } @@ -1020,7 +1088,12 @@ impl Tty7App { NewTabPosition::AfterCurrent => 0, NewTabPosition::End => 1, }; + let restore_session = cfg.restore_session; + let restore_switch = Switch::new("wt-restore-session") + .checked(restore_session) + .on_click(cx.listener(|this, on: &bool, _w, cx| this.set_restore_session(*on, cx))) + .into_any_element(); let startup_radio = self.segmented( "wt-startup", &["Normal", "Maximized", "Fullscreen"], @@ -1058,6 +1131,12 @@ impl Tty7App { startup_radio, cx, )) + .child(self.settings_row( + "Restore previous session", + "Reopen the last window's tabs, splits, and directories on launch. Off starts with a single fresh terminal.", + restore_switch, + cx, + )) .child(self.section_rule(cx)) .child(self.section_header("Tabs", cx)) .child(self.settings_row(