mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
feat(settings): add bell, notify-threshold, mouse-reporting, and session-restore controls (#68)
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>
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
+14
-1
@@ -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}");
|
||||
}
|
||||
|
||||
|
||||
+97
-41
@@ -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::<Config>().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>) {
|
||||
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::<Config>().notify_on_command_finish {
|
||||
// always. The configured long-command floor still applies
|
||||
// regardless.
|
||||
let cfg = cx.global::<Config>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+61
-2
@@ -204,8 +204,15 @@ pub struct Tty7App {
|
||||
|
||||
impl Tty7App {
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> 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::<Config>().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>) {
|
||||
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>,
|
||||
) {
|
||||
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>) {
|
||||
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>) {
|
||||
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>) {
|
||||
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::<Config>().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();
|
||||
}
|
||||
|
||||
|
||||
+81
-2
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user