oxwm

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

Added tag functionality, fixed issue where tag was killing windows, added alacritty to devshell, added options in just file

Commit
2eed31f6247b7f6adee04acebae6c5ef5cd0cdb2
Parent
8c774ac
Author
tonybtw <tonybtw@tonybtw.com>
Date
2025-10-03 04:36:08

Diff

diff --git a/flake.nix b/flake.nix
index d7e1da7..b0fa72d 100644
--- a/flake.nix
+++ b/flake.nix
@@ -14,9 +14,9 @@
       buildInputs = [
         pkgs.rustc
         pkgs.cargo
-        pkgs.xorg.xorgserver
         pkgs.xorg.xclock
         pkgs.xterm
+        pkgs.alacritty
         pkgs.just
       ];
       shellHook = ''
diff --git a/justfile b/justfile
index 5fe20ff..2cc6018 100644
--- a/justfile
+++ b/justfile
@@ -1,3 +1,16 @@
+build:
+    cargo build --release
+
+install: build
+    sudo cp target/release/oxwm /usr/local/bin/oxwm
+    @echo "✓ oxwm installed. Restart X or hit Mod+Shift+R (when implemented)"
+
+uninstall:
+    sudo rm -f /usr/local/bin/oxwm
+
+clean:
+    cargo clean
+
 test:
     pkill Xephyr || true
     Xephyr -screen 1280x800 :1 & sleep 1
diff --git a/src/keyboard/handlers.rs b/src/keyboard/handlers.rs
index d4b315e..87cd452 100644
--- a/src/keyboard/handlers.rs
+++ b/src/keyboard/handlers.rs
@@ -9,6 +9,8 @@ pub enum KeyAction {
     KillClient,
     FocusStack,
     Quit,
+    ViewTag,
+    MoveToTag,
     None,
 }
 
@@ -47,27 +49,73 @@ const KEYBINDINGS: &[Key] = &[
         &[KeyButMask::MOD1],
         keycodes::RETURN,
         KeyAction::Spawn,
-        Arg::Str("xclock"),
+        Arg::Str("alacritty"),
     ),
     Key::new(
-        &[KeyButMask::MOD1, KeyButMask::SHIFT],
+        &[KeyButMask::MOD1],
         keycodes::Q,
         KeyAction::KillClient,
         Arg::None,
     ),
-    Key::new(&[KeyButMask::MOD1], keycodes::Q, KeyAction::Quit, Arg::None),
+    Key::new(
+        &[KeyButMask::MOD1, KeyButMask::SHIFT],
+        keycodes::Q,
+        KeyAction::Quit,
+        Arg::None,
+    ),
     Key::new(
         &[KeyButMask::MOD1],
-        keycodes::J,
+        keycodes::K,
         KeyAction::FocusStack,
         Arg::Int(1),
     ),
     Key::new(
         &[KeyButMask::MOD1],
-        keycodes::K,
+        keycodes::J,
         KeyAction::FocusStack,
         Arg::Int(-1),
     ),
+    // NEW: Tag bindings (Mod+1 through Mod+9)
+    Key::new(
+        &[KeyButMask::MOD1],
+        keycodes::KEY_1,
+        KeyAction::ViewTag,
+        Arg::Int(0),
+    ),
+    Key::new(
+        &[KeyButMask::MOD1],
+        keycodes::KEY_2,
+        KeyAction::ViewTag,
+        Arg::Int(1),
+    ),
+    Key::new(
+        &[KeyButMask::MOD1],
+        keycodes::KEY_3,
+        KeyAction::ViewTag,
+        Arg::Int(2),
+    ),
+    // ... add KEY_4 through KEY_9
+
+    // NEW: Move window to tag (Mod+Shift+1 through Mod+Shift+9)
+    Key::new(
+        &[KeyButMask::MOD1, KeyButMask::SHIFT],
+        keycodes::KEY_1,
+        KeyAction::MoveToTag,
+        Arg::Int(0),
+    ),
+    Key::new(
+        &[KeyButMask::MOD1, KeyButMask::SHIFT],
+        keycodes::KEY_2,
+        KeyAction::MoveToTag,
+        Arg::Int(1),
+    ),
+    Key::new(
+        &[KeyButMask::MOD1, KeyButMask::SHIFT],
+        keycodes::KEY_3,
+        KeyAction::MoveToTag,
+        Arg::Int(2),
+    ),
+    // ... etc
 ];
 
 fn modifiers_to_mask(modifiers: &[KeyButMask]) -> u16 {
diff --git a/src/keyboard/keycodes.rs b/src/keyboard/keycodes.rs
index f1508e2..3b57218 100644
--- a/src/keyboard/keycodes.rs
+++ b/src/keyboard/keycodes.rs
@@ -1,4 +1,6 @@
-// wip
+#![allow(dead_code)]
+// Allowing dead code here because its just a file for potential keybinds
+
 pub const RETURN: u8 = 36;
 pub const Q: u8 = 24;
 pub const ESCAPE: u8 = 9;
@@ -7,13 +9,11 @@ pub const TAB: u8 = 23;
 pub const BACKSPACE: u8 = 22;
 pub const DELETE: u8 = 119;
 
-// Function keys
 pub const F1: u8 = 67;
 pub const F2: u8 = 68;
 pub const F3: u8 = 69;
 pub const F4: u8 = 70;
 
-// Letters (assuming QWERTY)
 pub const A: u8 = 38;
 pub const S: u8 = 39;
 pub const D: u8 = 40;
@@ -22,7 +22,12 @@ pub const J: u8 = 44;
 pub const K: u8 = 45;
 pub const L: u8 = 46;
 
-// Numbers
 pub const KEY_1: u8 = 10;
 pub const KEY_2: u8 = 11;
 pub const KEY_3: u8 = 12;
+pub const KEY_4: u8 = 13;
+pub const KEY_5: u8 = 14;
+pub const KEY_6: u8 = 15;
+pub const KEY_7: u8 = 16;
+pub const KEY_8: u8 = 17;
+pub const KEY_9: u8 = 18;
diff --git a/src/window_manager.rs b/src/window_manager.rs
index b344af5..422e09b 100644
--- a/src/window_manager.rs
+++ b/src/window_manager.rs
@@ -9,6 +9,15 @@ use x11rb::protocol::Event;
 use x11rb::protocol::xproto::*;
 use x11rb::rust_connection::RustConnection;
 
+// TODO: move this to its own file? (tag.rs?)
+pub type TagMask = u32;
+
+pub const TAG_COUNT: usize = 9;
+
+pub fn tag_mask(tag: usize) -> TagMask {
+    1 << tag
+}
+
 pub struct WindowManager {
     connection: RustConnection,
     screen_number: usize,
@@ -17,6 +26,8 @@ pub struct WindowManager {
     windows: Vec<Window>,
     focused_window: Option<Window>,
     layout: Box<dyn Layout>,
+    window_tags: std::collections::HashMap<Window, TagMask>,
+    selected_tags: TagMask,
 }
 
 impl WindowManager {
@@ -45,6 +56,8 @@ impl WindowManager {
             windows: Vec::new(),
             focused_window: None,
             layout: Box::new(TilingLayout),
+            window_tags: std::collections::HashMap::new(),
+            selected_tags: tag_mask(0),
         });
     }
 
@@ -95,6 +108,16 @@ impl WindowManager {
                 println!("Quitting window manager");
                 std::process::exit(0);
             }
+            KeyAction::ViewTag => {
+                if let Arg::Int(tag_index) = arg {
+                    self.view_tag(*tag_index as usize)?;
+                }
+            }
+            KeyAction::MoveToTag => {
+                if let Arg::Int(tag_index) = arg {
+                    self.move_to_tag(*tag_index as usize)?;
+                }
+            }
             KeyAction::None => {
                 //no-op
             }
@@ -102,28 +125,89 @@ impl WindowManager {
         Ok(())
     }
 
+    fn is_window_visible(&self, window: Window) -> bool {
+        if let Some(&tags) = self.window_tags.get(&window) {
+            (tags & self.selected_tags) != 0
+        } else {
+            false
+        }
+    }
+
+    fn visible_windows(&self) -> Vec<Window> {
+        self.windows
+            .iter()
+            .filter(|&&w| self.is_window_visible(w))
+            .copied()
+            .collect()
+    }
+
+    fn update_window_visibility(&self) -> Result<()> {
+        for &window in &self.windows {
+            if self.is_window_visible(window) {
+                self.connection.map_window(window)?;
+            } else {
+                self.connection.unmap_window(window)?;
+            }
+        }
+        self.connection.flush()?;
+        Ok(())
+    }
+
+    pub fn view_tag(&mut self, tag_index: usize) -> Result<()> {
+        if tag_index >= TAG_COUNT {
+            return Ok(());
+        }
+
+        self.selected_tags = tag_mask(tag_index);
+        println!("Viewing tag {}", tag_index + 1);
+
+        self.update_window_visibility()?;
+        self.apply_layout()?;
+
+        let visible = self.visible_windows();
+        self.set_focus(visible.first().copied())?;
+
+        Ok(())
+    }
+
+    pub fn move_to_tag(&mut self, tag_index: usize) -> Result<()> {
+        if tag_index >= TAG_COUNT {
+            return Ok(());
+        }
+
+        if let Some(focused) = self.focused_window {
+            let mask = tag_mask(tag_index);
+            self.window_tags.insert(focused, mask);
+
+            println!("Moved window {} to tag {}", focused, tag_index + 1);
+
+            self.update_window_visibility()?;
+            self.apply_layout()?;
+        }
+
+        Ok(())
+    }
+
     pub fn cycle_focus(&mut self, direction: i32) -> Result<()> {
-        if self.windows.is_empty() {
+        let visible = self.visible_windows();
+
+        if visible.is_empty() {
             return Ok(());
         }
 
         let next_window = if let Some(current) = self.focused_window {
-            if let Some(current_index) = self.windows.iter().position(|&w| w == current) {
+            if let Some(current_index) = visible.iter().position(|&w| w == current) {
                 let next_index = if direction > 0 {
-                    (current_index + 1) % self.windows.len()
+                    (current_index + 1) % visible.len()
                 } else {
-                    if current_index == 0 {
-                        self.windows.len() - 1
-                    } else {
-                        current_index - 1
-                    }
+                    (current_index + visible.len() - 1) % visible.len()
                 };
-                self.windows[next_index]
+                visible[next_index]
             } else {
-                self.windows[0]
+                visible[0]
             }
         } else {
-            self.windows[0]
+            visible[0]
         };
 
         self.set_focus(Some(next_window))?;
@@ -174,15 +258,29 @@ impl WindowManager {
             Event::MapRequest(event) => {
                 self.connection.map_window(event.window)?;
                 self.windows.push(event.window);
+                self.window_tags.insert(event.window, self.selected_tags);
                 self.apply_layout()?;
                 self.set_focus(Some(event.window))?;
             }
             Event::UnmapNotify(event) => {
                 if self.windows.contains(&event.window) {
-                    println!("Window {} unmapped, removing from layout", event.window);
-                    self.remove_window(event.window)?;
+                    if self.is_window_visible(event.window) {
+                        println!("Visible window {} unmapped, removing", event.window);
+                        self.remove_window(event.window)?;
+                    } else {
+                        println!(
+                            "Hidden window {} unmapped (expected), ignoring",
+                            event.window
+                        );
+                    }
                 }
             }
+            // Event::UnmapNotify(event) => {
+            //     if self.windows.contains(&event.window) {
+            //         println!("Window {} unmapped, removing from layout", event.window);
+            //         self.remove_window(event.window)?;
+            //     }
+            // }
             Event::DestroyNotify(event) => {
                 if self.windows.contains(&event.window) {
                     println!("Window {} destroyed, removing from layout", event.window);
@@ -203,12 +301,10 @@ impl WindowManager {
         let screen_height = self.screen.height_in_pixels as u32;
         let border_width = 2u32;
 
-        let geometries = self
-            .layout
-            .arrange(&self.windows, screen_width, screen_height);
+        let visible = self.visible_windows();
+        let geometries = self.layout.arrange(&visible, screen_width, screen_height);
 
-        for (window, geometry) in self.windows.iter().zip(geometries.iter()) {
-            // Adjust for full borders
+        for (window, geometry) in visible.iter().zip(geometries.iter()) {
             let adjusted_width = geometry.width.saturating_sub(2 * border_width);
             let adjusted_height = geometry.height.saturating_sub(2 * border_width);
 
@@ -222,12 +318,13 @@ impl WindowManager {
             )?;
         }
         self.connection.flush()?;
-        return Ok(());
+        Ok(())
     }
 
     fn remove_window(&mut self, window: Window) -> Result<()> {
         let initial_count = self.windows.len();
         self.windows.retain(|&w| w != window);
+        self.window_tags.remove(&window);
 
         if self.windows.len() < initial_count {
             println!("Removed window {} from management", window);
@@ -243,7 +340,6 @@ impl WindowManager {
 
             self.apply_layout()?;
         }
-
         Ok(())
     }
 }