oxwm

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

Added tabbed mode, fixed bug where nixos read only directory caused crash every launch, added sane default comments for default config file.

Commit
333cb2dcdb5cdea8eb8e74801b8c95209bee246c
Parent
b23ed69
Author
tonybtw <tonybtw@tonybtw.com>
Date
2025-11-23 07:59:13

Diff

diff --git a/Cargo.lock b/Cargo.lock
index da5edf7..8d9848c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -291,7 +291,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
 
 [[package]]
 name = "oxwm"
-version = "0.7.2"
+version = "0.7.3"
 dependencies = [
  "chrono",
  "dirs",
diff --git a/Cargo.toml b/Cargo.toml
index 2d398ba..5acef17 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "oxwm"
-version = "0.7.2"
+version = "0.7.3"
 edition = "2024"
 
 [lib]
diff --git a/default.nix b/default.nix
index 3faea76..412b4ae 100644
--- a/default.nix
+++ b/default.nix
@@ -34,6 +34,7 @@ rustPlatform.buildRustPackage (finalAttrs: {
   postInstall = ''
     install resources/oxwm.desktop -Dt $out/share/xsessions
     install -Dm644 resources/oxwm.1 -t $out/share/man/man1
+    install -Dm644 templates/oxwm.lua -t $out/share/oxwm
   '';
 
   passthru.providedSessions = ["oxwm"];
diff --git a/src/bin/main.rs b/src/bin/main.rs
index 06a79c7..db6cb3d 100644
--- a/src/bin/main.rs
+++ b/src/bin/main.rs
@@ -72,8 +72,6 @@ fn load_config(custom_path: Option<PathBuf>) -> Result<(oxwm::Config, bool), Box
                 println!("   Please manually port your configuration to the new Lua format.");
                 println!("   See the new config.lua template for examples.\n");
             }
-        } else {
-            update_lsp_files()?;
         }
 
         lua_path
@@ -116,19 +114,40 @@ fn init_config() -> Result<(), Box<dyn std::error::Error>> {
 fn update_lsp_files() -> Result<(), Box<dyn std::error::Error>> {
     let config_directory = get_config_path();
 
-    let library_directory = config_directory.join("lib");
-    std::fs::create_dir_all(&library_directory)?;
+    let system_paths = [
+        PathBuf::from("/usr/share/oxwm/oxwm.lua"),
+        PathBuf::from("/usr/local/share/oxwm/oxwm.lua"),
+    ];
 
-    let oxwm_lua_template = include_str!("../../templates/oxwm.lua");
-    let oxwm_lua_path = library_directory.join("oxwm.lua");
-    std::fs::write(&oxwm_lua_path, oxwm_lua_template)?;
+    let system_oxwm_lua = system_paths.iter().find(|path| path.exists());
 
-    let luarc_content = r#"{
+    let luarc_content = if let Some(system_path) = system_oxwm_lua {
+        format!(
+            r#"{{
+  "workspace.library": [
+    "{}"
+  ]
+}}
+"#,
+            system_path.parent().unwrap().display()
+        )
+    } else {
+        let library_directory = config_directory.join("lib");
+        std::fs::create_dir_all(&library_directory)?;
+
+        let oxwm_lua_template = include_str!("../../templates/oxwm.lua");
+        let oxwm_lua_path = library_directory.join("oxwm.lua");
+        std::fs::write(&oxwm_lua_path, oxwm_lua_template)?;
+
+        r#"{
   "workspace.library": [
     "lib"
   ]
 }
-"#;
+"#
+        .to_string()
+    };
+
     let luarc_path = config_directory.join(".luarc.json");
     std::fs::write(&luarc_path, luarc_content)?;
 
diff --git a/src/config/lua.rs b/src/config/lua.rs
index 9abb68a..a2341b4 100644
--- a/src/config/lua.rs
+++ b/src/config/lua.rs
@@ -48,45 +48,3 @@ pub fn parse_lua_config(
         autostart: builder_data.autostart,
     })
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_parse_minimal_lua_config() {
-        let config_str = r#"
-oxwm.border.set_width(2)
-oxwm.border.set_focused_color(0x6dade3)
-oxwm.border.set_unfocused_color(0xbbbbbb)
-oxwm.bar.set_font("monospace:style=Bold:size=10")
-
-oxwm.gaps.set_enabled(true)
-oxwm.gaps.set_inner(5, 5)
-oxwm.gaps.set_outer(5, 5)
-
-oxwm.set_modkey("Mod4")
-oxwm.set_terminal("st")
-oxwm.set_tags({"1", "2", "3"})
-
-oxwm.key.bind({"Mod4"}, "Return", oxwm.spawn("st"))
-oxwm.key.bind({"Mod4"}, "Q", oxwm.client.kill())
-
-oxwm.bar.add_block("{}", "DateTime", "%H:%M", 1, 0xffffff, true)
-
-oxwm.bar.set_scheme_normal(0xffffff, 0x000000, 0x444444)
-oxwm.bar.set_scheme_occupied(0xffffff, 0x000000, 0x444444)
-oxwm.bar.set_scheme_selected(0xffffff, 0x000000, 0x444444)
-"#;
-
-        let config = parse_lua_config(config_str, None).expect("Failed to parse config");
-
-        assert_eq!(config.border_width, 2);
-        assert_eq!(config.border_focused, 0x6dade3);
-        assert_eq!(config.terminal, "st");
-        assert_eq!(config.tags.len(), 3);
-        assert_eq!(config.keybindings.len(), 2);
-        assert_eq!(config.status_blocks.len(), 1);
-        assert!(config.gaps_enabled);
-    }
-}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 08bb24e..6e92db1 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -1,6 +1,7 @@
 pub mod grid;
 pub mod monocle;
 pub mod normie;
+pub mod tabbed;
 pub mod tiling;
 
 use x11rb::protocol::xproto::Window;
@@ -19,6 +20,7 @@ pub enum LayoutType {
     Normie,
     Grid,
     Monocle,
+    Tabbed,
 }
 
 impl LayoutType {
@@ -28,6 +30,7 @@ impl LayoutType {
             Self::Normie => Box::new(normie::NormieLayout),
             Self::Grid => Box::new(grid::GridLayout),
             Self::Monocle => Box::new(monocle::MonocleLayout),
+            Self::Tabbed => Box::new(tabbed::TabbedLayout),
         }
     }
 
@@ -36,7 +39,8 @@ impl LayoutType {
             Self::Tiling => Self::Normie,
             Self::Normie => Self::Grid,
             Self::Grid => Self::Monocle,
-            Self::Monocle => Self::Tiling,
+            Self::Monocle => Self::Tabbed,
+            Self::Tabbed => Self::Tiling,
         }
     }
 
@@ -46,6 +50,7 @@ impl LayoutType {
             Self::Normie => "normie",
             Self::Grid => "grid",
             Self::Monocle => "monocle",
+            Self::Tabbed => "tabbed",
         }
     }
 
@@ -55,6 +60,7 @@ impl LayoutType {
             "normie" | "floating" => Ok(Self::Normie),
             "grid" => Ok(Self::Grid),
             "monocle" => Ok(Self::Monocle),
+            "tabbed" => Ok(Self::Tabbed),
             _ => Err(format!("Invalid Layout Type: {}", s)),
         }
     }
diff --git a/src/layout/tabbed.rs b/src/layout/tabbed.rs
new file mode 100644
index 0000000..9245c1b
--- /dev/null
+++ b/src/layout/tabbed.rs
@@ -0,0 +1,45 @@
+use super::{GapConfig, Layout, WindowGeometry};
+use x11rb::protocol::xproto::Window;
+
+pub struct TabbedLayout;
+
+pub const TAB_BAR_HEIGHT: u32 = 28;
+
+impl Layout for TabbedLayout {
+    fn name(&self) -> &'static str {
+        super::LayoutType::Tabbed.as_str()
+    }
+
+    fn symbol(&self) -> &'static str {
+        "[=]"
+    }
+
+    fn arrange(
+        &self,
+        windows: &[Window],
+        screen_width: u32,
+        screen_height: u32,
+        gaps: &GapConfig,
+    ) -> Vec<WindowGeometry> {
+        let window_count = windows.len();
+        if window_count == 0 {
+            return Vec::new();
+        }
+
+        let x = gaps.outer_horizontal as i32;
+        let y = (gaps.outer_vertical + TAB_BAR_HEIGHT) as i32;
+        let width = screen_width.saturating_sub(2 * gaps.outer_horizontal);
+        let height = screen_height
+            .saturating_sub(2 * gaps.outer_vertical)
+            .saturating_sub(TAB_BAR_HEIGHT);
+
+        let geometry = WindowGeometry {
+            x_coordinate: x,
+            y_coordinate: y,
+            width,
+            height,
+        };
+
+        vec![geometry; window_count]
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index ad38775..34ae1bb 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -5,6 +5,7 @@ pub mod keyboard;
 pub mod layout;
 pub mod monitor;
 pub mod overlay;
+pub mod tab_bar;
 pub mod window_manager;
 
 pub mod prelude {
diff --git a/src/tab_bar.rs b/src/tab_bar.rs
new file mode 100644
index 0000000..efdf967
--- /dev/null
+++ b/src/tab_bar.rs
@@ -0,0 +1,307 @@
+use crate::bar::font::{Font, FontDraw};
+use crate::errors::X11Error;
+use crate::layout::tabbed::TAB_BAR_HEIGHT;
+use crate::ColorScheme;
+use x11rb::connection::Connection;
+use x11rb::protocol::xproto::*;
+use x11rb::rust_connection::RustConnection;
+use x11rb::COPY_DEPTH_FROM_PARENT;
+
+pub struct TabBar {
+    window: Window,
+    width: u16,
+    height: u16,
+    x_offset: i16,
+    y_offset: i16,
+    graphics_context: Gcontext,
+    pixmap: x11::xlib::Pixmap,
+    display: *mut x11::xlib::Display,
+    font_draw: FontDraw,
+    scheme_normal: ColorScheme,
+    scheme_selected: ColorScheme,
+}
+
+impl TabBar {
+    pub fn new(
+        connection: &RustConnection,
+        screen: &Screen,
+        screen_num: usize,
+        display: *mut x11::xlib::Display,
+        _font: &Font,
+        x: i16,
+        y: i16,
+        width: u16,
+        scheme_normal: ColorScheme,
+        scheme_selected: ColorScheme,
+    ) -> Result<Self, X11Error> {
+        let window = connection.generate_id()?;
+        let graphics_context = connection.generate_id()?;
+
+        let height = TAB_BAR_HEIGHT as u16;
+
+        connection.create_window(
+            COPY_DEPTH_FROM_PARENT,
+            window,
+            screen.root,
+            x,
+            y,
+            width,
+            height,
+            0,
+            WindowClass::INPUT_OUTPUT,
+            screen.root_visual,
+            &CreateWindowAux::new()
+                .background_pixel(scheme_normal.background)
+                .event_mask(EventMask::EXPOSURE | EventMask::BUTTON_PRESS)
+                .override_redirect(1),
+        )?;
+
+        connection.create_gc(
+            graphics_context,
+            window,
+            &CreateGCAux::new()
+                .foreground(scheme_normal.foreground)
+                .background(scheme_normal.background),
+        )?;
+
+        connection.map_window(window)?;
+        connection.flush()?;
+
+        let visual = unsafe { x11::xlib::XDefaultVisual(display, screen_num as i32) };
+        let colormap = unsafe { x11::xlib::XDefaultColormap(display, screen_num as i32) };
+        let depth = unsafe { x11::xlib::XDefaultDepth(display, screen_num as i32) };
+
+        let pixmap = unsafe {
+            x11::xlib::XCreatePixmap(
+                display,
+                window as x11::xlib::Drawable,
+                width as u32,
+                height as u32,
+                depth as u32,
+            )
+        };
+
+        let font_draw = FontDraw::new(display, pixmap, visual, colormap)?;
+
+        Ok(Self {
+            window,
+            width,
+            height,
+            x_offset: x,
+            y_offset: y,
+            graphics_context,
+            pixmap,
+            display,
+            font_draw,
+            scheme_normal,
+            scheme_selected,
+        })
+    }
+
+    pub fn window(&self) -> Window {
+        self.window
+    }
+
+    pub fn draw(
+        &mut self,
+        connection: &RustConnection,
+        font: &Font,
+        windows: &[Window],
+        focused_window: Option<Window>,
+    ) -> Result<(), X11Error> {
+        connection.change_gc(
+            self.graphics_context,
+            &ChangeGCAux::new().foreground(self.scheme_normal.background),
+        )?;
+        connection.flush()?;
+
+        unsafe {
+            let gc = x11::xlib::XCreateGC(self.display, self.pixmap, 0, std::ptr::null_mut());
+            x11::xlib::XSetForeground(
+                self.display,
+                gc,
+                self.scheme_normal.background as u64,
+            );
+            x11::xlib::XFillRectangle(
+                self.display,
+                self.pixmap,
+                gc,
+                0,
+                0,
+                self.width as u32,
+                self.height as u32,
+            );
+            x11::xlib::XFreeGC(self.display, gc);
+        }
+
+        if windows.is_empty() {
+            self.copy_pixmap_to_window();
+            return Ok(());
+        }
+
+        let tab_width = self.width / windows.len() as u16;
+        let mut x_position: i16 = 0;
+
+        for (index, &window) in windows.iter().enumerate() {
+            let is_focused = Some(window) == focused_window;
+            let scheme = if is_focused {
+                &self.scheme_selected
+            } else {
+                &self.scheme_normal
+            };
+
+            let title = self.get_window_title(connection, window);
+            let display_title = if title.is_empty() {
+                format!("Window {}", index + 1)
+            } else {
+                title
+            };
+
+            let text_width = font.text_width(&display_title);
+            let text_x = x_position + ((tab_width.saturating_sub(text_width)) / 2) as i16;
+
+            let top_padding = 6;
+            let text_y = top_padding + font.ascent();
+
+            self.font_draw
+                .draw_text(font, scheme.foreground, text_x, text_y, &display_title);
+
+            if is_focused {
+                let underline_height = 3;
+                let underline_y = self.height as i16 - underline_height;
+
+                unsafe {
+                    let gc =
+                        x11::xlib::XCreateGC(self.display, self.pixmap, 0, std::ptr::null_mut());
+                    x11::xlib::XSetForeground(self.display, gc, scheme.underline as u64);
+                    x11::xlib::XFillRectangle(
+                        self.display,
+                        self.pixmap,
+                        gc,
+                        x_position as i32,
+                        underline_y as i32,
+                        tab_width as u32,
+                        underline_height as u32,
+                    );
+                    x11::xlib::XFreeGC(self.display, gc);
+                }
+            }
+
+            x_position += tab_width as i16;
+        }
+
+        self.copy_pixmap_to_window();
+        Ok(())
+    }
+
+    fn copy_pixmap_to_window(&self) {
+        unsafe {
+            let gc = x11::xlib::XCreateGC(self.display, self.window as u64, 0, std::ptr::null_mut());
+            x11::xlib::XCopyArea(
+                self.display,
+                self.pixmap,
+                self.window as u64,
+                gc,
+                0,
+                0,
+                self.width as u32,
+                self.height as u32,
+                0,
+                0,
+            );
+            x11::xlib::XFreeGC(self.display, gc);
+        }
+    }
+
+    fn get_window_title(&self, connection: &RustConnection, window: Window) -> String {
+        connection
+            .get_property(false, window, AtomEnum::WM_NAME, AtomEnum::STRING, 0, 1024)
+            .ok()
+            .and_then(|cookie| cookie.reply().ok())
+            .and_then(|reply| {
+                if reply.value.is_empty() {
+                    None
+                } else {
+                    std::str::from_utf8(&reply.value).ok().map(|s| s.to_string())
+                }
+            })
+            .unwrap_or_default()
+    }
+
+    pub fn get_clicked_window(
+        &self,
+        windows: &[Window],
+        click_x: i16,
+    ) -> Option<Window> {
+        if windows.is_empty() {
+            return None;
+        }
+
+        let tab_width = self.width / windows.len() as u16;
+        let tab_index = (click_x as u16 / tab_width) as usize;
+
+        windows.get(tab_index).copied()
+    }
+
+    pub fn reposition(
+        &mut self,
+        connection: &RustConnection,
+        x: i16,
+        y: i16,
+        width: u16,
+    ) -> Result<(), X11Error> {
+        self.x_offset = x;
+        self.y_offset = y;
+        self.width = width;
+
+        connection.configure_window(
+            self.window,
+            &ConfigureWindowAux::new()
+                .x(x as i32)
+                .y(y as i32)
+                .width(width as u32),
+        )?;
+
+        unsafe {
+            x11::xlib::XFreePixmap(self.display, self.pixmap);
+        }
+
+        let depth = unsafe { x11::xlib::XDefaultDepth(self.display, 0) };
+        self.pixmap = unsafe {
+            x11::xlib::XCreatePixmap(
+                self.display,
+                self.window as x11::xlib::Drawable,
+                width as u32,
+                self.height as u32,
+                depth as u32,
+            )
+        };
+
+        let visual = unsafe { x11::xlib::XDefaultVisual(self.display, 0) };
+        let colormap = unsafe { x11::xlib::XDefaultColormap(self.display, 0) };
+        self.font_draw = FontDraw::new(self.display, self.pixmap, visual, colormap)?;
+
+        connection.flush()?;
+        Ok(())
+    }
+
+    pub fn hide(&self, connection: &RustConnection) -> Result<(), X11Error> {
+        connection.unmap_window(self.window)?;
+        connection.flush()?;
+        Ok(())
+    }
+
+    pub fn show(&self, connection: &RustConnection) -> Result<(), X11Error> {
+        connection.map_window(self.window)?;
+        connection.flush()?;
+        Ok(())
+    }
+}
+
+impl Drop for TabBar {
+    fn drop(&mut self) {
+        unsafe {
+            x11::xlib::XFreePixmap(self.display, self.pixmap);
+        }
+    }
+}
diff --git a/src/window_manager.rs b/src/window_manager.rs
index cfe5114..82a704f 100644
--- a/src/window_manager.rs
+++ b/src/window_manager.rs
@@ -107,6 +107,7 @@ pub struct WindowManager {
     fullscreen_windows: HashSet<Window>,
     floating_geometry_before_fullscreen: std::collections::HashMap<Window, (i16, i16, u16, u16, u16)>,
     bars: Vec<Bar>,
+    tab_bars: Vec<crate::tab_bar::TabBar>,
     show_bar: bool,
     last_layout: Option<&'static str>,
     monitors: Vec<Monitor>,
@@ -202,6 +203,24 @@ impl WindowManager {
             bars.push(bar);
         }
 
+        let bar_height = font.height() as f32 * 1.4;
+        let mut tab_bars = Vec::new();
+        for monitor in monitors.iter() {
+            let tab_bar = crate::tab_bar::TabBar::new(
+                &connection,
+                &screen,
+                screen_number,
+                display,
+                &font,
+                (monitor.x + config.gap_outer_horizontal as i32) as i16,
+                (monitor.y as f32 + bar_height + config.gap_outer_vertical as f32) as i16,
+                monitor.width.saturating_sub(2 * config.gap_outer_horizontal) as u16,
+                config.scheme_occupied,
+                config.scheme_selected,
+            )?;
+            tab_bars.push(tab_bar);
+        }
+
         let gaps_enabled = config.gaps_enabled;
 
         let atoms = AtomCache::new(&connection)?;
@@ -234,6 +253,7 @@ impl WindowManager {
             fullscreen_windows: HashSet::new(),
             floating_geometry_before_fullscreen: std::collections::HashMap::new(),
             bars,
+            tab_bars,
             show_bar: true,
             last_layout: None,
             monitors,
@@ -248,6 +268,10 @@ impl WindowManager {
             keybind_overlay,
         };
 
+        for tab_bar in &window_manager.tab_bars {
+            tab_bar.hide(&window_manager.connection)?;
+        }
+
         window_manager.scan_existing_windows()?;
         window_manager.update_bar()?;
         window_manager.run_autostart_commands()?;
@@ -904,6 +928,45 @@ impl WindowManager {
         Ok(())
     }
 
+    fn update_tab_bars(&mut self) -> WmResult<()> {
+        for (monitor_index, monitor) in self.monitors.iter().enumerate() {
+            if let Some(tab_bar) = self.tab_bars.get_mut(monitor_index) {
+                let visible_windows: Vec<Window> = self
+                    .windows
+                    .iter()
+                    .filter(|&&window| {
+                        let window_monitor_index = self.window_monitor.get(&window).copied().unwrap_or(0);
+                        if window_monitor_index != monitor_index {
+                            return false;
+                        }
+                        if self.floating_windows.contains(&window) {
+                            return false;
+                        }
+                        if self.fullscreen_windows.contains(&window) {
+                            return false;
+                        }
+                        if let Some(&tags) = self.window_tags.get(&window) {
+                            (tags & monitor.selected_tags) != 0
+                        } else {
+                            false
+                        }
+                    })
+                    .copied()
+                    .collect();
+
+                let focused_window = monitor.focused_window;
+
+                tab_bar.draw(
+                    &self.connection,
+                    &self.font,
+                    &visible_windows,
+                    focused_window,
+                )?;
+            }
+        }
+        Ok(())
+    }
+
     fn handle_key_action(&mut self, action: KeyAction, arg: &Arg) -> WmResult<()> {
         match action {
             KeyAction::Spawn => handlers::handle_spawn_action(action, arg)?,
@@ -1223,7 +1286,20 @@ impl WindowManager {
             visible[0]
         };
 
+        let is_tabbed = self.layout.name() == "tabbed";
+        if is_tabbed {
+            self.connection.configure_window(
+                next_window,
+                &ConfigureWindowAux::new().stack_mode(StackMode::ABOVE),
+            )?;
+        }
+
         self.set_focus(next_window)?;
+
+        if is_tabbed {
+            self.update_tab_bars()?;
+        }
+
         Ok(())
     }
 
@@ -1693,6 +1769,11 @@ impl WindowManager {
 
         self.update_focus_visuals(old_focused, window)?;
         self.previous_focused = Some(window);
+
+        if self.layout.name() == "tabbed" {
+            self.update_tab_bars()?;
+        }
+
         Ok(())
     }
 
@@ -2001,6 +2082,10 @@ impl WindowManager {
                 self.apply_layout()?;
                 self.update_bar()?;
                 self.set_focus(event.window)?;
+
+                if self.layout.name() == "tabbed" {
+                    self.update_tab_bars()?;
+                }
             }
             Event::UnmapNotify(event) => {
                 if self.windows.contains(&event.window) && self.is_window_visible(event.window) {
@@ -2123,13 +2208,58 @@ impl WindowManager {
                         }
                         self.view_tag(tag_index)?;
                     }
-                } else if event.child != x11rb::NONE {
-                    self.set_focus(event.child)?;
+                } else {
+                    let is_tab_bar_click = self
+                        .tab_bars
+                        .iter()
+                        .enumerate()
+                        .find(|(_, tab_bar)| tab_bar.window() == event.event);
+
+                    if let Some((monitor_index, tab_bar)) = is_tab_bar_click {
+                        if monitor_index != self.selected_monitor {
+                            self.selected_monitor = monitor_index;
+                        }
 
-                    if event.detail == ButtonIndex::M1.into() {
-                        self.move_mouse(event.child)?;
-                    } else if event.detail == ButtonIndex::M3.into() {
-                        self.resize_mouse(event.child)?;
+                        let visible_windows: Vec<Window> = self
+                            .windows
+                            .iter()
+                            .filter(|&&window| {
+                                let window_monitor_index = self.window_monitor.get(&window).copied().unwrap_or(0);
+                                if window_monitor_index != monitor_index {
+                                    return false;
+                                }
+                                if self.floating_windows.contains(&window) {
+                                    return false;
+                                }
+                                if self.fullscreen_windows.contains(&window) {
+                                    return false;
+                                }
+                                let monitor_tags = self.monitors.get(monitor_index).map(|m| m.selected_tags).unwrap_or(0);
+                                if let Some(&tags) = self.window_tags.get(&window) {
+                                    (tags & monitor_tags) != 0
+                                } else {
+                                    false
+                                }
+                            })
+                            .copied()
+                            .collect();
+
+                        if let Some(clicked_window) = tab_bar.get_clicked_window(&visible_windows, event.event_x) {
+                            self.connection.configure_window(
+                                clicked_window,
+                                &ConfigureWindowAux::new().stack_mode(StackMode::ABOVE),
+                            )?;
+                            self.set_focus(clicked_window)?;
+                            self.update_tab_bars()?;
+                        }
+                    } else if event.child != x11rb::NONE {
+                        self.set_focus(event.child)?;
+
+                        if event.detail == ButtonIndex::M1.into() {
+                            self.move_mouse(event.child)?;
+                        } else if event.detail == ButtonIndex::M3.into() {
+                            self.resize_mouse(event.child)?;
+                        }
                     }
                 }
             }
@@ -2141,6 +2271,12 @@ impl WindowManager {
                         break;
                     }
                 }
+                for _tab_bar in &self.tab_bars {
+                    if event.window == _tab_bar.window() {
+                        self.update_tab_bars()?;
+                        break;
+                    }
+                }
             }
             Event::ClientMessage(event) => {
                 if event.type_ == self.atoms.net_wm_state {
@@ -2260,6 +2396,86 @@ impl WindowManager {
         }
 
         self.connection.flush()?;
+
+        let is_tabbed = self.layout.name() == LayoutType::Tabbed.as_str();
+
+        if is_tabbed {
+            let outer_horizontal = if self.gaps_enabled {
+                self.config.gap_outer_horizontal
+            } else {
+                0
+            };
+            let outer_vertical = if self.gaps_enabled {
+                self.config.gap_outer_vertical
+            } else {
+                0
+            };
+
+            for monitor_index in 0..self.tab_bars.len() {
+                if let Some(monitor) = self.monitors.get(monitor_index) {
+                    let bar_height = if self.show_bar {
+                        self.bars
+                            .get(monitor_index)
+                            .map(|bar| bar.height() as f32)
+                            .unwrap_or(0.0)
+                    } else {
+                        0.0
+                    };
+
+                    let tab_bar_x = (monitor.x + outer_horizontal as i32) as i16;
+                    let tab_bar_y = (monitor.y as f32 + bar_height + outer_vertical as f32) as i16;
+                    let tab_bar_width = monitor.width.saturating_sub(2 * outer_horizontal) as u16;
+
+                    if let Err(e) = self.tab_bars[monitor_index].reposition(
+                        &self.connection,
+                        tab_bar_x,
+                        tab_bar_y,
+                        tab_bar_width,
+                    ) {
+                        eprintln!("Failed to reposition tab bar: {:?}", e);
+                    }
+                }
+            }
+        }
+
+        for monitor_index in 0..self.tab_bars.len() {
+            let has_visible_windows = self
+                .windows
+                .iter()
+                .any(|&window| {
+                    let window_monitor_index = self.window_monitor.get(&window).copied().unwrap_or(0);
+                    if window_monitor_index != monitor_index {
+                        return false;
+                    }
+                    if self.floating_windows.contains(&window) {
+                        return false;
+                    }
+                    if self.fullscreen_windows.contains(&window) {
+                        return false;
+                    }
+                    if let Some(monitor) = self.monitors.get(monitor_index) {
+                        if let Some(&tags) = self.window_tags.get(&window) {
+                            return (tags & monitor.selected_tags) != 0;
+                        }
+                    }
+                    false
+                });
+
+            if is_tabbed && has_visible_windows {
+                if let Err(e) = self.tab_bars[monitor_index].show(&self.connection) {
+                    eprintln!("Failed to show tab bar: {:?}", e);
+                }
+            } else {
+                if let Err(e) = self.tab_bars[monitor_index].hide(&self.connection) {
+                    eprintln!("Failed to hide tab bar: {:?}", e);
+                }
+            }
+        }
+
+        if is_tabbed {
+            self.update_tab_bars()?;
+        }
+
         Ok(())
     }
 
diff --git a/templates/config.lua b/templates/config.lua
index 1f5cf95..abb67d0 100644
--- a/templates/config.lua
+++ b/templates/config.lua
@@ -40,10 +40,60 @@ local colors = {
 
 -- Workspace tags - can be numbers, names, or icons (requires a Nerd Font)
 local tags = { "1", "2", "3", "4", "5", "6", "7", "8", "9" }
+-- local tags = { "", "󰊯", "", "", "󰙯", "󱇤", "", "󱘶", "󰧮" } -- Example of nerd font icon tags
 
 -- Font for the status bar (use "fc-list" to see available fonts)
 local bar_font = "monospace:style=Bold:size=10"
 
+-- Define your blocks
+-- Similar to widgets in qtile, or dwmblocks
+local blocks = {
+    oxwm.bar.block.ram({
+        format = "Ram: {used}/{total} GB",
+        interval = 5,
+        color = colors.light_blue,
+        underline = true,
+    }),
+    oxwm.bar.block.static({
+        format = "{}",
+        text = " │  ",
+        interval = 999999999,
+        color = colors.lavender,
+        underline = false,
+    }),
+    oxwm.bar.block.shell({
+        format = "Kernel: {}",
+        command = "uname -r",
+        interval = 999999999,
+        color = colors.red,
+        underline = true,
+    }),
+    oxwm.bar.block.static({
+        format = "{}",
+        text = " │  ",
+        interval = 999999999,
+        color = colors.lavender,
+        underline = false,
+    }),
+    oxwm.bar.block.datetime({
+        format = "{}",
+        date_format = "%a, %b %d - %-I:%M %P",
+        interval = 1,
+        color = colors.cyan,
+        underline = true,
+    }),
+    -- Uncomment to add battery status (useful for laptops)
+    -- oxwm.bar.block.battery({
+    --     format = "Bat: {}%",
+    --     charging = "⚡ Bat: {}%",
+    --     discharging = "- Bat: {}%",
+    --     full = "✓ Bat: {}%",
+    --     interval = 30,
+    --     color = colors.green,
+    --     underline = true,
+    -- }),
+};
+
 -------------------------------------------------------------------------------
 -- Basic Settings
 -------------------------------------------------------------------------------
@@ -55,9 +105,10 @@ oxwm.set_tags(tags)
 -- Layouts
 -------------------------------------------------------------------------------
 -- Set custom symbols for layouts (displayed in the status bar)
--- Available layouts: "tiling" (master-stack), "normie" (floating)
+-- Available layouts: "tiling", "normie" (floating), "grid", "monocle", "tabbed"
 oxwm.set_layout_symbol("tiling", "[T]")
 oxwm.set_layout_symbol("normie", "[F]")
+oxwm.set_layout_symbol("tabbed", "[=]")
 
 -------------------------------------------------------------------------------
 -- Appearance
@@ -78,6 +129,9 @@ oxwm.gaps.set_outer(5, 5)   -- Outer gaps (horizontal, vertical) in pixels
 -- Font configuration
 oxwm.bar.set_font(bar_font)
 
+-- Set your blocks here (defined above)
+oxwm.bar.set_blocks(blocks)
+
 -- Bar color schemes (for workspace tag display)
 -- Parameters: foreground, background, border
 oxwm.bar.set_scheme_normal(colors.fg, colors.bg, "#444444")         -- Unoccupied tags
@@ -96,7 +150,7 @@ oxwm.bar.set_scheme_selected(colors.cyan, colors.bg, colors.purple) -- Currently
 -- Common keys: Return, Space, Tab, Escape, Backspace, Delete, Left, Right, Up, Down
 
 -- Basic window management
-oxwm.key.bind({ modkey }, "Return", oxwm.spawn(terminal))                                                       -- Spawn terminal
+oxwm.key.bind({ modkey }, "Return", oxwm.spawn_terminal())                                                      -- Spawn terminal
 oxwm.key.bind({ modkey }, "D", oxwm.spawn({ "sh", "-c", "dmenu_run -l 10" }))                                   -- Application launcher
 oxwm.key.bind({ modkey }, "S", oxwm.spawn({ "sh", "-c", "maim -s | xclip -selection clipboard -t image/png" })) -- Screenshot selection
 oxwm.key.bind({ modkey }, "Q", oxwm.client.kill())                                                              -- Close focused window
@@ -168,7 +222,7 @@ oxwm.key.bind({ modkey, "Shift" }, "L", oxwm.client.swap_direction("right")) --
 oxwm.key.chord({
     { { modkey }, "Space" },
     { {},         "T" }
-}, oxwm.spawn(terminal))
+}, oxwm.spawn_terminal())
 
 -------------------------------------------------------------------------------
 -- Status Bar Blocks
diff --git a/templates/oxwm.lua b/templates/oxwm.lua
index 811f47d..d7023d1 100644
--- a/templates/oxwm.lua
+++ b/templates/oxwm.lua
@@ -38,8 +38,8 @@ function oxwm.set_modkey(modkey) end
 function oxwm.set_tags(tags) end
 
 ---Set layout symbol override
----@param name string Layout name (e.g., "tiling", "normie")
----@param symbol string Symbol to display (e.g., "[T]", "[F]")
+---@param name string Layout name (e.g., "tiling", "normie", "tabbed", "grid", "monocle")
+---@param symbol string Symbol to display (e.g., "[T]", "[F]", "[=]")
 function oxwm.set_layout_symbol(name, symbol) end
 
 ---Quit the window manager
@@ -171,7 +171,7 @@ oxwm.layout = {}
 function oxwm.layout.cycle() end
 
 ---Set specific layout
----@param name string Layout name (e.g., "tiling", "normie")
+---@param name string Layout name (e.g., "tiling", "normie", "tabbed", "grid", "monocle")
 ---@return table Action table for keybinding
 function oxwm.layout.set(name) end