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