Reliable letter-based default keybinds + kitty keyboard protocol

alt+tab is grabbed by desktop WMs and alt+arrows are re-encoded or
swallowed by several terminals, leaking plain arrow keys into the
focused window. Primary defaults are now letter chords which every
terminal delivers via ESC-prefix: alt+w (switcher), alt+h/alt+l
(snap), alt+m (maximize). The alt+tab/arrow variants stay as extra
bindings, and standalone/client now enable the kitty keyboard
protocol (DISAMBIGUATE_ESCAPE_CODES) when the terminal supports it
so even those are delivered unambiguously (Konsole 23.08+, kitty,
foot, wezterm, ghostty). 34/34 integration checks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 20:15:17 +02:00
parent 8296081557
commit ca13abc9b7
9 changed files with 152 additions and 32 deletions

View File

@@ -7,8 +7,8 @@ En terminal-baserad fönsterhanterare med flytande fönster, full mussuport och
- Flytande terminaler med fri positionering och storleksändring - Flytande terminaler med fri positionering och storleksändring
- Full mussuport: vänster/höger/mitten-klick, scroll, drag, ANSI-muse-protokoll vidarebefordras till appar - Full mussuport: vänster/höger/mitten-klick, scroll, drag, ANSI-muse-protokoll vidarebefordras till appar
- Scrollback-historik med scrollbar + copy-mode med sökning (`alt+c`) - Scrollback-historik med scrollbar + copy-mode med sökning (`alt+c`)
- Fönsterväxlare i MRU-ordning (`alt+tab`/`alt+w`) - Fönsterväxlare i MRU-ordning (`alt+w`, även `alt+tab` där den inte fångas av skrivbordet)
- Snap till halvskärm (`alt+left`/`alt+right` eller dra mot kanten) och maximera (`alt+up`/dubbelklick på titelraden) - Snap till halvskärm (`alt+h`/`alt+l` eller dra mot kanten) och maximera (`alt+m`/dubbelklick på titelraden) — alt+pilar fungerar också i terminaler med kitty keyboard-protokoll
- Teman i egna TOML-filer (`themes/`) + inbyggd inställningsdialog (kugghjulet `≡` i panelen) - Teman i egna TOML-filer (`themes/`) + inbyggd inställningsdialog (kugghjulet `≡` i panelen)
- Hot-reload: config.toml, panel- och temafiler laddas om live när de ändras - Hot-reload: config.toml, panel- och temafiler laddas om live när de ändras
- Panel (topp/botten) med klickbara knappar och dynamiska status-widgets - Panel (topp/botten) med klickbara knappar och dynamiska status-widgets

View File

@@ -45,34 +45,49 @@ label = "Ny terminal"
action = { type = "spawn_terminal" } action = { type = "spawn_terminal" }
[[keybind]] [[keybind]]
key = "alt+tab" key = "alt+w"
scope = "global" scope = "global"
label = "Fönsterväxlare" label = "Fönsterväxlare"
action = { type = "window_switcher" } action = { type = "window_switcher" }
[[keybind]] [[keybind]]
key = "alt+w" key = "alt+tab"
scope = "global" scope = "global"
action = { type = "window_switcher" } action = { type = "window_switcher" }
[[keybind]] [[keybind]]
key = "alt+left" key = "alt+h"
scope = "global" scope = "global"
label = "Fäst vänster" label = "Fäst vänster"
action = { type = "snap_left" } action = { type = "snap_left" }
[[keybind]] [[keybind]]
key = "alt+right" key = "alt+left"
scope = "global"
action = { type = "snap_left" }
[[keybind]]
key = "alt+l"
scope = "global" scope = "global"
label = "Fäst höger" label = "Fäst höger"
action = { type = "snap_right" } action = { type = "snap_right" }
[[keybind]] [[keybind]]
key = "alt+up" key = "alt+right"
scope = "global"
action = { type = "snap_right" }
[[keybind]]
key = "alt+m"
scope = "global" scope = "global"
label = "Maximera/återställ" label = "Maximera/återställ"
action = { type = "maximize" } action = { type = "maximize" }
[[keybind]]
key = "alt+up"
scope = "global"
action = { type = "maximize" }
[[keybind]] [[keybind]]
key = "alt+c" key = "alt+c"
scope = "global" scope = "global"

View File

@@ -44,37 +44,54 @@ scope = "global"
label = "Ny terminal" label = "Ny terminal"
action = { type = "spawn_terminal" } action = { type = "spawn_terminal" }
# Fönsterväxlare (Alt+Tab-overlay i MRU-ordning). # Fönsterväxlare (MRU-ordning). alt+w är primär — alt+tab fångas ofta
# OBS: vissa terminaler/skrivbord fångar alt+tab själva — alt+w är reserv. # av skrivbordsmiljön och alt+pilar av vissa terminaler; bokstavs-binds
# fungerar överallt. (I terminaler med kitty keyboard-protokoll
# levereras även alt+tab/pilar korrekt.)
[[keybind]] [[keybind]]
key = "alt+tab" key = "alt+w"
scope = "global" scope = "global"
label = "Fönsterväxlare" label = "Fönsterväxlare"
action = { type = "window_switcher" } action = { type = "window_switcher" }
[[keybind]] [[keybind]]
key = "alt+w" key = "alt+tab"
scope = "global" scope = "global"
action = { type = "window_switcher" } action = { type = "window_switcher" }
# Fäst fokuserat fönster som vänster/höger halvskärm # Fäst fokuserat fönster som vänster/höger halvskärm
[[keybind]] [[keybind]]
key = "alt+left" key = "alt+h"
scope = "global" scope = "global"
label = "Fäst vänster" label = "Fäst vänster"
action = { type = "snap_left" } action = { type = "snap_left" }
[[keybind]] [[keybind]]
key = "alt+right" key = "alt+left"
scope = "global"
action = { type = "snap_left" }
[[keybind]]
key = "alt+l"
scope = "global" scope = "global"
label = "Fäst höger" label = "Fäst höger"
action = { type = "snap_right" } action = { type = "snap_right" }
[[keybind]]
key = "alt+right"
scope = "global"
action = { type = "snap_right" }
# Maximera/återställ fokuserat fönster (även dubbelklick på titelraden) # Maximera/återställ fokuserat fönster (även dubbelklick på titelraden)
[[keybind]]
key = "alt+m"
scope = "global"
label = "Maximera/återställ"
action = { type = "maximize" }
[[keybind]] [[keybind]]
key = "alt+up" key = "alt+up"
scope = "global" scope = "global"
label = "Maximera/återställ"
action = { type = "maximize" } action = { type = "maximize" }
# Copy-mode: bläddra/söka i fokuserat fönsters historik med tangentbordet # Copy-mode: bläddra/söka i fokuserat fönsters historik med tangentbordet

View File

@@ -45,34 +45,49 @@ label = "Ny terminal"
action = { type = "spawn_terminal" } action = { type = "spawn_terminal" }
[[keybind]] [[keybind]]
key = "alt+tab" key = "alt+w"
scope = "global" scope = "global"
label = "Fönsterväxlare" label = "Fönsterväxlare"
action = { type = "window_switcher" } action = { type = "window_switcher" }
[[keybind]] [[keybind]]
key = "alt+w" key = "alt+tab"
scope = "global" scope = "global"
action = { type = "window_switcher" } action = { type = "window_switcher" }
[[keybind]] [[keybind]]
key = "alt+left" key = "alt+h"
scope = "global" scope = "global"
label = "Fäst vänster" label = "Fäst vänster"
action = { type = "snap_left" } action = { type = "snap_left" }
[[keybind]] [[keybind]]
key = "alt+right" key = "alt+left"
scope = "global"
action = { type = "snap_left" }
[[keybind]]
key = "alt+l"
scope = "global" scope = "global"
label = "Fäst höger" label = "Fäst höger"
action = { type = "snap_right" } action = { type = "snap_right" }
[[keybind]] [[keybind]]
key = "alt+up" key = "alt+right"
scope = "global"
action = { type = "snap_right" }
[[keybind]]
key = "alt+m"
scope = "global" scope = "global"
label = "Maximera/återställ" label = "Maximera/återställ"
action = { type = "maximize" } action = { type = "maximize" }
[[keybind]]
key = "alt+up"
scope = "global"
action = { type = "maximize" }
[[keybind]] [[keybind]]
key = "alt+c" key = "alt+c"
scope = "global" scope = "global"

25
dist/win/config.toml vendored
View File

@@ -45,34 +45,49 @@ label = "Ny PowerShell-terminal"
action = { type = "spawn_terminal", shell = "powershell.exe" } action = { type = "spawn_terminal", shell = "powershell.exe" }
[[keybind]] [[keybind]]
key = "alt+tab" key = "alt+w"
scope = "global" scope = "global"
label = "Fönsterväxlare" label = "Fönsterväxlare"
action = { type = "window_switcher" } action = { type = "window_switcher" }
[[keybind]] [[keybind]]
key = "alt+w" key = "alt+tab"
scope = "global" scope = "global"
action = { type = "window_switcher" } action = { type = "window_switcher" }
[[keybind]] [[keybind]]
key = "alt+left" key = "alt+h"
scope = "global" scope = "global"
label = "Fäst vänster" label = "Fäst vänster"
action = { type = "snap_left" } action = { type = "snap_left" }
[[keybind]] [[keybind]]
key = "alt+right" key = "alt+left"
scope = "global"
action = { type = "snap_left" }
[[keybind]]
key = "alt+l"
scope = "global" scope = "global"
label = "Fäst höger" label = "Fäst höger"
action = { type = "snap_right" } action = { type = "snap_right" }
[[keybind]] [[keybind]]
key = "alt+up" key = "alt+right"
scope = "global"
action = { type = "snap_right" }
[[keybind]]
key = "alt+m"
scope = "global" scope = "global"
label = "Maximera/återställ" label = "Maximera/återställ"
action = { type = "maximize" } action = { type = "maximize" }
[[keybind]]
key = "alt+up"
scope = "global"
action = { type = "maximize" }
[[keybind]] [[keybind]]
key = "alt+c" key = "alt+c"
scope = "global" scope = "global"

View File

@@ -60,9 +60,21 @@ pub fn run(socket_path: &str) -> io::Result<()> {
enable_raw_mode()?; enable_raw_mode()?;
let mut stdout = io::stdout(); let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture, crossterm::cursor::Hide)?; execute!(stdout, EnterAlternateScreen, EnableMouseCapture, crossterm::cursor::Hide)?;
let keyboard_enhanced = crossterm::terminal::supports_keyboard_enhancement().unwrap_or(false);
if keyboard_enhanced {
let _ = execute!(
io::stdout(),
crossterm::event::PushKeyboardEnhancementFlags(
crossterm::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
)
);
}
let result = client_loop(&mut stdout, &mut writer, &frame_rx, term_w, term_h); let result = client_loop(&mut stdout, &mut writer, &frame_rx, term_w, term_h);
if keyboard_enhanced {
let _ = execute!(io::stdout(), crossterm::event::PopKeyboardEnhancementFlags);
}
execute!( execute!(
stdout, stdout,
crossterm::cursor::Show, crossterm::cursor::Show,

View File

@@ -173,35 +173,53 @@ fn default_keybinds() -> Vec<KeybindConfig> {
label: Some("Ny terminal".to_string()), label: Some("Ny terminal".to_string()),
}, },
KeybindConfig { KeybindConfig {
key: "alt+tab".to_string(), key: "alt+w".to_string(),
scope: KeybindScope::Global, scope: KeybindScope::Global,
action: MenuAction::WindowSwitcher, action: MenuAction::WindowSwitcher,
label: Some("Fönsterväxlare".to_string()), label: Some("Fönsterväxlare".to_string()),
}, },
KeybindConfig { KeybindConfig {
key: "alt+w".to_string(), key: "alt+tab".to_string(),
scope: KeybindScope::Global, scope: KeybindScope::Global,
action: MenuAction::WindowSwitcher, action: MenuAction::WindowSwitcher,
label: None, label: None,
}, },
KeybindConfig { KeybindConfig {
key: "alt+left".to_string(), key: "alt+h".to_string(),
scope: KeybindScope::Global, scope: KeybindScope::Global,
action: MenuAction::SnapLeft, action: MenuAction::SnapLeft,
label: Some("Fäst vänster".to_string()), label: Some("Fäst vänster".to_string()),
}, },
KeybindConfig { KeybindConfig {
key: "alt+right".to_string(), key: "alt+left".to_string(),
scope: KeybindScope::Global,
action: MenuAction::SnapLeft,
label: None,
},
KeybindConfig {
key: "alt+l".to_string(),
scope: KeybindScope::Global, scope: KeybindScope::Global,
action: MenuAction::SnapRight, action: MenuAction::SnapRight,
label: Some("Fäst höger".to_string()), label: Some("Fäst höger".to_string()),
}, },
KeybindConfig { KeybindConfig {
key: "alt+up".to_string(), key: "alt+right".to_string(),
scope: KeybindScope::Global,
action: MenuAction::SnapRight,
label: None,
},
KeybindConfig {
key: "alt+m".to_string(),
scope: KeybindScope::Global, scope: KeybindScope::Global,
action: MenuAction::Maximize, action: MenuAction::Maximize,
label: Some("Maximera/återställ".to_string()), label: Some("Maximera/återställ".to_string()),
}, },
KeybindConfig {
key: "alt+up".to_string(),
scope: KeybindScope::Global,
action: MenuAction::Maximize,
label: None,
},
KeybindConfig { KeybindConfig {
key: "alt+c".to_string(), key: "alt+c".to_string(),
scope: KeybindScope::Global, scope: KeybindScope::Global,

View File

@@ -170,12 +170,17 @@ KONFIGURATION (./config.toml, annars ~/.config/tui-wm/config.toml):
TANGENTER (standard, ändras via [[keybind]] i config.toml): TANGENTER (standard, ändras via [[keybind]] i config.toml):
ctrl+space Kommandodialog — skriv t.ex. `htop` och tryck Enter ctrl+space Kommandodialog — skriv t.ex. `htop` och tryck Enter
alt+enter Ny terminal alt+enter Ny terminal
alt+tab/alt+w Fönsterväxlare (MRU) — Tab stegar, Enter väljer alt+w Fönsterväxlare (MRU) — Tab stegar, Enter väljer
alt+left/right Fäst fönster som vänster/höger halvskärm alt+h/alt+l Fäst fönster som vänster/höger halvskärm
alt+up Maximera/återställ (även dubbelklick på titelraden) alt+m Maximera/återställ (även dubbelklick på titelraden)
alt+c Copy-mode: bläddra/söka i historiken (/ n N q) alt+c Copy-mode: bläddra/söka i historiken (/ n N q)
ctrl+q Avsluta (från en ansluten klient: kopplar bara ner den) ctrl+q Avsluta (från en ansluten klient: kopplar bara ner den)
alt+tab och alt+pilar är också bundna men fångas ofta av
skrivbordsmiljön/terminalen — bokstavsvarianterna fungerar överallt.
I terminaler med kitty keyboard-protokoll (Konsole 23.08+, kitty,
foot, wezterm, ghostty) levereras även pil-varianterna korrekt.
TEMA & INSTÄLLNINGAR: TEMA & INSTÄLLNINGAR:
theme = \"themes/dark.toml\" i config.toml väljer tema (egna teman = theme = \"themes/dark.toml\" i config.toml väljer tema (egna teman =
egna TOML-filer). Kugghjulet ≡ i panelen öppnar inbyggda egna TOML-filer). Kugghjulet ≡ i panelen öppnar inbyggda
@@ -203,6 +208,18 @@ fn run_standalone() -> io::Result<()> {
enable_raw_mode()?; enable_raw_mode()?;
let mut stdout = io::stdout(); let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste, EnableFocusChange)?; execute!(stdout, EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste, EnableFocusChange)?;
// Kitty keyboard-protokoll (om terminalen stödjer det): gör att
// alt+pilar, alt+tab m.fl. levereras entydigt i stället för att
// kollidera med legacy-escape-sekvenser.
let keyboard_enhanced = crossterm::terminal::supports_keyboard_enhancement().unwrap_or(false);
if keyboard_enhanced {
let _ = execute!(
io::stdout(),
crossterm::event::PushKeyboardEnhancementFlags(
crossterm::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
)
);
}
let backend = CrosstermBackend::new(stdout); let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
@@ -212,6 +229,9 @@ fn run_standalone() -> io::Result<()> {
let result = run_standalone_loop(&mut terminal, &mut app, ipc_rx, &socket_path); let result = run_standalone_loop(&mut terminal, &mut app, ipc_rx, &socket_path);
if keyboard_enhanced {
let _ = execute!(io::stdout(), crossterm::event::PopKeyboardEnhancementFlags);
}
disable_raw_mode()?; disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture, DisableBracketedPaste, DisableFocusChange)?; execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture, DisableBracketedPaste, DisableFocusChange)?;
terminal.show_cursor()?; terminal.show_cursor()?;

View File

@@ -346,6 +346,14 @@ def main():
maxed = next(w for w in wmx["windows"] if w["id"] == sw_open["id"]) maxed = next(w for w in wmx["windows"] if w["id"] == sw_open["id"])
check("alt+up maximerar fönstret", maxed["width"] >= 95, check("alt+up maximerar fönstret", maxed["width"] >= 95,
f"geometri: {maxed}") f"geometri: {maxed}")
# Bokstavs-varianten: alt+m ska toggla tillbaka (återställa)
disp.send(key_event(ch="m", modifiers="ALT"))
time.sleep(0.3)
app.send({"type": "list_windows", "request_id": "lrest"})
wre = app.recv_until("window_list")
restored = next(w for w in wre["windows"] if w["id"] == sw_open["id"])
check("alt+m återställer (bokstavs-bind)", 45 <= restored["width"] <= 55,
f"geometri: {restored}")
app.send({"type": "close_window", "window_id": sw_open["id"]}) app.send({"type": "close_window", "window_id": sw_open["id"]})
# ── 4f. Copy-mode: alt+c, sök, avsluta ─────────────────────────── # ── 4f. Copy-mode: alt+c, sök, avsluta ───────────────────────────