Add --help, terminal scrollback + scrollbar, run-dialog fix, client-safe exit, true rounded corners
- -h/--help prints a full usage overview (modes, config keys, keys, mouse) - Run dialog now executes the typed command directly in the new window (previously it spawned the default shell and typed the command into it, which showed up as just an empty terminal) - Virtual terminals get vt100 scrollback (config scrollback_lines, default 1000): mouse wheel scrolls history when the app doesn't capture the mouse, arrow-key fallback on the alternate screen, any keypress jumps back to the bottom; optional scrollbar on the right border (show_scrollbar) plus configurable default_window_size - Exit triggered from a connected display client (ctrl+q / panel button) now only disconnects that client — the daemon/standalone session and its programs keep running - Windows now paint the border zone in the desktop background color so rounded corners actually look rounded instead of a filled square with an inner rounded frame; popup dialogs rounded too - Integration suite extended to 19 checks (scrollback, run dialog, client-safe exit) — all passing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
17
README.md
17
README.md
@@ -75,6 +75,17 @@ TUI-WM/
|
|||||||
|
|
||||||
## Konfiguration
|
## Konfiguration
|
||||||
|
|
||||||
|
Toppnivå-nycklar i `config.toml`:
|
||||||
|
|
||||||
|
| Nyckel | Default | Beskrivning |
|
||||||
|
|---|---|---|
|
||||||
|
| `default_shell` | `$SHELL` | Skal för nya terminaler |
|
||||||
|
| `default_window_size` | `[82, 26]` | `[bredd, höjd]` för nya terminalfönster |
|
||||||
|
| `scrollback_lines` | `1000` | Historikrader per virtuell terminal |
|
||||||
|
| `show_scrollbar` | `true` | Scrollbar på terminalfönstrens högerkant |
|
||||||
|
| `background_image` | – | Bakgrundsbild via Kitty graphics |
|
||||||
|
| `terminal_bg_color` | genomskinlig | Terminalfönstrens bakgrundsfärg |
|
||||||
|
|
||||||
Konfigurationen delas upp i två nivåer:
|
Konfigurationen delas upp i två nivåer:
|
||||||
|
|
||||||
1. **`config.toml`** — huvud-konfigfil, definierar paneler och keybinds
|
1. **`config.toml`** — huvud-konfigfil, definierar paneler och keybinds
|
||||||
@@ -206,7 +217,7 @@ Exempelnycklar: `a`–`z`, `0`–`9`, `space`, `enter`, `esc`, `tab`, `backspace
|
|||||||
| Vänsterklick på `[x]` | Stänger fönstret |
|
| Vänsterklick på `[x]` | Stänger fönstret |
|
||||||
| Vänsterklick på kant/hörn | Börjar storleksändra fönstret |
|
| Vänsterklick på kant/hörn | Börjar storleksändra fönstret |
|
||||||
| Klick i innehållsyta | Fokuserar fönstret och skickar musklick till appen i terminalen |
|
| Klick i innehållsyta | Fokuserar fönstret och skickar musklick till appen i terminalen |
|
||||||
| Scroll inuti terminal | Skickar scroll-event till appen (med ANSI mus-protokoll) eller pilar som fallback |
|
| Scroll inuti terminal | Vidarebefordras till appen om den fångar musen; annars scrollas terminalens egen historik (`scrollback_lines`, scrollbar på högerkanten). På alternativskärm utan mus-stöd skickas pilar |
|
||||||
| Höger/mitten-klick i terminal | Vidarebefordras direkt till appen |
|
| Höger/mitten-klick i terminal | Vidarebefordras direkt till appen |
|
||||||
|
|
||||||
Möss-tracking fungerar automatiskt — om appen i terminalen aktiverar ANSI mus-protokoll (t.ex. vim, htop, ncurses-appar) vidarebefordras alla mus-events korrekt med rätt encoding (SGR, UTF-8 eller default X10).
|
Möss-tracking fungerar automatiskt — om appen i terminalen aktiverar ANSI mus-protokoll (t.ex. vim, htop, ncurses-appar) vidarebefordras alla mus-events korrekt med rätt encoding (SGR, UTF-8 eller default X10).
|
||||||
@@ -215,6 +226,8 @@ Möss-tracking fungerar automatiskt — om appen i terminalen aktiverar ANSI mus
|
|||||||
|
|
||||||
## Startlägen
|
## Startlägen
|
||||||
|
|
||||||
|
Kör `tui-wm --help` för en komplett flagg- och tangentöversikt.
|
||||||
|
|
||||||
TUI-WM kan startas i tre lägen:
|
TUI-WM kan startas i tre lägen:
|
||||||
|
|
||||||
### Standalone (standard)
|
### Standalone (standard)
|
||||||
@@ -233,6 +246,8 @@ tui-wm -d
|
|||||||
|
|
||||||
Kör headless — ingen lokal terminal-rendering. State hanteras enbart via socketen. Frames renderas internt och skickas till anslutna display-klienter. Passar för att köra TUI-WM som bakgrundstjänst.
|
Kör headless — ingen lokal terminal-rendering. State hanteras enbart via socketen. Frames renderas internt och skickas till anslutna display-klienter. Passar för att köra TUI-WM som bakgrundstjänst.
|
||||||
|
|
||||||
|
Daemonen stängs INTE av att en klient kopplar ner eller trycker `ctrl+q` — det kopplar bara ner den klienten. Stoppa daemonen med `pkill tui-wm` eller via systemd.
|
||||||
|
|
||||||
### Klient (`-c`)
|
### Klient (`-c`)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
11
config.toml
11
config.toml
@@ -11,6 +11,17 @@ background_image = "/home/brasse/Bilder/backrunder/pexels-veeterzy-38136.jpg"
|
|||||||
# Standard-shell för nya terminaler. Om inte satt → $SHELL eller /bin/bash.
|
# Standard-shell för nya terminaler. Om inte satt → $SHELL eller /bin/bash.
|
||||||
# default_shell = "/bin/fish"
|
# default_shell = "/bin/fish"
|
||||||
|
|
||||||
|
# Antal raders scrollback-historik per virtuell terminal (default: 1000).
|
||||||
|
# Scrolla med mushjulet i fönster vars app inte själv fångar musen;
|
||||||
|
# tangenttryck hoppar tillbaka till botten.
|
||||||
|
# scrollback_lines = 1000
|
||||||
|
|
||||||
|
# Visa scrollbar på terminalfönstrens högerkant när historik finns (default: true)
|
||||||
|
# show_scrollbar = true
|
||||||
|
|
||||||
|
# Standardstorlek [bredd, höjd] för nya terminalfönster (default: [82, 26])
|
||||||
|
# default_window_size = [82, 26]
|
||||||
|
|
||||||
[[panel]]
|
[[panel]]
|
||||||
position = "top"
|
position = "top"
|
||||||
file = "panels/topbar.toml"
|
file = "panels/topbar.toml"
|
||||||
|
|||||||
58
src/app.rs
58
src/app.rs
@@ -168,6 +168,9 @@ pub struct FloatingWindow {
|
|||||||
pub kitty_gfx_carry: Vec<u8>,
|
pub kitty_gfx_carry: Vec<u8>,
|
||||||
/// Köade Kitty graphics-kommandon att vidarebefordra till värdterminalen
|
/// Köade Kitty graphics-kommandon att vidarebefordra till värdterminalen
|
||||||
pub pending_graphics: Vec<PendingGraphic>,
|
pub pending_graphics: Vec<PendingGraphic>,
|
||||||
|
/// Totalt antal rader i scrollback-historiken (uppdateras i tick,
|
||||||
|
/// används av scrollbaren).
|
||||||
|
pub scrollback_total: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// En Kitty graphics-sekvens extraherad från PTY-data, redo att vidarebefordras.
|
/// En Kitty graphics-sekvens extraherad från PTY-data, redo att vidarebefordras.
|
||||||
@@ -572,6 +575,13 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Uppdatera scrollback-totalen för scrollbaren.
|
||||||
|
// set_scrollback klampar till historikens faktiska längd, så
|
||||||
|
// "hoppa till toppen och läs av" ger antalet historikrader.
|
||||||
|
let cur = parser.screen().scrollback();
|
||||||
|
parser.screen_mut().set_scrollback(usize::MAX);
|
||||||
|
window.scrollback_total = parser.screen().scrollback();
|
||||||
|
parser.screen_mut().set_scrollback(cur);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── PTY resize om fönsterstorlek ändrats ─────────────────────────────
|
// ── PTY resize om fönsterstorlek ändrats ─────────────────────────────
|
||||||
@@ -588,7 +598,7 @@ impl App {
|
|||||||
if cur_rows != new_rows || cur_cols != new_cols {
|
if cur_rows != new_rows || cur_cols != new_cols {
|
||||||
crate::log::log(&format!("PTY resize: {}x{} -> {}x{} (mouse mode was {:?})", cur_cols, cur_rows, new_cols, new_rows, window.mouse_mode));
|
crate::log::log(&format!("PTY resize: {}x{} -> {}x{} (mouse mode was {:?})", cur_cols, cur_rows, new_cols, new_rows, window.mouse_mode));
|
||||||
let _ = pty.resize(new_rows, new_cols);
|
let _ = pty.resize(new_rows, new_cols);
|
||||||
*parser = vt100::Parser::new(new_rows, new_cols, 0);
|
*parser = vt100::Parser::new(new_rows, new_cols, self.config.scrollback_lines);
|
||||||
// OBS: window.mouse_mode / window.mouse_encoding berörs inte — överlever restet.
|
// OBS: window.mouse_mode / window.mouse_encoding berörs inte — överlever restet.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -693,16 +703,10 @@ impl App {
|
|||||||
RunDialogResult::Execute(cmd) => {
|
RunDialogResult::Execute(cmd) => {
|
||||||
self.close_window(id);
|
self.close_window(id);
|
||||||
if !cmd.is_empty() {
|
if !cmd.is_empty() {
|
||||||
let shell = self.config.default_shell.clone()
|
// Kör kommandot direkt i fönstret (argument hanteras
|
||||||
.unwrap_or_else(default_shell);
|
// via sh -c i pty::build_command). Fönstret stängs
|
||||||
let win_id = self.spawn_terminal(&shell);
|
// när programmet avslutas.
|
||||||
// Skriv kommandot + Enter till den nya terminalens PTY
|
self.spawn_terminal(&cmd);
|
||||||
if let Some(w) = self.windows.iter_mut().find(|w| w.id == win_id) {
|
|
||||||
if let WindowContent::Terminal { pty, .. } = &mut w.content {
|
|
||||||
let input = format!("{}\n", cmd);
|
|
||||||
let _ = pty.write_input(input.as_bytes());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RunDialogResult::None => {}
|
RunDialogResult::None => {}
|
||||||
@@ -740,6 +744,8 @@ impl App {
|
|||||||
let app_cursor = parser.screen().application_cursor();
|
let app_cursor = parser.screen().application_cursor();
|
||||||
let app_keypad = parser.screen().application_keypad();
|
let app_keypad = parser.screen().application_keypad();
|
||||||
if let Some(bytes) = key_to_bytes(key, app_cursor, app_keypad) {
|
if let Some(bytes) = key_to_bytes(key, app_cursor, app_keypad) {
|
||||||
|
// Tangenttryck → hoppa tillbaka till botten av historiken
|
||||||
|
parser.screen_mut().set_scrollback(0);
|
||||||
let _ = pty.write_input(&bytes);
|
let _ = pty.write_input(&bytes);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -1213,12 +1219,22 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: piltangenter om ingen mus-tracking är aktiv
|
// Ingen mus-tracking i appen:
|
||||||
crate::log::log(&format!(" scroll fallback arrows up={} window={}", up, id));
|
// - alternativskärm (htop/less m.fl. utan mus): piltangenter
|
||||||
let bytes: &[u8] = if up { b"\x1b[A\x1b[A\x1b[A" } else { b"\x1b[B\x1b[B\x1b[B" };
|
// - primärskärm (vanligt skal): scrolla i vt100-historiken
|
||||||
if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
|
if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
|
||||||
if let WindowContent::Terminal { pty, .. } = &mut w.content {
|
if let WindowContent::Terminal { pty, parser, .. } = &mut w.content {
|
||||||
let _ = pty.write_input(bytes);
|
if parser.screen().alternate_screen() {
|
||||||
|
crate::log::log(&format!(" scroll -> pilar (altskärm) up={} window={}", up, id));
|
||||||
|
let bytes: &[u8] = if up { b"\x1b[A\x1b[A\x1b[A" } else { b"\x1b[B\x1b[B\x1b[B" };
|
||||||
|
let _ = pty.write_input(bytes);
|
||||||
|
} else {
|
||||||
|
let cur = parser.screen().scrollback();
|
||||||
|
let new_off = if up { cur.saturating_add(3) } else { cur.saturating_sub(3) };
|
||||||
|
crate::log::log(&format!(" scroll -> historik {} -> {} window={}", cur, new_off, id));
|
||||||
|
// set_scrollback klampar till historikens längd
|
||||||
|
parser.screen_mut().set_scrollback(new_off);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1300,6 +1316,7 @@ impl App {
|
|||||||
selection: None,
|
selection: None,
|
||||||
kitty_gfx_carry: Vec::new(),
|
kitty_gfx_carry: Vec::new(),
|
||||||
pending_graphics: Vec::new(),
|
pending_graphics: Vec::new(),
|
||||||
|
scrollback_total: 0,
|
||||||
});
|
});
|
||||||
self.focus_window(id);
|
self.focus_window(id);
|
||||||
}
|
}
|
||||||
@@ -1340,6 +1357,7 @@ impl App {
|
|||||||
selection: None,
|
selection: None,
|
||||||
kitty_gfx_carry: Vec::new(),
|
kitty_gfx_carry: Vec::new(),
|
||||||
pending_graphics: Vec::new(),
|
pending_graphics: Vec::new(),
|
||||||
|
scrollback_total: 0,
|
||||||
});
|
});
|
||||||
self.focus_window(id);
|
self.focus_window(id);
|
||||||
}
|
}
|
||||||
@@ -1445,8 +1463,9 @@ impl App {
|
|||||||
pub fn spawn_terminal(&mut self, shell: &str) -> usize {
|
pub fn spawn_terminal(&mut self, shell: &str) -> usize {
|
||||||
let ca = self.content_area;
|
let ca = self.content_area;
|
||||||
let offset = (self.windows.len() as i32) * 2;
|
let offset = (self.windows.len() as i32) * 2;
|
||||||
let w = 82u16.min(ca.width.saturating_sub(4));
|
let [def_w, def_h] = self.config.default_window_size;
|
||||||
let h = 26u16.min(ca.height.saturating_sub(4));
|
let w = def_w.max(8).min(ca.width.saturating_sub(4));
|
||||||
|
let h = def_h.max(4).min(ca.height.saturating_sub(4));
|
||||||
let x = ca.x as i32 + 2 + offset;
|
let x = ca.x as i32 + 2 + offset;
|
||||||
let y = ca.y as i32 + 1 + offset;
|
let y = ca.y as i32 + 1 + offset;
|
||||||
let rows = h.saturating_sub(2).max(1);
|
let rows = h.saturating_sub(2).max(1);
|
||||||
@@ -1462,7 +1481,7 @@ impl App {
|
|||||||
|
|
||||||
match PtyTerminal::spawn(shell, rows, cols, &env_vars) {
|
match PtyTerminal::spawn(shell, rows, cols, &env_vars) {
|
||||||
Ok((pty, rx)) => {
|
Ok((pty, rx)) => {
|
||||||
let parser = vt100::Parser::new(rows, cols, 0);
|
let parser = vt100::Parser::new(rows, cols, self.config.scrollback_lines);
|
||||||
self.next_id += 1;
|
self.next_id += 1;
|
||||||
let title = shell.split('/').next_back().unwrap_or(shell).to_string();
|
let title = shell.split('/').next_back().unwrap_or(shell).to_string();
|
||||||
self.windows.push(FloatingWindow {
|
self.windows.push(FloatingWindow {
|
||||||
@@ -1481,6 +1500,7 @@ impl App {
|
|||||||
selection: None,
|
selection: None,
|
||||||
kitty_gfx_carry: Vec::new(),
|
kitty_gfx_carry: Vec::new(),
|
||||||
pending_graphics: Vec::new(),
|
pending_graphics: Vec::new(),
|
||||||
|
scrollback_total: 0,
|
||||||
});
|
});
|
||||||
self.focus_window(id);
|
self.focus_window(id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,33 @@ pub struct Config {
|
|||||||
/// Standard-shell för nya terminaler. Om None → $SHELL eller /bin/bash.
|
/// Standard-shell för nya terminaler. Om None → $SHELL eller /bin/bash.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub default_shell: Option<String>,
|
pub default_shell: Option<String>,
|
||||||
|
/// Antal raders scrollback-historik per virtuell terminal.
|
||||||
|
#[serde(default = "default_scrollback_lines")]
|
||||||
|
pub scrollback_lines: usize,
|
||||||
|
/// Visa scrollbar på terminalfönstrens högerkant när historik finns.
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub show_scrollbar: bool,
|
||||||
|
/// Standardstorlek [bredd, höjd] för nya terminalfönster.
|
||||||
|
#[serde(default = "default_window_size")]
|
||||||
|
pub default_window_size: [u16; 2],
|
||||||
#[serde(default, rename = "panel")]
|
#[serde(default, rename = "panel")]
|
||||||
pub panels: Vec<PanelConfig>,
|
pub panels: Vec<PanelConfig>,
|
||||||
#[serde(default, rename = "keybind")]
|
#[serde(default, rename = "keybind")]
|
||||||
pub keybinds: Vec<KeybindConfig>,
|
pub keybinds: Vec<KeybindConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_scrollback_lines() -> usize {
|
||||||
|
1000
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_true() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_window_size() -> [u16; 2] {
|
||||||
|
[82, 26]
|
||||||
|
}
|
||||||
|
|
||||||
/// En tangentbords-genväg definierad i config
|
/// En tangentbords-genväg definierad i config
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct KeybindConfig {
|
pub struct KeybindConfig {
|
||||||
@@ -186,6 +207,9 @@ impl Config {
|
|||||||
background_image: None,
|
background_image: None,
|
||||||
terminal_bg_color: None,
|
terminal_bg_color: None,
|
||||||
default_shell: None,
|
default_shell: None,
|
||||||
|
scrollback_lines: default_scrollback_lines(),
|
||||||
|
show_scrollbar: default_true(),
|
||||||
|
default_window_size: default_window_size(),
|
||||||
panels: default_panels(),
|
panels: default_panels(),
|
||||||
keybinds: default_keybinds(),
|
keybinds: default_keybinds(),
|
||||||
}
|
}
|
||||||
|
|||||||
69
src/main.rs
69
src/main.rs
@@ -28,6 +28,7 @@ enum Mode {
|
|||||||
Standalone,
|
Standalone,
|
||||||
Daemon,
|
Daemon,
|
||||||
Client(String),
|
Client(String),
|
||||||
|
Help,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_args() -> Mode {
|
fn parse_args() -> Mode {
|
||||||
@@ -35,6 +36,7 @@ fn parse_args() -> Mode {
|
|||||||
let mut i = 1;
|
let mut i = 1;
|
||||||
while i < args.len() {
|
while i < args.len() {
|
||||||
match args[i].as_str() {
|
match args[i].as_str() {
|
||||||
|
"-h" | "--help" => return Mode::Help,
|
||||||
"-d" | "--daemon" => return Mode::Daemon,
|
"-d" | "--daemon" => return Mode::Daemon,
|
||||||
"-c" | "--connect" => {
|
"-c" | "--connect" => {
|
||||||
let path = args
|
let path = args
|
||||||
@@ -56,9 +58,58 @@ fn main() -> io::Result<()> {
|
|||||||
Mode::Standalone => run_standalone(),
|
Mode::Standalone => run_standalone(),
|
||||||
Mode::Daemon => run_daemon_mode(),
|
Mode::Daemon => run_daemon_mode(),
|
||||||
Mode::Client(path) => client::run(&path),
|
Mode::Client(path) => client::run(&path),
|
||||||
|
Mode::Help => {
|
||||||
|
print_help();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_help() {
|
||||||
|
println!(
|
||||||
|
"\
|
||||||
|
TUI-WM — fönsterhanterare i terminalen (tmux-liknande, med mus)
|
||||||
|
|
||||||
|
ANVÄNDNING:
|
||||||
|
tui-wm [FLAGGA]
|
||||||
|
|
||||||
|
FLAGGOR:
|
||||||
|
(ingen) Standalone: kör fönsterhanteraren i den här
|
||||||
|
terminalen och starta samtidigt socket-servern.
|
||||||
|
-d, --daemon Daemon: kör headless utan lokal rendering.
|
||||||
|
Anslut med `tui-wm -c`. Stoppas med `pkill tui-wm`
|
||||||
|
eller via systemd — INTE av att klienter kopplar ner.
|
||||||
|
-c, --connect [SOCKET]
|
||||||
|
Klient: anslut till en körande server (även över
|
||||||
|
SSH). Tangentbord, mus (klick/scroll/drag) och
|
||||||
|
resize vidarebefordras. Koppla ner med Ctrl+Shift+Q
|
||||||
|
— servern och dess program fortsätter köra.
|
||||||
|
-h, --help Visa den här hjälpen.
|
||||||
|
|
||||||
|
KONFIGURATION (./config.toml + panels/*.toml):
|
||||||
|
default_shell = \"/bin/bash\" # skal för nya terminaler
|
||||||
|
default_window_size = [82, 26] # [bredd, höjd] för nya fönster
|
||||||
|
scrollback_lines = 1000 # historikrader per terminal
|
||||||
|
show_scrollbar = true # scrollbar på fönstrens högerkant
|
||||||
|
background_image = \"bild.png\" # bakgrund (Kitty graphics)
|
||||||
|
terminal_bg_color = \"#1e1e2e\" # terminalbakgrund
|
||||||
|
|
||||||
|
TANGENTER (standard, ändras via [[keybind]] i config.toml):
|
||||||
|
ctrl+space Kommandodialog — skriv t.ex. `htop` och tryck Enter
|
||||||
|
alt+enter Ny terminal
|
||||||
|
ctrl+q Avsluta (från en ansluten klient: kopplar bara ner den)
|
||||||
|
|
||||||
|
MUS:
|
||||||
|
Dra titelraden = flytta fönster Dra kant/hörn = ändra storlek
|
||||||
|
Scroll = historik (skal) eller vidarebefordras (appar med mus-stöd)
|
||||||
|
Klick/hover vidarebefordras till appar som vim, htop, mc ...
|
||||||
|
|
||||||
|
SOCKET-API:
|
||||||
|
Program i fönstren får $TUI_WM_SOCKET och $TUI_WM_WINDOW_ID och kan
|
||||||
|
öppna fönster/popups via socketen — se SOCKET_API.md."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn run_standalone() -> io::Result<()> {
|
fn run_standalone() -> io::Result<()> {
|
||||||
log::init();
|
log::init();
|
||||||
let socket_path = ipc::default_socket_path();
|
let socket_path = ipc::default_socket_path();
|
||||||
@@ -400,9 +451,11 @@ fn handle_ipc_event(
|
|||||||
}
|
}
|
||||||
ClientMessage::KeyEvent { event } => {
|
ClientMessage::KeyEvent { event } => {
|
||||||
app.handle_event(crossterm::event::Event::Key(event));
|
app.handle_event(crossterm::event::Event::Key(event));
|
||||||
|
disconnect_instead_of_quit(app, all_clients, client_id);
|
||||||
}
|
}
|
||||||
ClientMessage::MouseEvent { event } => {
|
ClientMessage::MouseEvent { event } => {
|
||||||
app.handle_event(crossterm::event::Event::Mouse(event));
|
app.handle_event(crossterm::event::Event::Mouse(event));
|
||||||
|
disconnect_instead_of_quit(app, all_clients, client_id);
|
||||||
}
|
}
|
||||||
ClientMessage::Resize { width, height } => {
|
ClientMessage::Resize { width, height } => {
|
||||||
if let Some(client) = all_clients.get_mut(&client_id) {
|
if let Some(client) = all_clients.get_mut(&client_id) {
|
||||||
@@ -476,6 +529,22 @@ fn handle_ipc_event(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Exit utlöst av en ansluten klient (t.ex. ctrl+q eller panelens
|
||||||
|
/// Avsluta-knapp) ska bara koppla ner DEN klienten — servern/daemonen
|
||||||
|
/// och alla program fortsätter köra. Lokal Exit i standalone-läget
|
||||||
|
/// går inte via IPC och stänger som vanligt hela WM:et.
|
||||||
|
fn disconnect_instead_of_quit(
|
||||||
|
app: &mut App,
|
||||||
|
all_clients: &mut HashMap<usize, (ClientRole, u16, u16, mpsc::SyncSender<ServerMessage>)>,
|
||||||
|
client_id: usize,
|
||||||
|
) {
|
||||||
|
if app.should_quit {
|
||||||
|
app.should_quit = false;
|
||||||
|
// Sändaren droppas → klientens ström stängs → klienten avslutar.
|
||||||
|
all_clients.remove(&client_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn send_app_ipc_out(
|
fn send_app_ipc_out(
|
||||||
out_ev: AppIpcOut,
|
out_ev: AppIpcOut,
|
||||||
all_clients: &HashMap<usize, (ClientRole, u16, u16, mpsc::SyncSender<ServerMessage>)>,
|
all_clients: &HashMap<usize, (ClientRole, u16, u16, mpsc::SyncSender<ServerMessage>)>,
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ pub fn render(frame: &mut Frame, app: &App) {
|
|||||||
.hovered_resize
|
.hovered_resize
|
||||||
.filter(|(id, _)| *id == window.id)
|
.filter(|(id, _)| *id == window.id)
|
||||||
.map(|(_, e)| e);
|
.map(|(_, e)| e);
|
||||||
render_window(frame, window, app.focused_id, app.hovered_window_close, resize_hover, terminal_bg);
|
render_window(
|
||||||
|
frame, window, app.focused_id, app.hovered_window_close, resize_hover,
|
||||||
|
terminal_bg, bg_color, app.config.show_scrollbar,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paneler – alltid ovanpå fönster
|
// Paneler – alltid ovanpå fönster
|
||||||
@@ -186,6 +189,7 @@ fn render_dropdown(frame: &mut Frame, dd: &DropdownState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn render_window(
|
fn render_window(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
window: &FloatingWindow,
|
window: &FloatingWindow,
|
||||||
@@ -193,6 +197,8 @@ fn render_window(
|
|||||||
hovered_close: Option<usize>,
|
hovered_close: Option<usize>,
|
||||||
resize_hover: Option<ResizeEdge>,
|
resize_hover: Option<ResizeEdge>,
|
||||||
terminal_bg: Color,
|
terminal_bg: Color,
|
||||||
|
outer_bg: Color,
|
||||||
|
show_scrollbar: bool,
|
||||||
) {
|
) {
|
||||||
let full_rect = window.rect().intersection(frame.area());
|
let full_rect = window.rect().intersection(frame.area());
|
||||||
if full_rect.width < 4 || full_rect.height < 2 {
|
if full_rect.width < 4 || full_rect.height < 2 {
|
||||||
@@ -211,16 +217,28 @@ fn render_window(
|
|||||||
};
|
};
|
||||||
|
|
||||||
frame.render_widget(Clear, full_rect);
|
frame.render_widget(Clear, full_rect);
|
||||||
|
// Ramzonen målas i skrivbordets färg — annars fylls även hörncellerna
|
||||||
|
// utanför de rundade hörnen med fönstrets bakgrund och rutan ser
|
||||||
|
// fyrkantig ut med en "inre" rundad ram.
|
||||||
|
frame.render_widget(
|
||||||
|
Block::default().style(Style::default().bg(outer_bg)),
|
||||||
|
full_rect,
|
||||||
|
);
|
||||||
|
let content_bg = match &window.content {
|
||||||
|
WindowContent::Terminal { .. } => terminal_bg,
|
||||||
|
_ => Color::Black,
|
||||||
|
};
|
||||||
|
let inner_rect = full_rect.inner(ratatui::layout::Margin { vertical: 1, horizontal: 1 });
|
||||||
|
frame.render_widget(
|
||||||
|
Block::default().style(Style::default().bg(content_bg)),
|
||||||
|
inner_rect,
|
||||||
|
);
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Block::default()
|
Block::default()
|
||||||
.title(Span::styled(&title_text, theme::window_title()))
|
.title(Span::styled(&title_text, theme::window_title()))
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.border_style(border_style)
|
.border_style(border_style),
|
||||||
.style(Style::default().bg(match &window.content {
|
|
||||||
WindowContent::Terminal { .. } => terminal_bg,
|
|
||||||
_ => Color::Black,
|
|
||||||
})),
|
|
||||||
full_rect,
|
full_rect,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -267,6 +285,28 @@ fn render_window(
|
|||||||
render_popup_dialog(frame, window, focused_id);
|
render_popup_dialog(frame, window, focused_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scrollbar på högerkanten när terminalen har scrollback-historik
|
||||||
|
if show_scrollbar && window.scrollback_total > 0 {
|
||||||
|
if let WindowContent::Terminal { parser, alive: true, .. } = &window.content {
|
||||||
|
use ratatui::widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState};
|
||||||
|
let total = window.scrollback_total;
|
||||||
|
let offset = parser.screen().scrollback().min(total); // 0 = botten
|
||||||
|
let mut state = ScrollbarState::new(total).position(total - offset);
|
||||||
|
let sb = Scrollbar::new(ScrollbarOrientation::VerticalRight)
|
||||||
|
.begin_symbol(None)
|
||||||
|
.end_symbol(None)
|
||||||
|
.track_symbol(Some("│"))
|
||||||
|
.thumb_symbol("█")
|
||||||
|
.track_style(border_style)
|
||||||
|
.thumb_style(Style::default().fg(theme::ACCENT));
|
||||||
|
frame.render_stateful_widget(
|
||||||
|
sb,
|
||||||
|
full_rect.inner(ratatui::layout::Margin { vertical: 1, horizontal: 0 }),
|
||||||
|
&mut state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_popup_dialog(
|
fn render_popup_dialog(
|
||||||
@@ -276,9 +316,9 @@ fn render_popup_dialog(
|
|||||||
) {
|
) {
|
||||||
let is_focused = focused_id == Some(window.id);
|
let is_focused = focused_id == Some(window.id);
|
||||||
let border_style = if is_focused {
|
let border_style = if is_focused {
|
||||||
Style::default().fg(Color::Cyan)
|
Style::default().fg(theme::ACCENT)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(Color::Indexed(240))
|
Style::default().fg(theme::HINT)
|
||||||
};
|
};
|
||||||
|
|
||||||
let WindowContent::PopupDialog { message, buttons, selected, .. } = &window.content else {
|
let WindowContent::PopupDialog { message, buttons, selected, .. } = &window.content else {
|
||||||
@@ -290,9 +330,10 @@ fn render_popup_dialog(
|
|||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Block::default()
|
Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
.border_style(border_style)
|
.border_style(border_style)
|
||||||
.title(Span::styled(" TUI-WM ", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)))
|
.title(Span::styled(" TUI-WM ", theme::logo()))
|
||||||
.style(Style::default().bg(Color::Indexed(235))),
|
.style(Style::default().bg(theme::DESKTOP_BG)),
|
||||||
rect,
|
rect,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -59,10 +59,16 @@ class Conn:
|
|||||||
|
|
||||||
def recv(self):
|
def recv(self):
|
||||||
while len(self.buf) < 4:
|
while len(self.buf) < 4:
|
||||||
self.buf += self.sock.recv(65536)
|
data = self.sock.recv(65536)
|
||||||
|
if not data:
|
||||||
|
raise EOFError("servern stängde anslutningen")
|
||||||
|
self.buf += data
|
||||||
(length,) = struct.unpack(">I", self.buf[:4])
|
(length,) = struct.unpack(">I", self.buf[:4])
|
||||||
while len(self.buf) < 4 + length:
|
while len(self.buf) < 4 + length:
|
||||||
self.buf += self.sock.recv(65536)
|
data = self.sock.recv(65536)
|
||||||
|
if not data:
|
||||||
|
raise EOFError("servern stängde anslutningen")
|
||||||
|
self.buf += data
|
||||||
payload = self.buf[4 : 4 + length]
|
payload = self.buf[4 : 4 + length]
|
||||||
self.buf = self.buf[4 + length :]
|
self.buf = self.buf[4 + length :]
|
||||||
return json.loads(payload)
|
return json.loads(payload)
|
||||||
@@ -135,6 +141,23 @@ def main():
|
|||||||
frame = disp.recv_until("frame")
|
frame = disp.recv_until("frame")
|
||||||
check("frames strömmas till display", frame.get("width", 0) > 0)
|
check("frames strömmas till display", frame.get("width", 0) > 0)
|
||||||
|
|
||||||
|
# ── 1b. Exit från en klient kopplar bara ner klienten ───────────
|
||||||
|
disp2 = Conn(sock_path)
|
||||||
|
disp2.send({"type": "hello", "role": "display", "width": 80, "height": 24})
|
||||||
|
disp2.recv_until("hello_ok")
|
||||||
|
disp2.recv_until("frame")
|
||||||
|
disp2.send(key_event(ch="q", modifiers="CONTROL"))
|
||||||
|
try:
|
||||||
|
deadline = time.time() + 3
|
||||||
|
while time.time() < deadline:
|
||||||
|
disp2.recv()
|
||||||
|
disconnected = False
|
||||||
|
except (EOFError, TimeoutError, OSError):
|
||||||
|
disconnected = True
|
||||||
|
check("ctrl+q från klient kopplar ner klienten", disconnected)
|
||||||
|
frame = disp.recv_until("frame")
|
||||||
|
check("daemonen överlever klientens exit", frame.get("width", 0) > 0)
|
||||||
|
|
||||||
# ── 2. spawn_window med mus-tracking-testapp ────────────────────
|
# ── 2. spawn_window med mus-tracking-testapp ────────────────────
|
||||||
# printf slår på AnyMotion + SGR-mus-tracking, cat -v ekar allt
|
# printf slår på AnyMotion + SGR-mus-tracking, cat -v ekar allt
|
||||||
# PTY:n tar emot — inklusive forwardade mus-sekvenser.
|
# PTY:n tar emot — inklusive forwardade mus-sekvenser.
|
||||||
@@ -188,6 +211,46 @@ def main():
|
|||||||
f"frame-text: {text[-200:]!r}")
|
f"frame-text: {text[-200:]!r}")
|
||||||
app.send({"type": "close_window", "window_id": emoji_open["id"]})
|
app.send({"type": "close_window", "window_id": emoji_open["id"]})
|
||||||
|
|
||||||
|
# ── 4c. Scrollback: mushjul utan mus-tracking scrollar historiken ─
|
||||||
|
app.send({"type": "spawn_window",
|
||||||
|
"command": "seq 1 200; exec cat",
|
||||||
|
"request_id": "w-scroll"})
|
||||||
|
scroll_open = app.recv_until("window_opened")
|
||||||
|
text = disp.frame_text(timeout=5.0, contains="200")
|
||||||
|
app.send({"type": "list_windows", "request_id": "ls"})
|
||||||
|
wls = app.recv_until("window_list")
|
||||||
|
sw = next(w for w in wls["windows"] if w["id"] == scroll_open["id"])
|
||||||
|
scx, scy = sw["x"] + 10, sw["y"] + 5
|
||||||
|
visible_before = "163" in text
|
||||||
|
for _ in range(8):
|
||||||
|
disp.send(mouse_event("ScrollUp", scx, scy))
|
||||||
|
text = disp.frame_text(timeout=5.0, contains="163")
|
||||||
|
check("mushjul scrollar terminalens historik",
|
||||||
|
"163" in text and not visible_before,
|
||||||
|
f"före: {visible_before}, frame-tail: {text[-150:]!r}")
|
||||||
|
# Tangenttryck hoppar tillbaka till botten av historiken
|
||||||
|
disp.send(key_event(ch="x"))
|
||||||
|
text = disp.frame_text(timeout=5.0, contains="200")
|
||||||
|
check("tangenttryck hoppar till historikens botten", "200" in text)
|
||||||
|
app.send({"type": "close_window", "window_id": scroll_open["id"]})
|
||||||
|
|
||||||
|
# ── 4d. Kommandodialogen kör kommandot direkt ────────────────────
|
||||||
|
disp.send(key_event(ch=" ", modifiers="CONTROL")) # ctrl+space
|
||||||
|
time.sleep(0.3)
|
||||||
|
for ch in "top":
|
||||||
|
disp.send(key_event(ch=ch))
|
||||||
|
disp.send(key_event(code="Enter"))
|
||||||
|
text = disp.frame_text(timeout=8.0, contains="PID")
|
||||||
|
check("kommandodialogen kör programmet (top)", "PID" in text,
|
||||||
|
f"frame-tail: {text[-200:]!r}")
|
||||||
|
app.send({"type": "list_windows", "request_id": "lt"})
|
||||||
|
wlt = app.recv_until("window_list")
|
||||||
|
top_win = next((w for w in wlt["windows"] if w["title"].strip() == "top"), None)
|
||||||
|
check("fönstret har programmets namn som titel", top_win is not None,
|
||||||
|
f"fönster: {[w['title'] for w in wlt['windows']]}")
|
||||||
|
if top_win:
|
||||||
|
app.send({"type": "close_window", "window_id": top_win["id"]})
|
||||||
|
|
||||||
# ── 5. Popup + popup_result via Enter ───────────────────────────
|
# ── 5. Popup + popup_result via Enter ───────────────────────────
|
||||||
app.send({
|
app.send({
|
||||||
"type": "spawn_popup", "message": "Integrationstest?",
|
"type": "spawn_popup", "message": "Integrationstest?",
|
||||||
|
|||||||
Reference in New Issue
Block a user