oxwm

https://git.tonybtw.com/oxwm.git git://git.tonybtw.com/oxwm.git

Added comboview logic from dwm, and added sticky tag view from dwm. updated logic so clients move offscreen instead of stay stored in respective tags.

Commit
fdbc8815968e9025ce980f05130443f4541b0bb2
Parent
23d144c
Author
tonybtw <tonybtw@tonybtw.com>
Date
2025-12-01 06:02:31

Diff

diff --git a/resources/test-config.lua b/resources/test-config.lua
index 099a6cd..4ec331a 100644
--- a/resources/test-config.lua
+++ b/resources/test-config.lua
@@ -94,6 +94,7 @@ oxwm.key.bind({ modkey, "Shift" }, "J", oxwm.client.swap_direction("down"))
 oxwm.key.bind({ modkey, "Shift" }, "K", oxwm.client.swap_direction("up"))
 oxwm.key.bind({ modkey, "Shift" }, "L", oxwm.client.swap_direction("right"))
 
+-- View tag (switch workspace)
 oxwm.key.bind({ modkey }, "1", oxwm.tag.view(0))
 oxwm.key.bind({ modkey }, "2", oxwm.tag.view(1))
 oxwm.key.bind({ modkey }, "3", oxwm.tag.view(2))
@@ -104,6 +105,7 @@ oxwm.key.bind({ modkey }, "7", oxwm.tag.view(6))
 oxwm.key.bind({ modkey }, "8", oxwm.tag.view(7))
 oxwm.key.bind({ modkey }, "9", oxwm.tag.view(8))
 
+-- Move window to tag
 oxwm.key.bind({ modkey, "Shift" }, "1", oxwm.tag.move_to(0))
 oxwm.key.bind({ modkey, "Shift" }, "2", oxwm.tag.move_to(1))
 oxwm.key.bind({ modkey, "Shift" }, "3", oxwm.tag.move_to(2))
@@ -114,6 +116,30 @@ oxwm.key.bind({ modkey, "Shift" }, "7", oxwm.tag.move_to(6))
 oxwm.key.bind({ modkey, "Shift" }, "8", oxwm.tag.move_to(7))
 oxwm.key.bind({ modkey, "Shift" }, "9", oxwm.tag.move_to(8))
 
+-- Toggle view (view multiple tags at once) - dwm-style multi-tag viewing
+-- Example: Mod+Ctrl+2 while on tag 1 will show BOTH tags 1 and 2
+oxwm.key.bind({ modkey, "Control" }, "1", oxwm.tag.toggleview(0))
+oxwm.key.bind({ modkey, "Control" }, "2", oxwm.tag.toggleview(1))
+oxwm.key.bind({ modkey, "Control" }, "3", oxwm.tag.toggleview(2))
+oxwm.key.bind({ modkey, "Control" }, "4", oxwm.tag.toggleview(3))
+oxwm.key.bind({ modkey, "Control" }, "5", oxwm.tag.toggleview(4))
+oxwm.key.bind({ modkey, "Control" }, "6", oxwm.tag.toggleview(5))
+oxwm.key.bind({ modkey, "Control" }, "7", oxwm.tag.toggleview(6))
+oxwm.key.bind({ modkey, "Control" }, "8", oxwm.tag.toggleview(7))
+oxwm.key.bind({ modkey, "Control" }, "9", oxwm.tag.toggleview(8))
+
+-- Toggle tag (window on multiple tags) - dwm-style sticky windows
+-- Example: Mod+Ctrl+Shift+2 puts focused window on BOTH current tag and tag 2
+oxwm.key.bind({ modkey, "Control", "Shift" }, "1", oxwm.tag.toggletag(0))
+oxwm.key.bind({ modkey, "Control", "Shift" }, "2", oxwm.tag.toggletag(1))
+oxwm.key.bind({ modkey, "Control", "Shift" }, "3", oxwm.tag.toggletag(2))
+oxwm.key.bind({ modkey, "Control", "Shift" }, "4", oxwm.tag.toggletag(3))
+oxwm.key.bind({ modkey, "Control", "Shift" }, "5", oxwm.tag.toggletag(4))
+oxwm.key.bind({ modkey, "Control", "Shift" }, "6", oxwm.tag.toggletag(5))
+oxwm.key.bind({ modkey, "Control", "Shift" }, "7", oxwm.tag.toggletag(6))
+oxwm.key.bind({ modkey, "Control", "Shift" }, "8", oxwm.tag.toggletag(7))
+oxwm.key.bind({ modkey, "Control", "Shift" }, "9", oxwm.tag.toggletag(8))
+
 oxwm.bar.set_blocks({
     oxwm.bar.block.battery({
         format = "Bat: {}%",
diff --git a/src/config/lua_api.rs b/src/config/lua_api.rs
index 5ed03fd..0eff531 100644
--- a/src/config/lua_api.rs
+++ b/src/config/lua_api.rs
@@ -301,12 +301,22 @@ fn register_tag_module(lua: &Lua, parent: &Table) -> Result<(), ConfigError> {
         create_action_table(lua, "ViewTag", Value::Integer(idx as i64))
     })?;
 
+    let toggleview = lua.create_function(|lua, idx: i32| {
+        create_action_table(lua, "ToggleView", Value::Integer(idx as i64))
+    })?;
+
     let move_to = lua.create_function(|lua, idx: i32| {
         create_action_table(lua, "MoveToTag", Value::Integer(idx as i64))
     })?;
 
+    let toggletag = lua.create_function(|lua, idx: i32| {
+        create_action_table(lua, "ToggleTag", Value::Integer(idx as i64))
+    })?;
+
     tag_table.set("view", view)?;
+    tag_table.set("toggleview", toggleview)?;
     tag_table.set("move_to", move_to)?;
+    tag_table.set("toggletag", toggletag)?;
     parent.set("tag", tag_table)?;
     Ok(())
 }
@@ -726,6 +736,9 @@ fn string_to_action(s: &str) -> mlua::Result<KeyAction> {
         "Restart" => Ok(KeyAction::Restart),
         "Recompile" => Ok(KeyAction::Recompile),
         "ViewTag" => Ok(KeyAction::ViewTag),
+        "ToggleView" => Ok(KeyAction::ToggleView),
+        "MoveToTag" => Ok(KeyAction::MoveToTag),
+        "ToggleTag" => Ok(KeyAction::ToggleTag),
         "ToggleGaps" => Ok(KeyAction::ToggleGaps),
         "SetMasterFactor" => Ok(KeyAction::SetMasterFactor),
         "IncNumMaster" => Ok(KeyAction::IncNumMaster),
@@ -733,7 +746,6 @@ fn string_to_action(s: &str) -> mlua::Result<KeyAction> {
         "ToggleFloating" => Ok(KeyAction::ToggleFloating),
         "ChangeLayout" => Ok(KeyAction::ChangeLayout),
         "CycleLayout" => Ok(KeyAction::CycleLayout),
-        "MoveToTag" => Ok(KeyAction::MoveToTag),
         "FocusMonitor" => Ok(KeyAction::FocusMonitor),
         "ExchangeClient" => Ok(KeyAction::ExchangeClient),
         "ShowKeybindOverlay" => Ok(KeyAction::ShowKeybindOverlay),
diff --git a/src/keyboard/handlers.rs b/src/keyboard/handlers.rs
index 475ecbd..7d867ec 100644
--- a/src/keyboard/handlers.rs
+++ b/src/keyboard/handlers.rs
@@ -21,12 +21,14 @@ pub enum KeyAction {
     Restart,
     Recompile,
     ViewTag,
+    ToggleView,
+    MoveToTag,
+    ToggleTag,
     ToggleGaps,
     ToggleFullScreen,
     ToggleFloating,
     ChangeLayout,
     CycleLayout,
-    MoveToTag,
     FocusMonitor,
     ExchangeClient,
     ShowKeybindOverlay,
diff --git a/src/overlay/keybind.rs b/src/overlay/keybind.rs
index 3a13bfe..9789510 100644
--- a/src/overlay/keybind.rs
+++ b/src/overlay/keybind.rs
@@ -209,7 +209,12 @@ impl KeybindOverlay {
                 Arg::Int(n) => format!("View Workspace {}", n),
                 _ => "View Workspace".to_string(),
             },
+            KeyAction::ToggleView => match &binding.arg {
+                Arg::Int(n) => format!("Toggle View Workspace {}", n),
+                _ => "Toggle View Workspace".to_string(),
+            },
             KeyAction::MoveToTag => "Move Window to Workspace".to_string(),
+            KeyAction::ToggleTag => "Toggle Window on Workspace".to_string(),
             KeyAction::ToggleGaps => "Toggle Window Gaps".to_string(),
             KeyAction::ToggleFullScreen => "Toggle Fullscreen Mode".to_string(),
             KeyAction::ToggleFloating => "Toggle Floating Mode".to_string(),
diff --git a/src/window_manager.rs b/src/window_manager.rs
index e1ca9b4..ab64690 100644
--- a/src/window_manager.rs
+++ b/src/window_manager.rs
@@ -900,11 +900,21 @@ impl WindowManager {
                     self.view_tag(*tag_index as usize)?;
                 }
             }
+            KeyAction::ToggleView => {
+                if let Arg::Int(tag_index) = arg {
+                    self.toggleview(*tag_index as usize)?;
+                }
+            }
             KeyAction::MoveToTag => {
                 if let Arg::Int(tag_index) = arg {
                     self.move_to_tag(*tag_index as usize)?;
                 }
             }
+            KeyAction::ToggleTag => {
+                if let Arg::Int(tag_index) = arg {
+                    self.toggletag(*tag_index as usize)?;
+                }
+            }
             KeyAction::ToggleGaps => {
                 self.gaps_enabled = !self.gaps_enabled;
                 self.apply_layout()?;
@@ -1041,25 +1051,112 @@ impl WindowManager {
         Ok(())
     }
 
+    fn showhide(&mut self, window: Option<Window>) -> WmResult<()> {
+        let Some(window) = window else {
+            return Ok(());
+        };
+
+        let Some(client) = self.clients.get(&window).cloned() else {
+            return Ok(());
+        };
+
+        let monitor = match self.monitors.get(client.monitor_index) {
+            Some(m) => m,
+            None => return Ok(()),
+        };
+
+        let is_visible = (client.tags & monitor.tagset[monitor.selected_tags_index]) != 0;
+
+        if is_visible {
+            self.connection.configure_window(
+                window,
+                &ConfigureWindowAux::new()
+                    .x(client.x_position as i32)
+                    .y(client.y_position as i32),
+            )?;
+
+            let is_floating = client.is_floating;
+            let is_fullscreen = client.is_fullscreen;
+            let has_no_layout = self.layout.name() == LayoutType::Normie.as_str();
+
+            if (has_no_layout || is_floating) && !is_fullscreen {
+                self.connection.configure_window(
+                    window,
+                    &ConfigureWindowAux::new()
+                        .x(client.x_position as i32)
+                        .y(client.y_position as i32)
+                        .width(client.width as u32)
+                        .height(client.height as u32),
+                )?;
+            }
+
+            self.showhide(client.stack_next)?;
+        } else {
+            self.showhide(client.stack_next)?;
+
+            let width = client.width_with_border() as i32;
+            self.connection.configure_window(
+                window,
+                &ConfigureWindowAux::new()
+                    .x(width * -2)
+                    .y(client.y_position as i32),
+            )?;
+        }
+
+        Ok(())
+    }
+
     pub fn view_tag(&mut self, tag_index: usize) -> WmResult<()> {
         if tag_index >= self.config.tags.len() {
             return Ok(());
         }
 
-        if let Some(monitor) = self.monitors.get_mut(self.selected_monitor) {
-            monitor.tagset[monitor.selected_tags_index] = tag_mask(tag_index);
+        let monitor = match self.monitors.get_mut(self.selected_monitor) {
+            Some(m) => m,
+            None => return Ok(()),
+        };
+
+        let new_tagset = tag_mask(tag_index);
+
+        if new_tagset == monitor.tagset[monitor.selected_tags_index] {
+            return Ok(());
         }
 
+        monitor.selected_tags_index ^= 1;
+        monitor.tagset[monitor.selected_tags_index] = new_tagset;
+
         self.save_selected_tags()?;
-        self.update_window_visibility()?;
-        self.apply_layout()?;
+        self.focus(None)?;
+        self.apply_layout()?;  
         self.update_bar()?;
 
-        let visible = self.visible_windows_on_monitor(self.selected_monitor);
-        if let Some(&win) = visible.first() {
-            self.focus(Some(win))?;
+        Ok(())
+    }
+
+    pub fn toggleview(&mut self, tag_index: usize) -> WmResult<()> {
+        if tag_index >= self.config.tags.len() {
+            return Ok(());
         }
 
+        let monitor = match self.monitors.get_mut(self.selected_monitor) {
+            Some(m) => m,
+            None => return Ok(()),
+        };
+
+        let mask = tag_mask(tag_index);
+        let new_tagset = monitor.tagset[monitor.selected_tags_index] ^ mask;
+
+        if new_tagset == 0 {
+            return Ok(());
+        }
+
+        monitor.tagset[monitor.selected_tags_index] = new_tagset;
+
+        self.save_selected_tags()?;
+        self.focus(None)?;
+        self.apply_layout()?;
+        self.update_bar()?;
+
         Ok(())
     }
 
@@ -1093,23 +1190,70 @@ impl WindowManager {
             return Ok(());
         }
 
-        if let Some(focused) = self
+        let focused = match self
             .monitors
             .get(self.selected_monitor)
             .and_then(|m| m.selected_client)
         {
-            let mask = tag_mask(tag_index);
-            self.window_tags.insert(focused, mask);
+            Some(win) => win,
+            None => return Ok(()),
+        };
 
-            if let Err(error) = self.save_client_tag(focused, mask) {
-                eprintln!("Failed to save client tag: {:?}", error);
-            }
+        let mask = tag_mask(tag_index);
 
-            self.update_window_visibility()?;
-            self.apply_layout()?;
-            self.update_bar()?;
+        if let Some(client) = self.clients.get_mut(&focused) {
+            client.tags = mask;
+        }
+
+        self.window_tags.insert(focused, mask);
+
+        if let Err(error) = self.save_client_tag(focused, mask) {
+            eprintln!("Failed to save client tag: {:?}", error);
         }
 
+        self.focus(None)?;
+        self.apply_layout()?;
+        self.update_bar()?;
+
+        Ok(())
+    }
+
+    pub fn toggletag(&mut self, tag_index: usize) -> WmResult<()> {
+        if tag_index >= self.config.tags.len() {
+            return Ok(());
+        }
+
+        let focused = match self
+            .monitors
+            .get(self.selected_monitor)
+            .and_then(|m| m.selected_client)
+        {
+            Some(win) => win,
+            None => return Ok(()),
+        };
+
+        let mask = tag_mask(tag_index);
+        let current_tags = self.clients.get(&focused).map(|c| c.tags).unwrap_or(0);
+        let new_tags = current_tags ^ mask;
+
+        if new_tags == 0 {
+            return Ok(());
+        }
+
+        if let Some(client) = self.clients.get_mut(&focused) {
+            client.tags = new_tags;
+        }
+
+        self.window_tags.insert(focused, new_tags);
+
+        if let Err(error) = self.save_client_tag(focused, new_tags) {
+            eprintln!("Failed to save client tag: {:?}", error);
+        }
+
+        self.focus(None)?;
+        self.apply_layout()?;
+        self.update_bar()?;
+
         Ok(())
     }
 
@@ -2729,6 +2873,13 @@ impl WindowManager {
                 let adjusted_x = geometry.x_coordinate + monitor_x;
                 let adjusted_y = geometry.y_coordinate + monitor_y + bar_height as i32;
 
+                if let Some(client) = self.clients.get_mut(window) {
+                    client.x_position = adjusted_x as i16;
+                    client.y_position = adjusted_y as i16;
+                    client.width = adjusted_width as u16;
+                    client.height = adjusted_height as u16;
+                }
+
                 self.connection.configure_window(
                     *window,
                     &ConfigureWindowAux::new()
@@ -2749,6 +2900,11 @@ impl WindowManager {
             }
         }
 
+        for monitor_index in 0..self.monitors.len() {
+            let stack_head = self.monitors[monitor_index].stack_head;
+            self.showhide(stack_head)?;
+        }
+
         self.connection.flush()?;
 
         let is_tabbed = self.layout.name() == LayoutType::Tabbed.as_str();