Add stack promotion action
- SHA
3b49a3e9ee3e285b915d69045b89e4b506f6f310- Parents
-
b7782ed - Tree
024c2c4
3b49a3e
3b49a3e9ee3e285b915d69045b89e4b506f6f310b7782ed
024c2c4| Status | File | + | - |
|---|---|---|---|
| M |
tarmac/src/config/lua.rs
|
8 | 0 |
| M |
tarmac/src/core/input.rs
|
1 | 0 |
| M |
tarmac/src/core/state.rs
|
57 | 0 |
| M |
tarmac/src/core/tree.rs
|
40 | 0 |
| M |
tarmac/src/main.rs
|
6 | 0 |
| M |
tarmacctl/src/main.rs
|
1 | 0 |
tarmac/src/config/lua.rsmodified@@ -841,6 +841,7 @@ fn parse_action(action: &str) -> Result<Action, &'static str> { | ||
| 841 | 841 | "equalize" => Ok(Action::Equalize), |
| 842 | 842 | "toggle_float" => Ok(Action::ToggleFloat), |
| 843 | 843 | "unstack" => Ok(Action::Unstack), |
| 844 | + "promote_stack" => Ok(Action::PromoteStack), | |
| 844 | 845 | "workspace_next" => Ok(Action::WorkspaceNext), |
| 845 | 846 | "workspace_prev" => Ok(Action::WorkspacePrev), |
| 846 | 847 | "focus_monitor_next" => Ok(Action::FocusMonitorNext), |
@@ -903,6 +904,11 @@ pub fn default_keybinds(settings: &Settings) -> Vec<LuaKeybind> { | ||
| 903 | 904 | key: Key::U, |
| 904 | 905 | action: Action::Unstack, |
| 905 | 906 | }, |
| 907 | + LuaKeybind { | |
| 908 | + modifiers: Modifiers::OPTION | Modifiers::SHIFT, | |
| 909 | + key: Key::Period, | |
| 910 | + action: Action::PromoteStack, | |
| 911 | + }, | |
| 906 | 912 | // Focus |
| 907 | 913 | LuaKeybind { |
| 908 | 914 | modifiers: m, |
@@ -1169,6 +1175,7 @@ pub fn format_action(action: &Action) -> String { | ||
| 1169 | 1175 | Action::WorkspacePrev => "workspace_prev".to_string(), |
| 1170 | 1176 | Action::ToggleFloat => "toggle_float".to_string(), |
| 1171 | 1177 | Action::Unstack => "unstack".to_string(), |
| 1178 | + Action::PromoteStack => "promote_stack".to_string(), | |
| 1172 | 1179 | Action::ToggleSpecial(name) => format!("toggle_special {name}"), |
| 1173 | 1180 | Action::MoveToSpecial(name) => format!("move_to_special {name}"), |
| 1174 | 1181 | Action::FocusMonitorNext => "focus_monitor_next".to_string(), |
@@ -1287,6 +1294,7 @@ mod tests { | ||
| 1287 | 1294 | assert_eq!(parse_action("equalize").unwrap(), Action::Equalize); |
| 1288 | 1295 | assert_eq!(parse_action("close").unwrap(), Action::CloseWindow); |
| 1289 | 1296 | assert_eq!(parse_action("toggle_float").unwrap(), Action::ToggleFloat); |
| 1297 | + assert_eq!(parse_action("promote_stack").unwrap(), Action::PromoteStack); | |
| 1290 | 1298 | } |
| 1291 | 1299 | |
| 1292 | 1300 | #[test] |
tarmac/src/core/input.rsmodified@@ -192,6 +192,7 @@ pub enum Action { | ||
| 192 | 192 | MoveToMonitorNext, // Move window to next monitor |
| 193 | 193 | MoveToMonitorPrev, // Move window to previous monitor |
| 194 | 194 | Unstack, // Restore a stacked subtree to its saved BSP shape |
| 195 | + PromoteStack, // Move the active stack member to the top/front of the stack | |
| 195 | 196 | Reload, // Hot reload config |
| 196 | 197 | Exit, // Clean exit |
| 197 | 198 | } |
tarmac/src/core/state.rsmodified@@ -1493,6 +1493,28 @@ impl WmState { | ||
| 1493 | 1493 | } |
| 1494 | 1494 | } |
| 1495 | 1495 | |
| 1496 | + pub fn promote_stack_focused(&mut self) { | |
| 1497 | + let focused = match self.effective_focused() { | |
| 1498 | + Some(f) => f, | |
| 1499 | + None => return, | |
| 1500 | + }; | |
| 1501 | + | |
| 1502 | + let Some(ws_idx) = self.workspaces.find_window(focused) else { | |
| 1503 | + return; | |
| 1504 | + }; | |
| 1505 | + | |
| 1506 | + if self | |
| 1507 | + .workspaces | |
| 1508 | + .get_mut(ws_idx) | |
| 1509 | + .tree | |
| 1510 | + .promote_stack_window(focused) | |
| 1511 | + { | |
| 1512 | + self.apply_layout(); | |
| 1513 | + self.focus_window(focused); | |
| 1514 | + tracing::info!(id = focused, "promoted focused stack window to top"); | |
| 1515 | + } | |
| 1516 | + } | |
| 1517 | + | |
| 1496 | 1518 | pub fn click_to_focus(&mut self, x: f64, y: f64) { |
| 1497 | 1519 | let ws = self.active_workspace(); |
| 1498 | 1520 | |
@@ -3707,6 +3729,41 @@ mod tests { | ||
| 3707 | 3729 | assert_eq!(state.active_workspace().focused, Some(40)); |
| 3708 | 3730 | } |
| 3709 | 3731 | |
| 3732 | + #[test] | |
| 3733 | + fn promote_stack_focused_moves_active_window_to_top() { | |
| 3734 | + let mut state = WmState::new(); | |
| 3735 | + state.monitors = vec![Monitor { | |
| 3736 | + id: 42, | |
| 3737 | + frame: Rect::new(0.0, 0.0, 1920.0, 1080.0), | |
| 3738 | + usable_frame: Rect::new(0.0, 33.0, 1920.0, 1047.0), | |
| 3739 | + is_primary: true, | |
| 3740 | + active_workspace: 0, | |
| 3741 | + }]; | |
| 3742 | + state.sync_workspace_visibility(); | |
| 3743 | + let screen = state.monitors[0].usable_frame; | |
| 3744 | + | |
| 3745 | + { | |
| 3746 | + let ws = state.workspaces.get_mut(0); | |
| 3747 | + ws.tree.insert_with_rect(1, None, screen); | |
| 3748 | + ws.tree.insert_with_rect(2, Some(1), screen); | |
| 3749 | + ws.tree.insert_with_rect(3, Some(2), screen); | |
| 3750 | + assert!(ws.tree.make_stack_for_window(3)); | |
| 3751 | + ws.tree.set_stack_active(3); | |
| 3752 | + ws.record_focus(3); | |
| 3753 | + } | |
| 3754 | + | |
| 3755 | + state.focus_direction(Direction::Left); | |
| 3756 | + assert_eq!(state.active_workspace().focused, Some(2)); | |
| 3757 | + | |
| 3758 | + state.promote_stack_focused(); | |
| 3759 | + | |
| 3760 | + assert_eq!(state.active_workspace().focused, Some(2)); | |
| 3761 | + assert_eq!( | |
| 3762 | + state.active_workspace().tree.stack_info(2), | |
| 3763 | + Some((vec![2, 3], 0)) | |
| 3764 | + ); | |
| 3765 | + } | |
| 3766 | + | |
| 3710 | 3767 | #[test] |
| 3711 | 3768 | fn unstack_focused_restores_original_tree() { |
| 3712 | 3769 | let mut state = WmState::new(); |
tarmac/src/core/tree.rsmodified@@ -808,6 +808,34 @@ impl Node { | ||
| 808 | 808 | } |
| 809 | 809 | } |
| 810 | 810 | |
| 811 | + pub fn promote_stack_window(&mut self, window: WindowId) -> bool { | |
| 812 | + match self { | |
| 813 | + Node::Stack { | |
| 814 | + windows, | |
| 815 | + active, | |
| 816 | + previous, | |
| 817 | + } if windows.contains(&window) => { | |
| 818 | + let Some(idx) = windows.iter().position(|wid| *wid == window) else { | |
| 819 | + return false; | |
| 820 | + }; | |
| 821 | + if idx == 0 { | |
| 822 | + *active = 0; | |
| 823 | + return true; | |
| 824 | + } | |
| 825 | + let displaced = windows[0]; | |
| 826 | + windows.remove(idx); | |
| 827 | + windows.insert(0, window); | |
| 828 | + *active = 0; | |
| 829 | + previous.swap(window, displaced); | |
| 830 | + true | |
| 831 | + } | |
| 832 | + Node::Internal { left, right, .. } => { | |
| 833 | + left.promote_stack_window(window) || right.promote_stack_window(window) | |
| 834 | + } | |
| 835 | + _ => false, | |
| 836 | + } | |
| 837 | + } | |
| 838 | + | |
| 811 | 839 | pub fn make_stack_for_window(&mut self, window: WindowId) -> bool { |
| 812 | 840 | self.make_stack_for_window_impl(window) |
| 813 | 841 | } |
@@ -1493,6 +1521,18 @@ mod tests { | ||
| 1493 | 1521 | assert_eq!(g2.height, g3.height); |
| 1494 | 1522 | } |
| 1495 | 1523 | |
| 1524 | + #[test] | |
| 1525 | + fn promote_stack_window_moves_target_to_top() { | |
| 1526 | + let mut tree = Node::Stack { | |
| 1527 | + windows: vec![1, 2, 3], | |
| 1528 | + active: 2, | |
| 1529 | + previous: Box::new(Node::Leaf { window: Some(3) }), | |
| 1530 | + }; | |
| 1531 | + | |
| 1532 | + assert!(tree.promote_stack_window(3)); | |
| 1533 | + assert_eq!(tree.stack_info(3), Some((vec![3, 1, 2], 0))); | |
| 1534 | + } | |
| 1535 | + | |
| 1496 | 1536 | #[test] |
| 1497 | 1537 | fn cycle_stack_stops_at_directional_boundary() { |
| 1498 | 1538 | let mut tree = Node::empty(); |
tarmac/src/main.rsmodified@@ -251,6 +251,7 @@ fn handle_action(action: Action) { | ||
| 251 | 251 | } |
| 252 | 252 | Action::ToggleFloat => state.toggle_float(), |
| 253 | 253 | Action::Unstack => state.unstack_focused(), |
| 254 | + Action::PromoteStack => state.promote_stack_focused(), | |
| 254 | 255 | Action::ToggleSpecial(ref name) => state.toggle_special(name), |
| 255 | 256 | Action::MoveToSpecial(ref name) => state.move_to_special(name), |
| 256 | 257 | Action::FocusMonitorNext => { |
@@ -333,6 +334,7 @@ gar.bind("mod+shift+q", "close") | ||
| 333 | 334 | gar.bind("mod+e", "equalize") |
| 334 | 335 | gar.bind("mod+shift+space", "toggle_float") |
| 335 | 336 | gar.bind("mod+shift+u", "unstack") |
| 337 | +gar.bind("alt+shift+.", "promote_stack") | |
| 336 | 338 | |
| 337 | 339 | -- Focus |
| 338 | 340 | gar.bind("mod+h", "focus left") |
@@ -508,6 +510,10 @@ fn process_ipc_command( | ||
| 508 | 510 | state.unstack_focused(); |
| 509 | 511 | Response::ok_empty() |
| 510 | 512 | } |
| 513 | + "promote_stack" => { | |
| 514 | + state.promote_stack_focused(); | |
| 515 | + Response::ok_empty() | |
| 516 | + } | |
| 511 | 517 | "workspace" => { |
| 512 | 518 | if let Some(target) = request |
| 513 | 519 | .args |
tarmacctl/src/main.rsmodified@@ -39,6 +39,7 @@ fn main() { | ||
| 39 | 39 | eprintln!(" close Close focused window"); |
| 40 | 40 | eprintln!(" equalize Equalize all splits"); |
| 41 | 41 | eprintln!(" unstack Restore the focused stacked subtree"); |
| 42 | + eprintln!(" promote_stack Move the focused stack window to the top"); | |
| 42 | 43 | eprintln!(" workspace <1-10> Switch workspace"); |
| 43 | 44 | eprintln!(" move-to-workspace <1-10> Move window to workspace"); |
| 44 | 45 | eprintln!(" toggle-floating Toggle floating state"); |