Add build output dependency file for mouse-test application
Add standard menu fore keybinds
This commit is contained in:
441
src/app.rs
441
src/app.rs
@@ -108,6 +108,10 @@ pub struct App {
|
||||
|
||||
// Parsade keybinds från config
|
||||
pub parsed_keybinds: Vec<ParsedKeybind>,
|
||||
|
||||
// TUI-WM logo-knapp rects (en per panel), för klick och hover
|
||||
pub tui_wm_btn_rects: Vec<Rect>,
|
||||
pub hovered_tui_btn: bool,
|
||||
}
|
||||
|
||||
pub struct DropdownState {
|
||||
@@ -116,6 +120,8 @@ pub struct DropdownState {
|
||||
pub rect: Rect,
|
||||
pub item_rects: Vec<Rect>,
|
||||
pub hovered: Option<usize>,
|
||||
/// Explicit ankar-X vid layout-uppdatering (används för TUI-WM-menyn)
|
||||
pub anchor_x: Option<u16>,
|
||||
}
|
||||
|
||||
pub struct FloatingWindow {
|
||||
@@ -129,6 +135,13 @@ pub struct FloatingWindow {
|
||||
pub resizing: Option<ResizeState>,
|
||||
/// Om false: inga resize-kanter, kan inte storleksändras
|
||||
pub resizable: bool,
|
||||
/// Mus-tracking-läge (PressRelease / AnyMotion osv.) begärt av appen.
|
||||
/// Lagras oberoende av vt100-parser så att det överlever parser-återskapning vid resize.
|
||||
pub mouse_mode: vt100::MouseProtocolMode,
|
||||
/// Mus-encoding begärd av appen (Default / Sgr / Utf8).
|
||||
pub mouse_encoding: vt100::MouseProtocolEncoding,
|
||||
/// Delvis mottagen escape-sekvens från PTY (för sekvenser som klippts tvärs genom en chunk-gräns).
|
||||
mouse_seq_carry: Vec<u8>,
|
||||
}
|
||||
|
||||
pub enum WindowContent {
|
||||
@@ -324,12 +337,15 @@ impl App {
|
||||
focused_id: None,
|
||||
status_states,
|
||||
parsed_keybinds,
|
||||
tui_wm_btn_rects: Vec::new(),
|
||||
hovered_tui_btn: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_layout(&mut self, area: Rect) {
|
||||
self.panel_rects.clear();
|
||||
self.panel_item_rects.clear();
|
||||
self.tui_wm_btn_rects.clear();
|
||||
|
||||
let mut top_used = 0u16;
|
||||
let mut bottom_used = 0u16;
|
||||
@@ -349,7 +365,8 @@ impl App {
|
||||
};
|
||||
|
||||
let mut item_rects = Vec::new();
|
||||
let mut x = rect.x + 10;
|
||||
// Items börjar efter logo (9) + fokus-dot (1) + mellanrum (1) = x+12
|
||||
let mut x = rect.x + 12;
|
||||
for item in &panel.items {
|
||||
let w = item.label.len() as u16 + 2;
|
||||
item_rects.push(Rect::new(x, rect.y, w, 1));
|
||||
@@ -358,6 +375,8 @@ impl App {
|
||||
|
||||
self.panel_rects.push(rect);
|
||||
self.panel_item_rects.push(item_rects);
|
||||
// Logo-knapp rect: " TUI-WM " (9 tecken bred) vid x+1
|
||||
self.tui_wm_btn_rects.push(Rect::new(rect.x + 1, rect.y, 9, 1));
|
||||
}
|
||||
|
||||
self.content_area = Rect::new(
|
||||
@@ -372,7 +391,9 @@ impl App {
|
||||
if let (Some(pr), Some(ir)) =
|
||||
(self.panel_rects.get(pi), self.panel_item_rects.get(pi))
|
||||
{
|
||||
let x = ir.first().map(|r| r.x).unwrap_or(pr.x);
|
||||
let x = dd.anchor_x
|
||||
.or_else(|| ir.first().map(|r| r.x))
|
||||
.unwrap_or(pr.x);
|
||||
let y = pr.y + 1;
|
||||
let width = dd
|
||||
.items
|
||||
@@ -382,7 +403,7 @@ impl App {
|
||||
.unwrap_or(12)
|
||||
.max(12);
|
||||
dd.item_rects = (0..dd.items.len())
|
||||
.map(|i| Rect::new(x, y + i as u16, width, 1))
|
||||
.map(|i| Rect::new(x, y + 1 + i as u16, width, 1))
|
||||
.collect();
|
||||
dd.rect = Rect::new(x, y, width, dd.items.len() as u16);
|
||||
}
|
||||
@@ -390,7 +411,7 @@ impl App {
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) {
|
||||
// ── PTY-data → vt100-parser ──────────────────────────────────────────
|
||||
// ── PTY-data → vt100-parser + mus-tracking-skanning ──────────────────────────────────────
|
||||
for window in &mut self.windows {
|
||||
let WindowContent::Terminal { rx, parser, alive, .. } = &mut window.content else { continue };
|
||||
if !*alive {
|
||||
@@ -398,7 +419,17 @@ impl App {
|
||||
}
|
||||
loop {
|
||||
match rx.try_recv() {
|
||||
Ok(data) => parser.process(&data),
|
||||
Ok(data) => {
|
||||
// Scanna för mus-escape-sekvenser FÖRE vi ger data till vt100-parsern.
|
||||
// På så sätt överlever mus-läget parser-återskapning vid resize.
|
||||
scan_mouse_tracking(
|
||||
&data,
|
||||
&mut window.mouse_mode,
|
||||
&mut window.mouse_encoding,
|
||||
&mut window.mouse_seq_carry,
|
||||
);
|
||||
parser.process(&data);
|
||||
}
|
||||
Err(mpsc::TryRecvError::Empty) => break,
|
||||
Err(mpsc::TryRecvError::Disconnected) => {
|
||||
*alive = false;
|
||||
@@ -418,8 +449,10 @@ impl App {
|
||||
}
|
||||
let (cur_rows, cur_cols) = parser.screen().size();
|
||||
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));
|
||||
let _ = pty.resize(new_rows, new_cols);
|
||||
*parser = vt100::Parser::new(new_rows, new_cols, 0);
|
||||
// OBS: window.mouse_mode / window.mouse_encoding berörs inte — överlever restet.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,16 +627,58 @@ impl App {
|
||||
}
|
||||
|
||||
fn handle_mouse(&mut self, col: u16, row: u16, kind: MouseEventKind) {
|
||||
// Logga alla mushändelser utom Moved (för mycker brus)
|
||||
if !matches!(kind, MouseEventKind::Moved) {
|
||||
crate::log::log(&format!("MOUSE {:?} col={} row={}", kind, col, row));
|
||||
}
|
||||
match kind {
|
||||
MouseEventKind::Moved => self.update_hover(col, row),
|
||||
MouseEventKind::Drag(MouseButton::Left) => self.handle_drag(col, row),
|
||||
MouseEventKind::Up(MouseButton::Left) => {
|
||||
MouseEventKind::Moved => {
|
||||
self.update_hover(col, row);
|
||||
self.forward_mouse_to_focused_terminal(kind, col, row);
|
||||
}
|
||||
MouseEventKind::Drag(MouseButton::Left) => {
|
||||
self.handle_drag(col, row);
|
||||
// Vidarebefordra drag-rörelse till terminal om vi inte håller på med WM-drag/resize
|
||||
let doing_wm = self.windows.iter().any(|w| w.dragging.is_some() || w.resizing.is_some());
|
||||
if !doing_wm {
|
||||
self.forward_mouse_to_focused_terminal(kind, col, row);
|
||||
}
|
||||
}
|
||||
MouseEventKind::Up(btn) => {
|
||||
for w in &mut self.windows {
|
||||
w.dragging = None;
|
||||
w.resizing = None;
|
||||
}
|
||||
self.forward_mouse_to_focused_terminal(MouseEventKind::Up(btn), col, row);
|
||||
}
|
||||
MouseEventKind::Down(MouseButton::Left) => self.handle_click(col, row),
|
||||
MouseEventKind::Down(btn) => {
|
||||
// Höger/mitten-klick: fokusera fönster och vidarebefordra till terminal
|
||||
let hit = self.windows.iter().rev().find(|w| w.in_content(col, row)).and_then(|w| {
|
||||
if let WindowContent::Terminal { alive, .. } = &w.content {
|
||||
if !*alive { return None; }
|
||||
Some((w.id, w.content_rect(), w.mouse_mode, w.mouse_encoding))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some((id, content_rect, mode, encoding)) = hit {
|
||||
crate::log::log(&format!(" -> right/middle hit window={} mode={:?} enc={:?} content_rect={:?}", id, mode, encoding, content_rect));
|
||||
self.focus_window(id);
|
||||
if let Some(bytes) = encode_mouse_event(MouseEventKind::Down(btn), col, row, content_rect, mode, encoding) {
|
||||
crate::log::log(&format!(" -> sending {} bytes: {:?}", bytes.len(), bytes));
|
||||
if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
|
||||
if let WindowContent::Terminal { pty, .. } = &mut w.content {
|
||||
let _ = pty.write_input(&bytes);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
crate::log::log(" -> encode_mouse_event returned None (mode=None or filtered)");
|
||||
}
|
||||
} else {
|
||||
crate::log::log(" -> no terminal window hit");
|
||||
}
|
||||
}
|
||||
MouseEventKind::ScrollUp => self.handle_scroll(col, row, true),
|
||||
MouseEventKind::ScrollDown => self.handle_scroll(col, row, false),
|
||||
_ => {}
|
||||
@@ -614,6 +689,7 @@ impl App {
|
||||
self.hovered_panel_item = None;
|
||||
self.hovered_window_close = None;
|
||||
self.hovered_resize = None;
|
||||
self.hovered_tui_btn = false;
|
||||
|
||||
// Dropdown-hover
|
||||
if let Some(dd) = &mut self.dropdown {
|
||||
@@ -654,6 +730,14 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TUI-WM logo-knapp
|
||||
for &r in &self.tui_wm_btn_rects {
|
||||
if in_rect(col, row, r) {
|
||||
self.hovered_tui_btn = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_drag(&mut self, col: u16, row: u16) {
|
||||
@@ -735,14 +819,42 @@ impl App {
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Innehållsyta → fokus
|
||||
let content_id = self.windows.iter().rev().find(|w| w.in_content(col, row)).map(|w| w.id);
|
||||
if let Some(id) = content_id {
|
||||
// 5. Innehållsyta → fokus + vidarebefordra musklick till terminal
|
||||
let hit = self.windows.iter().rev().find(|w| w.in_content(col, row)).and_then(|w| {
|
||||
let (mode, encoding) = match &w.content {
|
||||
WindowContent::Terminal { alive, .. } if *alive => (w.mouse_mode, w.mouse_encoding),
|
||||
_ => (vt100::MouseProtocolMode::None, vt100::MouseProtocolEncoding::Default),
|
||||
};
|
||||
Some((w.id, w.content_rect(), mode, encoding))
|
||||
});
|
||||
if let Some((id, content_rect, mode, encoding)) = hit {
|
||||
crate::log::log(&format!(" click step5: window={} mode={:?} enc={:?} col={} row={} content_rect={:?}", id, mode, encoding, col, row, content_rect));
|
||||
self.focus_window(id);
|
||||
if let Some(bytes) = encode_mouse_event(
|
||||
MouseEventKind::Down(MouseButton::Left), col, row, content_rect, mode, encoding,
|
||||
) {
|
||||
crate::log::log(&format!(" -> sending {} bytes: {:?}", bytes.len(), bytes));
|
||||
if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
|
||||
if let WindowContent::Terminal { pty, .. } = &mut w.content {
|
||||
let _ = pty.write_input(&bytes);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
crate::log::log(" -> encode_mouse_event returned None");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 6. Panel-knappar
|
||||
// 6. TUI-WM logo → keybind-meny
|
||||
for pi in 0..self.tui_wm_btn_rects.len() {
|
||||
let r = self.tui_wm_btn_rects[pi];
|
||||
if in_rect(col, row, r) {
|
||||
self.open_tui_wm_dropdown(pi);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Panel-knappar
|
||||
for (pi, item_rects) in self.panel_item_rects.clone().iter().enumerate() {
|
||||
for (ii, r) in item_rects.iter().enumerate() {
|
||||
if in_rect(col, row, *r) {
|
||||
@@ -756,11 +868,43 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Inget träffat → skrivbordet fokuseras
|
||||
self.focused_id = None;
|
||||
}
|
||||
|
||||
fn handle_scroll(&mut self, col: u16, row: u16, up: bool) {
|
||||
let id = self.windows.iter().rev().find(|w| in_rect(col, row, w.rect())).map(|w| w.id);
|
||||
if let Some(id) = id {
|
||||
// Hämta mus-tracking-info utan mutable lån
|
||||
let info = self.windows.iter().find(|w| w.id == id).and_then(|w| {
|
||||
if let WindowContent::Terminal { alive, .. } = &w.content {
|
||||
if !*alive { return None; }
|
||||
if w.mouse_mode == vt100::MouseProtocolMode::None { return None; }
|
||||
Some((w.content_rect(), w.mouse_mode, w.mouse_encoding))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((content_rect, mode, encoding)) = info {
|
||||
let kind = if up { MouseEventKind::ScrollUp } else { MouseEventKind::ScrollDown };
|
||||
crate::log::log(&format!(" scroll up={} window={} mode={:?} enc={:?}", up, id, mode, encoding));
|
||||
if let Some(bytes) = encode_mouse_event(kind, col, row, content_rect, mode, encoding) {
|
||||
crate::log::log(&format!(" -> scroll sending {} bytes: {:?}", bytes.len(), bytes));
|
||||
if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
|
||||
if let WindowContent::Terminal { pty, .. } = &mut w.content {
|
||||
let _ = pty.write_input(&bytes);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
crate::log::log(" -> scroll encode returned None");
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: piltangenter om ingen mus-tracking är aktiv
|
||||
crate::log::log(&format!(" scroll fallback arrows up={} window={}", up, id));
|
||||
let bytes: &[u8] = if up { b"\x1b[A\x1b[A\x1b[A" } else { b"\x1b[B\x1b[B\x1b[B" };
|
||||
if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
|
||||
if let WindowContent::Terminal { pty, .. } = &mut w.content {
|
||||
@@ -770,6 +914,33 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Vidarebefordrar ett mushändelse till det fokuserade terminalfönstret
|
||||
/// om musen befinner sig i fönstrets innehållsyta och mus-tracking är aktivt.
|
||||
fn forward_mouse_to_focused_terminal(&mut self, kind: MouseEventKind, col: u16, row: u16) {
|
||||
let Some(id) = self.focused_id else { return };
|
||||
|
||||
let info = self.windows.iter().find(|w| w.id == id).and_then(|w| {
|
||||
if !w.in_content(col, row) { return None; }
|
||||
if let WindowContent::Terminal { alive, .. } = &w.content {
|
||||
if !*alive { return None; }
|
||||
Some((w.content_rect(), w.mouse_mode, w.mouse_encoding))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((content_rect, mode, encoding)) = info {
|
||||
if let Some(bytes) = encode_mouse_event(kind, col, row, content_rect, mode, encoding) {
|
||||
crate::log::log(&format!(" forward {:?} -> window={} {} bytes: {:?}", kind, id, bytes.len(), bytes));
|
||||
if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
|
||||
if let WindowContent::Terminal { pty, .. } = &mut w.content {
|
||||
let _ = pty.write_input(&bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_action(&mut self, action: MenuAction) {
|
||||
match action {
|
||||
MenuAction::Exit => self.should_quit = true,
|
||||
@@ -781,6 +952,7 @@ impl App {
|
||||
MenuAction::RunProgram { command, .. } => self.spawn_terminal(&command),
|
||||
MenuAction::SpawnRunDialog => self.spawn_run_dialog(),
|
||||
MenuAction::Submenu { .. } => {}
|
||||
MenuAction::NoOp => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,6 +975,9 @@ impl App {
|
||||
dragging: None,
|
||||
resizing: None,
|
||||
resizable: false,
|
||||
mouse_mode: vt100::MouseProtocolMode::None,
|
||||
mouse_encoding: vt100::MouseProtocolEncoding::Default,
|
||||
mouse_seq_carry: Vec::new(),
|
||||
});
|
||||
self.focus_window(id);
|
||||
}
|
||||
@@ -821,8 +996,9 @@ impl App {
|
||||
let x = ir.x;
|
||||
let y = pr.y + 1;
|
||||
|
||||
// item_rects peker på den faktiska synliga raden (y+1 = innanför övre kant)
|
||||
let item_rects: Vec<Rect> =
|
||||
(0..items.len()).map(|i| Rect::new(x, y + i as u16, width, 1)).collect();
|
||||
(0..items.len()).map(|i| Rect::new(x, y + 1 + i as u16, width, 1)).collect();
|
||||
|
||||
self.dropdown = Some(DropdownState {
|
||||
panel_idx,
|
||||
@@ -830,9 +1006,51 @@ impl App {
|
||||
item_rects,
|
||||
items,
|
||||
hovered: None,
|
||||
anchor_x: None,
|
||||
});
|
||||
}
|
||||
|
||||
/// Öppnar en dropdown med alla keybinds, förankrad vid TUI-WM-logotypen.
|
||||
fn open_tui_wm_dropdown(&mut self, panel_idx: usize) {
|
||||
let pr = match self.panel_rects.get(panel_idx) {
|
||||
Some(r) => *r,
|
||||
None => return,
|
||||
};
|
||||
let btn_r = self.tui_wm_btn_rects.get(panel_idx).copied().unwrap_or(pr);
|
||||
let items = self.build_keybind_menu_items();
|
||||
if items.is_empty() {
|
||||
return;
|
||||
}
|
||||
let width = items.iter().map(|i| i.label.len() as u16 + 2).max().unwrap_or(20).max(20);
|
||||
let x = btn_r.x;
|
||||
let y = pr.y + 1;
|
||||
// item_rects peker på den faktiska synliga raden (y+1 = innanför övre kant)
|
||||
let item_rects: Vec<Rect> =
|
||||
(0..items.len()).map(|i| Rect::new(x, y + 1 + i as u16, width, 1)).collect();
|
||||
self.dropdown = Some(DropdownState {
|
||||
panel_idx,
|
||||
rect: Rect::new(x, y, width, items.len() as u16),
|
||||
item_rects,
|
||||
items,
|
||||
hovered: None,
|
||||
anchor_x: Some(x),
|
||||
});
|
||||
}
|
||||
|
||||
/// Bygger meny-items från config.keybinds för visning i TUI-WM-menyn.
|
||||
/// Varje item kör keybindens faktiska action vid klick.
|
||||
fn build_keybind_menu_items(&self) -> Vec<MenuItem> {
|
||||
use crate::config::KeybindScope;
|
||||
self.config.keybinds.iter().map(|kb| {
|
||||
let desc = kb.label.as_deref().unwrap_or_else(|| action_display(&kb.action));
|
||||
let scope = if kb.scope == KeybindScope::Global { " [global]" } else { "" };
|
||||
MenuItem {
|
||||
label: format!("{:<14} {}{}", kb.key, desc, scope),
|
||||
action: kb.action.clone(),
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub fn spawn_terminal(&mut self, shell: &str) {
|
||||
let ca = self.content_area;
|
||||
let offset = (self.windows.len() as i32) * 2;
|
||||
@@ -859,6 +1077,9 @@ impl App {
|
||||
dragging: None,
|
||||
resizing: None,
|
||||
resizable: true,
|
||||
mouse_mode: vt100::MouseProtocolMode::None,
|
||||
mouse_encoding: vt100::MouseProtocolEncoding::Default,
|
||||
mouse_seq_carry: Vec::new(),
|
||||
});
|
||||
self.focus_window(id);
|
||||
}
|
||||
@@ -884,6 +1105,187 @@ impl App {
|
||||
|
||||
// ─── Hjälpfunktioner ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Kodar ett crossterm-mushändelse till ANSI-byte-sekvens enligt terminalen
|
||||
/// begärda mus-tracking-protokoll. Returnerar None om protokollet är None eller
|
||||
/// om händelsetypen inte täcks av det aktiva protokollet.
|
||||
fn encode_mouse_event(
|
||||
kind: MouseEventKind,
|
||||
col: u16,
|
||||
row: u16,
|
||||
content_rect: Rect,
|
||||
mode: vt100::MouseProtocolMode,
|
||||
encoding: vt100::MouseProtocolEncoding,
|
||||
) -> Option<Vec<u8>> {
|
||||
if mode == vt100::MouseProtocolMode::None {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 1-baserade terminalkoordinater relativt innehållsytan
|
||||
let term_col = col.saturating_sub(content_rect.x) + 1;
|
||||
let term_row = row.saturating_sub(content_rect.y) + 1;
|
||||
|
||||
let (button, is_release): (u8, bool) = match kind {
|
||||
MouseEventKind::Down(MouseButton::Left) => (0, false),
|
||||
MouseEventKind::Down(MouseButton::Middle) => (1, false),
|
||||
MouseEventKind::Down(MouseButton::Right) => (2, false),
|
||||
MouseEventKind::Up(_) => (3, true),
|
||||
MouseEventKind::Drag(MouseButton::Left) => (32, false),
|
||||
MouseEventKind::Drag(MouseButton::Middle) => (33, false),
|
||||
MouseEventKind::Drag(MouseButton::Right) => (34, false),
|
||||
MouseEventKind::Moved => (35, false),
|
||||
MouseEventKind::ScrollUp => (64, false),
|
||||
MouseEventKind::ScrollDown => (65, false),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// Filtrera händelsetyp mot aktivt protokoll
|
||||
match mode {
|
||||
vt100::MouseProtocolMode::None => return None,
|
||||
vt100::MouseProtocolMode::Press => {
|
||||
if !matches!(kind,
|
||||
MouseEventKind::Down(_) |
|
||||
MouseEventKind::ScrollUp |
|
||||
MouseEventKind::ScrollDown
|
||||
) { return None; }
|
||||
}
|
||||
vt100::MouseProtocolMode::PressRelease => {
|
||||
if !matches!(kind,
|
||||
MouseEventKind::Down(_) |
|
||||
MouseEventKind::Up(_) |
|
||||
MouseEventKind::ScrollUp |
|
||||
MouseEventKind::ScrollDown
|
||||
) { return None; }
|
||||
}
|
||||
vt100::MouseProtocolMode::ButtonMotion => {
|
||||
if !matches!(kind,
|
||||
MouseEventKind::Down(_) |
|
||||
MouseEventKind::Up(_) |
|
||||
MouseEventKind::Drag(_) |
|
||||
MouseEventKind::ScrollUp |
|
||||
MouseEventKind::ScrollDown
|
||||
) { return None; }
|
||||
}
|
||||
vt100::MouseProtocolMode::AnyMotion => {} // alla händelsetyper passerar
|
||||
}
|
||||
|
||||
Some(match encoding {
|
||||
vt100::MouseProtocolEncoding::Default => {
|
||||
if term_col > 223 || term_row > 223 { return None; }
|
||||
vec![0x1b, b'[', b'M', button + 32, term_col as u8 + 32, term_row as u8 + 32]
|
||||
}
|
||||
vt100::MouseProtocolEncoding::Sgr => {
|
||||
let suffix = if is_release { 'm' } else { 'M' };
|
||||
format!("\x1b[<{};{};{}{}", button, term_col, term_row, suffix).into_bytes()
|
||||
}
|
||||
vt100::MouseProtocolEncoding::Utf8 => {
|
||||
let mut bytes = vec![0x1b, b'[', b'M', button + 32];
|
||||
for coord in [term_col, term_row] {
|
||||
let code_point = coord as u32 + 32;
|
||||
if let Some(c) = char::from_u32(code_point) {
|
||||
let mut buf = [0u8; 4];
|
||||
bytes.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
bytes
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Skannar ett chunk av r\u00e5 PTY-data efter DEC private mode escape-sekvenser som
|
||||
/// styr mus-tracking (\x1b[?<n>h / \x1b[?<n>l) och uppdaterar `mode` och `encoding`.
|
||||
/// En liten "carry"-buffer anv\u00e4nds f\u00f6r att hantera sekvenser som klippts mitt i
|
||||
/// en chunk-gr\u00e4ns.\n///
|
||||
/// Hanterar b\u00e5de enkla sekvenser (\x1b[?1003h) och kombinerade sekvenser (\x1b[?1003;1006h).
|
||||
fn scan_mouse_tracking(
|
||||
data: &[u8],
|
||||
mode: &mut vt100::MouseProtocolMode,
|
||||
encoding: &mut vt100::MouseProtocolEncoding,
|
||||
carry: &mut Vec<u8>,
|
||||
) {
|
||||
// Kombinera carry med ny data
|
||||
let mut buf = std::mem::take(carry);
|
||||
buf.extend_from_slice(data);
|
||||
|
||||
let mut i = 0;
|
||||
while i < buf.len() {
|
||||
if buf[i] != 0x1b {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
// Kolla om vi har ESC [ ?
|
||||
if i + 2 < buf.len() && buf[i + 1] == b'[' && buf[i + 2] == b'?' {
|
||||
let num_start = i + 3;
|
||||
let mut j = num_start;
|
||||
// Scanna siffror och semikolon tills h eller l (eller slut p\u00e5 buffer)
|
||||
while j < buf.len() && (buf[j].is_ascii_digit() || buf[j] == b';') {
|
||||
j += 1;
|
||||
}
|
||||
if j >= buf.len() {
|
||||
// Ofullst\u00e4ndig sekvens \u2014 spara resten som carry
|
||||
*carry = buf[i..].to_vec();
|
||||
return;
|
||||
}
|
||||
if buf[j] == b'h' || buf[j] == b'l' {
|
||||
let enable = buf[j] == b'h';
|
||||
if let Ok(s) = std::str::from_utf8(&buf[num_start..j]) {
|
||||
let old_mode = *mode;
|
||||
let old_enc = *encoding;
|
||||
for part in s.split(';') {
|
||||
if let Ok(n) = part.trim().parse::<u16>() {
|
||||
apply_mouse_dec_mode(n, enable, mode, encoding);
|
||||
}
|
||||
}
|
||||
if *mode != old_mode || *encoding != old_enc {
|
||||
crate::log::log(&format!(
|
||||
"PTY mus-lage uppdaterat: mode={:?} enc={:?} via ESC[?{}{}",
|
||||
mode, encoding, s, if enable { 'h' } else { 'l' }
|
||||
));
|
||||
}
|
||||
}
|
||||
i = j + 1;
|
||||
} else {
|
||||
// Inte h/l \u2014 bara en ESC vi inte k\u00e4nner
|
||||
i += 1;
|
||||
}
|
||||
} else if i + 1 >= buf.len() {
|
||||
// M\u00f6jlig ofullst\u00e4ndig ESC sekvens vid bufferkanten
|
||||
*carry = buf[i..].to_vec();
|
||||
return;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Till\u00e4mpar en DEC private mode-\u00e4ndring p\u00e5 det laggrade mus-l\u00e4get.
|
||||
fn apply_mouse_dec_mode(
|
||||
n: u16,
|
||||
enable: bool,
|
||||
mode: &mut vt100::MouseProtocolMode,
|
||||
encoding: &mut vt100::MouseProtocolEncoding,
|
||||
) {
|
||||
if enable {
|
||||
match n {
|
||||
9 => *mode = vt100::MouseProtocolMode::Press, // X10
|
||||
1000 => *mode = vt100::MouseProtocolMode::PressRelease, // VT200
|
||||
1002 => *mode = vt100::MouseProtocolMode::ButtonMotion,
|
||||
1003 => *mode = vt100::MouseProtocolMode::AnyMotion,
|
||||
1005 => *encoding = vt100::MouseProtocolEncoding::Utf8,
|
||||
1006 => *encoding = vt100::MouseProtocolEncoding::Sgr,
|
||||
1015 => {} // URXVT \u2014 behandla som Default (ignorera)
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
match n {
|
||||
9 | 1000 | 1002 | 1003 => *mode = vt100::MouseProtocolMode::None,
|
||||
1005 | 1006 => *encoding = vt100::MouseProtocolEncoding::Default,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn in_rect(col: u16, row: u16, rect: Rect) -> bool {
|
||||
col >= rect.x
|
||||
&& col < rect.x + rect.width
|
||||
@@ -929,6 +1331,19 @@ fn run_command(command: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returnerar ett kortfattat namn för en MenuAction (används som fallback i keybind-menyn).
|
||||
fn action_display(action: &MenuAction) -> &'static str {
|
||||
match action {
|
||||
MenuAction::Exit => "Avsluta",
|
||||
MenuAction::SpawnTerminal { .. } => "Ny terminal",
|
||||
MenuAction::RunScript { .. } => "Kör skript",
|
||||
MenuAction::RunProgram { .. } => "Kör program",
|
||||
MenuAction::Submenu { .. } => "Undermeny",
|
||||
MenuAction::SpawnRunDialog => "Kommandodialog",
|
||||
MenuAction::NoOp => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn key_to_bytes(key: KeyEvent) -> Option<Vec<u8>> {
|
||||
use KeyCode::*;
|
||||
let bytes: Vec<u8> = match key.code {
|
||||
|
||||
@@ -18,6 +18,9 @@ pub struct KeybindConfig {
|
||||
#[serde(default)]
|
||||
pub scope: KeybindScope,
|
||||
pub action: MenuAction,
|
||||
/// Visningsnamn som visas i TUI-WM-menyn (valfritt)
|
||||
#[serde(default)]
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default, PartialEq)]
|
||||
@@ -77,6 +80,8 @@ pub enum MenuAction {
|
||||
},
|
||||
/// Öppnar Tui-run kommandodialog
|
||||
SpawnRunDialog,
|
||||
/// Gör ingenting – används för display-only rader i menyer
|
||||
NoOp,
|
||||
}
|
||||
|
||||
/// En status-widget i panelen: kör ett kommando periodiskt och renderar output
|
||||
@@ -106,6 +111,29 @@ fn default_interval() -> u64 {
|
||||
10
|
||||
}
|
||||
|
||||
fn default_keybinds() -> Vec<KeybindConfig> {
|
||||
vec![
|
||||
KeybindConfig {
|
||||
key: "ctrl+space".to_string(),
|
||||
scope: KeybindScope::Global,
|
||||
action: MenuAction::SpawnRunDialog,
|
||||
label: Some("Öppna kommandodialog".to_string()),
|
||||
},
|
||||
KeybindConfig {
|
||||
key: "alt+enter".to_string(),
|
||||
scope: KeybindScope::Global,
|
||||
action: MenuAction::SpawnTerminal { shell: None },
|
||||
label: Some("Ny terminal".to_string()),
|
||||
},
|
||||
KeybindConfig {
|
||||
key: "ctrl+q".to_string(),
|
||||
scope: KeybindScope::Wm,
|
||||
action: MenuAction::Exit,
|
||||
label: Some("Avsluta TUI-WM".to_string()),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load(path: &str) -> anyhow::Result<Config> {
|
||||
let content = fs::read_to_string(path)?;
|
||||
@@ -128,7 +156,10 @@ impl Config {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("Varning: kunde inte ladda {}: {}", path, e);
|
||||
Config::default()
|
||||
Config {
|
||||
keybinds: default_keybinds(),
|
||||
..Config::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
98
src/log.rs
Normal file
98
src/log.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
/// Enkel fil-logger. Loggar till <exe_dir>/logs/<datum>.log
|
||||
/// Trådsäker via en global Mutex<File>.
|
||||
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use std::time::SystemTime;
|
||||
|
||||
static LOG: Mutex<Option<std::fs::File>> = Mutex::new(None);
|
||||
|
||||
/// Initialisera loggern. Anropas en gång från main().
|
||||
pub fn init() {
|
||||
let dir = log_dir();
|
||||
if let Err(e) = fs::create_dir_all(&dir) {
|
||||
eprintln!("log::init: kunde inte skapa logg-mapp {:?}: {}", dir, e);
|
||||
return;
|
||||
}
|
||||
let name = today_filename();
|
||||
let path = dir.join(name);
|
||||
match OpenOptions::new().create(true).append(true).open(&path) {
|
||||
Ok(f) => {
|
||||
*LOG.lock().unwrap() = Some(f);
|
||||
log(&format!("=== TUI-WM startad ==="));
|
||||
}
|
||||
Err(e) => eprintln!("log::init: kunde inte öppna {:?}: {}", path, e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Skriv en logg-rad med tidsstämpel.
|
||||
pub fn log(msg: &str) {
|
||||
let ts = timestamp();
|
||||
let line = format!("[{}] {}\n", ts, msg);
|
||||
if let Ok(mut guard) = LOG.lock() {
|
||||
if let Some(f) = guard.as_mut() {
|
||||
let _ = f.write_all(line.as_bytes());
|
||||
let _ = f.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Makro för bekväm formatering — används som log!("foo {}", bar)
|
||||
#[macro_export]
|
||||
macro_rules! log {
|
||||
($($arg:tt)*) => {
|
||||
$crate::log::log(&format!($($arg)*))
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Interna hjälpfunktioner ──────────────────────────────────────────────────
|
||||
|
||||
fn log_dir() -> PathBuf {
|
||||
// Lägg logs/ bredvid den körande binären
|
||||
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("."));
|
||||
exe.parent().unwrap_or(std::path::Path::new(".")).join("logs")
|
||||
}
|
||||
|
||||
fn today_filename() -> String {
|
||||
// Bygg YYYY-MM-DD från SystemTime utan externa beroenden
|
||||
let secs = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
let (y, mo, d) = unix_to_ymd(secs);
|
||||
format!("{:04}-{:02}-{:02}.log", y, mo, d)
|
||||
}
|
||||
|
||||
fn timestamp() -> String {
|
||||
let secs = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
let subsec_ms = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.subsec_millis())
|
||||
.unwrap_or(0);
|
||||
let (y, mo, d) = unix_to_ymd(secs);
|
||||
let h = (secs % 86400) / 3600;
|
||||
let m = (secs % 3600) / 60;
|
||||
let s = secs % 60;
|
||||
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}", y, mo, d, h, m, s, subsec_ms)
|
||||
}
|
||||
|
||||
/// Konverterar Unix-sekunder till (år, månad, dag). Ingen extern crate nödvändig.
|
||||
fn unix_to_ymd(secs: u64) -> (u32, u32, u32) {
|
||||
// Algoritm: https://howardhinnant.github.io/date_algorithms.html
|
||||
let z = (secs / 86400) as i64 + 719468;
|
||||
let era = if z >= 0 { z } else { z - 146096 } / 146097;
|
||||
let doe = (z - era * 146097) as u32;
|
||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||
let y = yoe as i64 + era * 400;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
let y = if m <= 2 { y + 1 } else { y };
|
||||
(y as u32, m, d)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
mod app;
|
||||
mod config;
|
||||
mod log;
|
||||
mod pty;
|
||||
mod render;
|
||||
|
||||
@@ -14,6 +15,7 @@ use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
use std::{io, time::Duration};
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
log::init();
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
|
||||
@@ -41,6 +41,8 @@ pub fn render(frame: &mut Frame, app: &App) {
|
||||
pi,
|
||||
&panel_cfg.status_widgets,
|
||||
status_states,
|
||||
app.focused_id.is_none(),
|
||||
app.hovered_tui_btn,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -61,6 +63,8 @@ fn render_panel(
|
||||
panel_idx: usize,
|
||||
status_widgets: &[crate::config::StatusWidget],
|
||||
status_states: &[crate::app::StatusWidgetState],
|
||||
desktop_focused: bool,
|
||||
hovered_tui: bool,
|
||||
) {
|
||||
// Panelens bakgrund
|
||||
frame.render_widget(
|
||||
@@ -68,15 +72,24 @@ fn render_panel(
|
||||
rect,
|
||||
);
|
||||
|
||||
// Logotyp
|
||||
// Logotyp – klickbar meny-knapp med hover-highlight
|
||||
let logo_style = if hovered_tui {
|
||||
Style::default().fg(Color::Black).bg(Color::Cyan).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)
|
||||
};
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled(
|
||||
" TUI-WM ",
|
||||
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
Paragraph::new(Span::styled(" TUI-WM ", logo_style)),
|
||||
Rect::new(rect.x + 1, rect.y, 9, 1),
|
||||
);
|
||||
|
||||
// Fokus-indikator: grön ● = skrivbord fokuserat, grå ● = fönster fokuserat
|
||||
let dot_color = if desktop_focused { Color::Green } else { Color::Indexed(240) };
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled("●", Style::default().fg(dot_color).bg(Color::Indexed(238)))),
|
||||
Rect::new(rect.x + 10, rect.y, 1, 1),
|
||||
);
|
||||
|
||||
// Vänsterjusterade knappar
|
||||
for (ii, item) in items.iter().enumerate() {
|
||||
let Some(&ir) = item_rects.get(ii) else { continue };
|
||||
@@ -117,7 +130,7 @@ fn render_panel(
|
||||
}
|
||||
|
||||
// Vänster-dockade status-widgets renderas efter knapparna
|
||||
let left_start = item_rects.last().map(|r| r.x + r.width + 1).unwrap_or(rect.x + 10);
|
||||
let left_start = item_rects.last().map(|r| r.x + r.width + 1).unwrap_or(rect.x + 12);
|
||||
let mut left_x = left_start;
|
||||
for (si, sw_cfg) in status_widgets.iter().enumerate() {
|
||||
if sw_cfg.align != StatusAlign::Left {
|
||||
@@ -154,7 +167,8 @@ fn render_dropdown(frame: &mut Frame, dd: &DropdownState) {
|
||||
} else {
|
||||
Style::default().fg(Color::White).bg(Color::Indexed(236))
|
||||
};
|
||||
let draw_rect = Rect::new(ir.x + 1, ir.y + 1, ir.width.saturating_sub(2), 1);
|
||||
// ir pekar redan på den faktiska raden innanför kanten
|
||||
let draw_rect = Rect::new(ir.x + 1, ir.y, ir.width.saturating_sub(2), 1);
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled(format!(" {} ", item.label), style)),
|
||||
draw_rect,
|
||||
|
||||
Reference in New Issue
Block a user