Alt+Tab window switcher (MRU), snap/maximize, panel separators
- window_switcher action: overlay listing windows in most-recently-used order; Tab/arrows cycle, Enter confirms, Esc cancels. Bound to alt+tab with alt+w as fallback for terminals that swallow alt+tab - snap_left/snap_right actions (alt+left/alt+right) tile the focused window to half the content area; dragging a window to the screen edge snaps it too - maximize action (alt+up) toggles maximize/restore, double-click on the title bar does the same; restore returns the pre-snap geometry - Panel: separator between focus dot and buttons, separators between right-docked status widgets, and item widths now use display width (fixes rects for åäö/wide labels) - Integration suite: 23 checks, all passing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
33
config.toml
33
config.toml
@@ -44,6 +44,39 @@ 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).
|
||||||
|
# OBS: vissa terminaler/skrivbord fångar alt+tab själva — alt+w är reserv.
|
||||||
|
[[keybind]]
|
||||||
|
key = "alt+tab"
|
||||||
|
scope = "global"
|
||||||
|
label = "Fönsterväxlare"
|
||||||
|
action = { type = "window_switcher" }
|
||||||
|
|
||||||
|
[[keybind]]
|
||||||
|
key = "alt+w"
|
||||||
|
scope = "global"
|
||||||
|
action = { type = "window_switcher" }
|
||||||
|
|
||||||
|
# Fäst fokuserat fönster som vänster/höger halvskärm
|
||||||
|
[[keybind]]
|
||||||
|
key = "alt+left"
|
||||||
|
scope = "global"
|
||||||
|
label = "Fäst vänster"
|
||||||
|
action = { type = "snap_left" }
|
||||||
|
|
||||||
|
[[keybind]]
|
||||||
|
key = "alt+right"
|
||||||
|
scope = "global"
|
||||||
|
label = "Fäst höger"
|
||||||
|
action = { type = "snap_right" }
|
||||||
|
|
||||||
|
# Maximera/återställ fokuserat fönster (även dubbelklick på titelraden)
|
||||||
|
[[keybind]]
|
||||||
|
key = "alt+up"
|
||||||
|
scope = "global"
|
||||||
|
label = "Maximera/återställ"
|
||||||
|
action = { type = "maximize" }
|
||||||
|
|
||||||
# Avsluta TUI-WM när skrivbordet är fokuserat
|
# Avsluta TUI-WM när skrivbordet är fokuserat
|
||||||
[[keybind]]
|
[[keybind]]
|
||||||
key = "ctrl+q"
|
key = "ctrl+q"
|
||||||
|
|||||||
170
src/app.rs
170
src/app.rs
@@ -103,6 +103,12 @@ pub struct App {
|
|||||||
pub config: Config,
|
pub config: Config,
|
||||||
/// Aktivt tema (laddas från config.theme, hot-reloadas med config)
|
/// Aktivt tema (laddas från config.theme, hot-reloadas med config)
|
||||||
pub theme: Theme,
|
pub theme: Theme,
|
||||||
|
/// Fokushistorik, senast använda fönstret först (för Alt+Tab)
|
||||||
|
pub focus_history: Vec<usize>,
|
||||||
|
/// Öppen fönsterväxlare (Alt+Tab-overlay)
|
||||||
|
pub switcher: Option<SwitcherState>,
|
||||||
|
/// Senaste titelradsklick (fönster-id, tidpunkt) för dubbelklick
|
||||||
|
last_title_click: Option<(usize, std::time::Instant)>,
|
||||||
|
|
||||||
// Layout – uppdateras varje frame
|
// Layout – uppdateras varje frame
|
||||||
pub panel_rects: Vec<Rect>,
|
pub panel_rects: Vec<Rect>,
|
||||||
@@ -174,6 +180,16 @@ pub struct FloatingWindow {
|
|||||||
/// Totalt antal rader i scrollback-historiken (uppdateras i tick,
|
/// Totalt antal rader i scrollback-historiken (uppdateras i tick,
|
||||||
/// används av scrollbaren).
|
/// används av scrollbaren).
|
||||||
pub scrollback_total: usize,
|
pub scrollback_total: usize,
|
||||||
|
/// Geometri före snap/maximering — används för återställning.
|
||||||
|
pub prev_geom: Option<(i32, i32, u16, u16)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tillstånd för fönsterväxlaren (Alt+Tab).
|
||||||
|
pub struct SwitcherState {
|
||||||
|
/// Fönster-id i MRU-ordning (senast använda först)
|
||||||
|
pub order: Vec<usize>,
|
||||||
|
/// Markerat index i `order`
|
||||||
|
pub selected: 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.
|
||||||
@@ -464,6 +480,9 @@ impl App {
|
|||||||
parsed_keybinds,
|
parsed_keybinds,
|
||||||
tui_wm_btn_rects: Vec::new(),
|
tui_wm_btn_rects: Vec::new(),
|
||||||
theme,
|
theme,
|
||||||
|
focus_history: Vec::new(),
|
||||||
|
switcher: None,
|
||||||
|
last_title_click: None,
|
||||||
hovered_tui_btn: false,
|
hovered_tui_btn: false,
|
||||||
socket_path: None,
|
socket_path: None,
|
||||||
ipc_out: Vec::new(),
|
ipc_out: Vec::new(),
|
||||||
@@ -495,7 +514,7 @@ impl App {
|
|||||||
// Items börjar efter logo (9) + fokus-dot (1) + mellanrum (1) = x+12
|
// Items börjar efter logo (9) + fokus-dot (1) + mellanrum (1) = x+12
|
||||||
let mut x = rect.x + 12;
|
let mut x = rect.x + 12;
|
||||||
for item in &panel.items {
|
for item in &panel.items {
|
||||||
let w = item.label.len() as u16 + 2;
|
let w = unicode_width::UnicodeWidthStr::width(item.label.as_str()) as u16 + 2;
|
||||||
item_rects.push(Rect::new(x, rect.y, w, 1));
|
item_rects.push(Rect::new(x, rect.y, w, 1));
|
||||||
x += w + 1;
|
x += w + 1;
|
||||||
}
|
}
|
||||||
@@ -617,6 +636,8 @@ impl App {
|
|||||||
WindowContent::RunDialog { .. } => true,
|
WindowContent::RunDialog { .. } => true,
|
||||||
WindowContent::PopupDialog { .. } => true,
|
WindowContent::PopupDialog { .. } => true,
|
||||||
});
|
});
|
||||||
|
let alive_ids: Vec<usize> = self.windows.iter().map(|w| w.id).collect();
|
||||||
|
self.focus_history.retain(|h| alive_ids.contains(h));
|
||||||
if let Some(fid) = prev_focused {
|
if let Some(fid) = prev_focused {
|
||||||
if !self.windows.iter().any(|w| w.id == fid) {
|
if !self.windows.iter().any(|w| w.id == fid) {
|
||||||
self.focused_id = self.windows.last().map(|w| w.id);
|
self.focused_id = self.windows.last().map(|w| w.id);
|
||||||
@@ -685,6 +706,11 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_key(&mut self, key: KeyEvent) {
|
fn handle_key(&mut self, key: KeyEvent) {
|
||||||
|
// 0a. Fönsterväxlaren fångar alla tangenter medan den är öppen
|
||||||
|
if self.switcher.is_some() {
|
||||||
|
self.handle_switcher_key(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 0. Ctrl+Shift+C → kopiera markerad text
|
// 0. Ctrl+Shift+C → kopiera markerad text
|
||||||
if key.modifiers.contains(KeyModifiers::CONTROL | KeyModifiers::SHIFT)
|
if key.modifiers.contains(KeyModifiers::CONTROL | KeyModifiers::SHIFT)
|
||||||
&& key.code == KeyCode::Char('C')
|
&& key.code == KeyCode::Char('C')
|
||||||
@@ -968,10 +994,22 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
MouseEventKind::Up(btn) => {
|
MouseEventKind::Up(btn) => {
|
||||||
|
// Släpptes ett fönster-drag vid skärmkanten? → snap till halva
|
||||||
|
let was_dragging = self.windows.iter().find(|w| w.dragging.is_some()).map(|w| w.id);
|
||||||
for w in &mut self.windows {
|
for w in &mut self.windows {
|
||||||
w.dragging = None;
|
w.dragging = None;
|
||||||
w.resizing = None;
|
w.resizing = None;
|
||||||
}
|
}
|
||||||
|
if let Some(id) = was_dragging {
|
||||||
|
let ca = self.content_area;
|
||||||
|
if col <= ca.x {
|
||||||
|
self.focused_id = Some(id);
|
||||||
|
self.snap_focused(true);
|
||||||
|
} else if col >= ca.x + ca.width.saturating_sub(1) {
|
||||||
|
self.focused_id = Some(id);
|
||||||
|
self.snap_focused(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
self.forward_mouse_to_focused_terminal(MouseEventKind::Up(btn), col, row);
|
self.forward_mouse_to_focused_terminal(MouseEventKind::Up(btn), col, row);
|
||||||
}
|
}
|
||||||
MouseEventKind::Down(MouseButton::Left) => self.handle_click(col, row),
|
MouseEventKind::Down(MouseButton::Left) => self.handle_click(col, row),
|
||||||
@@ -1136,6 +1174,17 @@ impl App {
|
|||||||
.map(|w| (w.id, col as i32 - w.x, row as i32 - w.y));
|
.map(|w| (w.id, col as i32 - w.x, row as i32 - w.y));
|
||||||
if let Some((id, ox, oy)) = title_hit {
|
if let Some((id, ox, oy)) = title_hit {
|
||||||
self.focus_window(id);
|
self.focus_window(id);
|
||||||
|
// Dubbelklick på titelraden → maximera/återställ
|
||||||
|
let now = std::time::Instant::now();
|
||||||
|
let is_double = self
|
||||||
|
.last_title_click
|
||||||
|
.map(|(lid, t)| lid == id && now.duration_since(t).as_millis() < 400)
|
||||||
|
.unwrap_or(false);
|
||||||
|
self.last_title_click = Some((id, now));
|
||||||
|
if is_double {
|
||||||
|
self.toggle_maximize_focused();
|
||||||
|
return;
|
||||||
|
}
|
||||||
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) {
|
||||||
w.dragging = Some((ox, oy));
|
w.dragging = Some((ox, oy));
|
||||||
}
|
}
|
||||||
@@ -1294,6 +1343,10 @@ impl App {
|
|||||||
}
|
}
|
||||||
MenuAction::SpawnRunDialog => self.spawn_run_dialog(),
|
MenuAction::SpawnRunDialog => self.spawn_run_dialog(),
|
||||||
MenuAction::Submenu { .. } => {}
|
MenuAction::Submenu { .. } => {}
|
||||||
|
MenuAction::WindowSwitcher => self.switcher_open_or_advance(),
|
||||||
|
MenuAction::SnapLeft => self.snap_focused(true),
|
||||||
|
MenuAction::SnapRight => self.snap_focused(false),
|
||||||
|
MenuAction::Maximize => self.toggle_maximize_focused(),
|
||||||
MenuAction::NoOp => {}
|
MenuAction::NoOp => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1324,6 +1377,7 @@ impl App {
|
|||||||
kitty_gfx_carry: Vec::new(),
|
kitty_gfx_carry: Vec::new(),
|
||||||
pending_graphics: Vec::new(),
|
pending_graphics: Vec::new(),
|
||||||
scrollback_total: 0,
|
scrollback_total: 0,
|
||||||
|
prev_geom: None,
|
||||||
});
|
});
|
||||||
self.focus_window(id);
|
self.focus_window(id);
|
||||||
}
|
}
|
||||||
@@ -1365,6 +1419,7 @@ impl App {
|
|||||||
kitty_gfx_carry: Vec::new(),
|
kitty_gfx_carry: Vec::new(),
|
||||||
pending_graphics: Vec::new(),
|
pending_graphics: Vec::new(),
|
||||||
scrollback_total: 0,
|
scrollback_total: 0,
|
||||||
|
prev_geom: None,
|
||||||
});
|
});
|
||||||
self.focus_window(id);
|
self.focus_window(id);
|
||||||
}
|
}
|
||||||
@@ -1508,6 +1563,7 @@ impl App {
|
|||||||
kitty_gfx_carry: Vec::new(),
|
kitty_gfx_carry: Vec::new(),
|
||||||
pending_graphics: Vec::new(),
|
pending_graphics: Vec::new(),
|
||||||
scrollback_total: 0,
|
scrollback_total: 0,
|
||||||
|
prev_geom: None,
|
||||||
});
|
});
|
||||||
self.focus_window(id);
|
self.focus_window(id);
|
||||||
}
|
}
|
||||||
@@ -1516,6 +1572,111 @@ impl App {
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Öppna fönsterväxlaren, eller stega framåt om den redan är öppen.
|
||||||
|
fn switcher_open_or_advance(&mut self) {
|
||||||
|
if self.windows.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match &mut self.switcher {
|
||||||
|
Some(sw) => sw.selected = (sw.selected + 1) % sw.order.len(),
|
||||||
|
None => {
|
||||||
|
// MRU-ordning: historiken först, sedan övriga fönster
|
||||||
|
let mut order: Vec<usize> = self
|
||||||
|
.focus_history
|
||||||
|
.iter()
|
||||||
|
.filter(|id| self.windows.iter().any(|w| w.id == **id))
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
for w in self.windows.iter().rev() {
|
||||||
|
if !order.contains(&w.id) {
|
||||||
|
order.push(w.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let selected = if order.len() > 1 { 1 } else { 0 };
|
||||||
|
self.switcher = Some(SwitcherState { order, selected });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_switcher_key(&mut self, key: KeyEvent) {
|
||||||
|
let Some(sw) = &mut self.switcher else { return };
|
||||||
|
let len = sw.order.len().max(1);
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Tab | KeyCode::Down | KeyCode::Right => {
|
||||||
|
sw.selected = (sw.selected + 1) % len;
|
||||||
|
}
|
||||||
|
KeyCode::BackTab | KeyCode::Up | KeyCode::Left => {
|
||||||
|
sw.selected = (sw.selected + len - 1) % len;
|
||||||
|
}
|
||||||
|
KeyCode::Enter | KeyCode::Char(' ') => {
|
||||||
|
let id = sw.order[sw.selected];
|
||||||
|
self.switcher = None;
|
||||||
|
self.focus_window(id);
|
||||||
|
}
|
||||||
|
KeyCode::Esc => {
|
||||||
|
self.switcher = None;
|
||||||
|
}
|
||||||
|
// alt+tab / alt+w igen stegar vidare
|
||||||
|
KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::ALT) => {
|
||||||
|
sw.selected = (sw.selected + 1) % len;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fäst fokuserat fönster mot vänster/höger halva av innehållsytan.
|
||||||
|
fn snap_focused(&mut self, left: bool) {
|
||||||
|
let ca = self.content_area;
|
||||||
|
let Some(id) = self.focused_id else { return };
|
||||||
|
let half_w = ca.width / 2;
|
||||||
|
if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
|
||||||
|
if !w.resizable {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if w.prev_geom.is_none() {
|
||||||
|
w.prev_geom = Some((w.x, w.y, w.width, w.height));
|
||||||
|
}
|
||||||
|
w.x = if left { ca.x as i32 } else { (ca.x + half_w) as i32 };
|
||||||
|
w.y = ca.y as i32;
|
||||||
|
w.width = if left { half_w } else { ca.width - half_w };
|
||||||
|
w.height = ca.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maximera fokuserat fönster till hela innehållsytan, eller
|
||||||
|
/// återställ till geometrin det hade innan snap/maximering.
|
||||||
|
fn toggle_maximize_focused(&mut self) {
|
||||||
|
let ca = self.content_area;
|
||||||
|
let Some(id) = self.focused_id else { return };
|
||||||
|
if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
|
||||||
|
if !w.resizable {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let is_maximized = w.x == ca.x as i32
|
||||||
|
&& w.y == ca.y as i32
|
||||||
|
&& w.width == ca.width
|
||||||
|
&& w.height == ca.height;
|
||||||
|
if is_maximized {
|
||||||
|
// Återställ till geometrin före snap/maximering
|
||||||
|
if let Some((px, py, pw, ph)) = w.prev_geom.take() {
|
||||||
|
w.x = px;
|
||||||
|
w.y = py;
|
||||||
|
w.width = pw;
|
||||||
|
w.height = ph;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Behåll ev. redan sparad ursprungsgeometri (t.ex. från snap)
|
||||||
|
if w.prev_geom.is_none() {
|
||||||
|
w.prev_geom = Some((w.x, w.y, w.width, w.height));
|
||||||
|
}
|
||||||
|
w.x = ca.x as i32;
|
||||||
|
w.y = ca.y as i32;
|
||||||
|
w.width = ca.width;
|
||||||
|
w.height = ca.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn close_window(&mut self, id: usize) {
|
fn close_window(&mut self, id: usize) {
|
||||||
self.windows.retain(|w| w.id != id);
|
self.windows.retain(|w| w.id != id);
|
||||||
if self.focused_id == Some(id) {
|
if self.focused_id == Some(id) {
|
||||||
@@ -1529,6 +1690,9 @@ impl App {
|
|||||||
self.windows.push(w);
|
self.windows.push(w);
|
||||||
}
|
}
|
||||||
self.focused_id = Some(id);
|
self.focused_id = Some(id);
|
||||||
|
// MRU-historik för Alt+Tab
|
||||||
|
self.focus_history.retain(|&h| h != id);
|
||||||
|
self.focus_history.insert(0, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1878,6 +2042,10 @@ fn action_display(action: &MenuAction) -> &'static str {
|
|||||||
MenuAction::RunProgram { .. } => "Kör program",
|
MenuAction::RunProgram { .. } => "Kör program",
|
||||||
MenuAction::Submenu { .. } => "Undermeny",
|
MenuAction::Submenu { .. } => "Undermeny",
|
||||||
MenuAction::SpawnRunDialog => "Kommandodialog",
|
MenuAction::SpawnRunDialog => "Kommandodialog",
|
||||||
|
MenuAction::WindowSwitcher => "Fönsterväxlare",
|
||||||
|
MenuAction::SnapLeft => "Fäst vänster",
|
||||||
|
MenuAction::SnapRight => "Fäst höger",
|
||||||
|
MenuAction::Maximize => "Maximera/återställ",
|
||||||
MenuAction::NoOp => "",
|
MenuAction::NoOp => "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,14 @@ pub enum MenuAction {
|
|||||||
},
|
},
|
||||||
/// Öppnar Tui-run kommandodialog
|
/// Öppnar Tui-run kommandodialog
|
||||||
SpawnRunDialog,
|
SpawnRunDialog,
|
||||||
|
/// Öppnar fönsterväxlaren (Alt+Tab-overlay, MRU-ordning)
|
||||||
|
WindowSwitcher,
|
||||||
|
/// Fäst fokuserat fönster mot vänster halva
|
||||||
|
SnapLeft,
|
||||||
|
/// Fäst fokuserat fönster mot höger halva
|
||||||
|
SnapRight,
|
||||||
|
/// Maximera/återställ fokuserat fönster
|
||||||
|
Maximize,
|
||||||
/// Gör ingenting – används för display-only rader i menyer
|
/// Gör ingenting – används för display-only rader i menyer
|
||||||
NoOp,
|
NoOp,
|
||||||
}
|
}
|
||||||
@@ -160,6 +168,36 @@ fn default_keybinds() -> Vec<KeybindConfig> {
|
|||||||
action: MenuAction::SpawnTerminal { shell: None },
|
action: MenuAction::SpawnTerminal { shell: None },
|
||||||
label: Some("Ny terminal".to_string()),
|
label: Some("Ny terminal".to_string()),
|
||||||
},
|
},
|
||||||
|
KeybindConfig {
|
||||||
|
key: "alt+tab".to_string(),
|
||||||
|
scope: KeybindScope::Global,
|
||||||
|
action: MenuAction::WindowSwitcher,
|
||||||
|
label: Some("Fönsterväxlare".to_string()),
|
||||||
|
},
|
||||||
|
KeybindConfig {
|
||||||
|
key: "alt+w".to_string(),
|
||||||
|
scope: KeybindScope::Global,
|
||||||
|
action: MenuAction::WindowSwitcher,
|
||||||
|
label: None,
|
||||||
|
},
|
||||||
|
KeybindConfig {
|
||||||
|
key: "alt+left".to_string(),
|
||||||
|
scope: KeybindScope::Global,
|
||||||
|
action: MenuAction::SnapLeft,
|
||||||
|
label: Some("Fäst vänster".to_string()),
|
||||||
|
},
|
||||||
|
KeybindConfig {
|
||||||
|
key: "alt+right".to_string(),
|
||||||
|
scope: KeybindScope::Global,
|
||||||
|
action: MenuAction::SnapRight,
|
||||||
|
label: Some("Fäst höger".to_string()),
|
||||||
|
},
|
||||||
|
KeybindConfig {
|
||||||
|
key: "alt+up".to_string(),
|
||||||
|
scope: KeybindScope::Global,
|
||||||
|
action: MenuAction::Maximize,
|
||||||
|
label: Some("Maximera/återställ".to_string()),
|
||||||
|
},
|
||||||
KeybindConfig {
|
KeybindConfig {
|
||||||
key: "ctrl+q".to_string(),
|
key: "ctrl+q".to_string(),
|
||||||
scope: KeybindScope::Wm,
|
scope: KeybindScope::Wm,
|
||||||
|
|||||||
@@ -53,6 +53,74 @@ pub fn render(frame: &mut Frame, app: &App) {
|
|||||||
if let Some(dd) = &app.dropdown {
|
if let Some(dd) = &app.dropdown {
|
||||||
render_dropdown(frame, dd, &app.theme);
|
render_dropdown(frame, dd, &app.theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fönsterväxlaren (Alt+Tab) – ovanpå allt
|
||||||
|
if let Some(sw) = &app.switcher {
|
||||||
|
render_switcher(frame, app, sw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alt+Tab-overlay: fönsterlista i MRU-ordning, centrerad.
|
||||||
|
fn render_switcher(frame: &mut Frame, app: &App, sw: &crate::app::SwitcherState) {
|
||||||
|
let th = &app.theme;
|
||||||
|
let area = frame.area();
|
||||||
|
let titles: Vec<String> = sw
|
||||||
|
.order
|
||||||
|
.iter()
|
||||||
|
.map(|id| {
|
||||||
|
app.windows
|
||||||
|
.iter()
|
||||||
|
.find(|w| w.id == *id)
|
||||||
|
.map(|w| match &w.content {
|
||||||
|
WindowContent::Terminal { title, .. } => {
|
||||||
|
format!("{} ({}x{})", title, w.width, w.height)
|
||||||
|
}
|
||||||
|
WindowContent::RunDialog { .. } => "Kommandodialog".to_string(),
|
||||||
|
WindowContent::PopupDialog { .. } => "Popup".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| format!("fönster {}", id))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let inner_w = titles
|
||||||
|
.iter()
|
||||||
|
.map(|t| unicode_width::UnicodeWidthStr::width(t.as_str()))
|
||||||
|
.max()
|
||||||
|
.unwrap_or(10)
|
||||||
|
.clamp(20, area.width.saturating_sub(6) as usize) as u16
|
||||||
|
+ 4;
|
||||||
|
let inner_h = titles.len() as u16;
|
||||||
|
let w = inner_w + 2;
|
||||||
|
let h = inner_h + 2;
|
||||||
|
let x = area.x + area.width.saturating_sub(w) / 2;
|
||||||
|
let y = area.y + area.height.saturating_sub(h) / 2;
|
||||||
|
let rect = Rect::new(x, y, w, h).intersection(area);
|
||||||
|
|
||||||
|
frame.render_widget(Clear, rect);
|
||||||
|
frame.render_widget(
|
||||||
|
Block::default()
|
||||||
|
.title(Span::styled(" Fönster ", th.logo()))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(th.accent))
|
||||||
|
.style(Style::default().bg(th.menu_bg)),
|
||||||
|
rect,
|
||||||
|
);
|
||||||
|
for (i, title) in titles.iter().enumerate() {
|
||||||
|
let row = Rect::new(rect.x + 1, rect.y + 1 + i as u16, rect.width.saturating_sub(2), 1);
|
||||||
|
if row.y >= rect.y + rect.height.saturating_sub(1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let style = if i == sw.selected {
|
||||||
|
th.hover_highlight()
|
||||||
|
} else {
|
||||||
|
Style::default().fg(th.text).bg(th.menu_bg)
|
||||||
|
};
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(Span::styled(format!(" {} ", title), style)),
|
||||||
|
row,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@@ -93,6 +161,12 @@ fn render_panel(
|
|||||||
Rect::new(rect.x + 10, rect.y, 1, 1),
|
Rect::new(rect.x + 10, rect.y, 1, 1),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Separator mellan fokus-punkten och knapparna
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(Span::styled("│", Style::default().fg(th.hint).bg(th.panel_bg))),
|
||||||
|
Rect::new(rect.x + 11, rect.y, 1, 1).intersection(frame.area()),
|
||||||
|
);
|
||||||
|
|
||||||
// Vänsterjusterade knappar
|
// Vänsterjusterade knappar
|
||||||
for (ii, item) in items.iter().enumerate() {
|
for (ii, item) in items.iter().enumerate() {
|
||||||
let Some(&ir) = item_rects.get(ii) else { continue };
|
let Some(&ir) = item_rects.get(ii) else { continue };
|
||||||
@@ -111,10 +185,20 @@ fn render_panel(
|
|||||||
// Status-widgets
|
// Status-widgets
|
||||||
// Höger-dockade renderas från höger kant inåt
|
// Höger-dockade renderas från höger kant inåt
|
||||||
let mut right_x = rect.x + rect.width;
|
let mut right_x = rect.x + rect.width;
|
||||||
|
let mut first_right = true;
|
||||||
for (si, sw_cfg) in status_widgets.iter().enumerate().rev() {
|
for (si, sw_cfg) in status_widgets.iter().enumerate().rev() {
|
||||||
if sw_cfg.align != StatusAlign::Right {
|
if sw_cfg.align != StatusAlign::Right {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if !first_right {
|
||||||
|
// Separator mellan status-widgets
|
||||||
|
right_x = right_x.saturating_sub(2);
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(Span::styled(" │", Style::default().fg(th.hint).bg(th.panel_bg))),
|
||||||
|
Rect::new(right_x, rect.y, 2, 1).intersection(frame.area()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
first_right = false;
|
||||||
right_x = right_x.saturating_sub(sw_cfg.width);
|
right_x = right_x.saturating_sub(sw_cfg.width);
|
||||||
let text = status_states
|
let text = status_states
|
||||||
.get(si)
|
.get(si)
|
||||||
|
|||||||
@@ -274,6 +274,38 @@ def main():
|
|||||||
if top_win:
|
if top_win:
|
||||||
app.send({"type": "close_window", "window_id": top_win["id"]})
|
app.send({"type": "close_window", "window_id": top_win["id"]})
|
||||||
|
|
||||||
|
# ── 4e. Alt+Tab-växlare + snap/maximera ──────────────────────────
|
||||||
|
# Två fönster finns (mouse-test + top stängdes; spawna ett till)
|
||||||
|
app.send({"type": "spawn_window", "command": "exec cat",
|
||||||
|
"request_id": "w-sw"})
|
||||||
|
sw_open = app.recv_until("window_opened")
|
||||||
|
disp.send(key_event(code="Tab", modifiers="ALT"))
|
||||||
|
text = disp.frame_text(timeout=5.0, contains="Fönster")
|
||||||
|
check("alt+tab öppnar fönsterväxlaren", "Fönster" in text)
|
||||||
|
disp.send(key_event(code="Esc"))
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Snap vänster: fokuserat fönster ska bli halva bredden
|
||||||
|
disp.send(key_event(code="Left", modifiers="ALT"))
|
||||||
|
time.sleep(0.3)
|
||||||
|
app.send({"type": "list_windows", "request_id": "lsnap"})
|
||||||
|
wsn = app.recv_until("window_list")
|
||||||
|
snapped = next(w for w in wsn["windows"] if w["id"] == sw_open["id"])
|
||||||
|
# innehållsytan är 100 bred (minus panel högst upp) → halvan ~50
|
||||||
|
check("alt+left fäster fönstret som halvskärm",
|
||||||
|
45 <= snapped["width"] <= 55 and snapped["x"] <= 1,
|
||||||
|
f"geometri: {snapped}")
|
||||||
|
|
||||||
|
# Maximera/återställ: alt+up två gånger ska ge tillbaka snappad storlek
|
||||||
|
disp.send(key_event(code="Up", modifiers="ALT"))
|
||||||
|
time.sleep(0.3)
|
||||||
|
app.send({"type": "list_windows", "request_id": "lmax"})
|
||||||
|
wmx = app.recv_until("window_list")
|
||||||
|
maxed = next(w for w in wmx["windows"] if w["id"] == sw_open["id"])
|
||||||
|
check("alt+up maximerar fönstret", maxed["width"] >= 95,
|
||||||
|
f"geometri: {maxed}")
|
||||||
|
app.send({"type": "close_window", "window_id": sw_open["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