Diff
diff --git a/readme.org b/readme.org
index cad6ea9..90f0d7f 100644
--- a/readme.org
+++ b/readme.org
@@ -6,9 +6,6 @@ A dynamic window manager written in Rust, inspired by dwm but designed to evolve
on its own. Configuration is done in Rust source code, keeping with the suckless
philosophy of *"edit + recompile."*
-This project is still in its early stages. Currently, it can claim ownership of
-an X display and log incoming events.
-
* Project Structure
#+begin_src sh
@@ -22,34 +19,58 @@ src/
│ │ ├── connection: RustConnection [X11 connection]
│ │ ├── windows: Vec<Window> [All managed windows]
│ │ ├── focused_window: Option<Window>
-│ │ └── layout: Box<dyn Layout>
+│ │ ├── layout: Box<dyn Layout>
+│ │ ├── window_tags: HashMap<Window, TagMask>
+│ │ ├── selected_tags: TagMask
+│ │ └── bar: Bar [Status bar]
│ │
-│ ├── new() [Initialize WM, grab root]
+│ ├── new() [Initialize WM, grab root, create bar]
│ ├── run() [Main event loop]
│ ├── handle_event() [Route X11 events]
-│ │ ├── MapRequest → add window, apply layout
-│ │ ├── UnmapNotify → remove window
-│ │ ├── DestroyNotify → remove window
-│ │ └── KeyPress → get action, handle it
+│ │ ├── MapRequest → add window, apply layout, update bar
+│ │ ├── UnmapNotify → remove window, update bar
+│ │ ├── DestroyNotify → remove window, update bar
+│ │ ├── KeyPress → get action, handle it
+│ │ ├── ButtonPress → handle bar clicks
+│ │ └── Expose → redraw bar
│ ├── handle_key_action() [Execute keyboard actions]
│ ├── remove_window() [Remove from Vec, reapply layout]
│ ├── set_focus() [Focus window, update visuals]
-│ ├── cycle_focus() [Move focus to next window]
+│ ├── cycle_focus() [Move focus to next/prev window]
+│ ├── view_tag() [Switch to tag/workspace]
+│ ├── move_to_tag() [Move window to tag]
+│ ├── update_bar() [Calculate occupied tags, redraw bar]
│ ├── update_focus_visuals() [Set border colors]
-│ └── apply_layout() [Position all windows]
+│ └── apply_layout() [Position all windows below bar]
+│
+├── config.rs [CONFIGURATION - all settings here]
+│ ├── BORDER_WIDTH, BORDER_FOCUSED, BORDER_UNFOCUSED
+│ ├── TAG_COUNT, TAGS [Workspace configuration]
+│ ├── TERMINAL, MODKEY
+│ └── KEYBINDINGS [All keybinds as const array]
+│
+├── bar/
+│ ├── mod.rs [Re-exports, constants]
+│ └── bar.rs
+│ ├── struct Bar [Status bar window]
+│ ├── new() [Create bar X11 window]
+│ ├── draw() [Render tags with state indicators]
+│ ├── handle_click() [Detect which tag was clicked]
+│ └── invalidate() [Mark bar as needing redraw]
│
├── keyboard/
│ ├── mod.rs [Re-exports]
│ ├── keycodes.rs [Key constants: Q, J, RETURN, etc]
│ └── handlers.rs
-│ ├── enum KeyAction [SpawnTerminal, CloseWindow, CycleWindow, Quit, None]
+│ ├── enum KeyAction [Spawn, KillClient, FocusStack, ViewTag, etc]
+│ ├── enum Arg [None, Int, Str, Array]
│ ├── setup_keybinds() [Register keys with X11]
│ └── handle_key_press() [Parse KeyPressEvent → KeyAction]
│
└── layout/
- ├── mod.rs [Layout trait definition]
- └── tiling.rs
- └── TilingLayout::arrange() [Calculate window positions]
+ ├── mod.rs [Layout trait definition]
+ └── tiling.rs
+ └── TilingLayout::arrange() [Calculate window positions]
#+end_src
* Event Flow
@@ -59,19 +80,27 @@ src/
3. For KeyPress:
- keyboard::handle_key_press() → KeyAction
- handle_key_action() executes action
+ - update_bar() if tags/windows changed
4. For Map/Unmap:
- - Modify windows Vec
- - apply_layout() repositions everything
+ - Modify windows Vec and window_tags HashMap
+ - apply_layout() repositions everything (accounting for bar)
+ - update_bar() shows occupied tags
- update_focus_visuals() redraws borders
+5. For ButtonPress on bar:
+ - bar.handle_click() determines clicked tag
+ - view_tag() switches workspace
* Key Bindings
-| Binding | Action |
-|---------------+----------------------|
-| Alt+Return | Spawn terminal |
-| Alt+J | Cycle focus |
-| Alt+Shift+Q | Close focused window |
-| Alt+Shift+Q | Quit WM |
+| Binding | Action |
+|-----------------+-------------------------|
+| Alt+Return | Spawn terminal |
+| Alt+J/K | Cycle focus down/up |
+| Alt+Q | Kill focused window |
+| Alt+Shift+Q | Quit WM |
+| Alt+1-9 | View tag 1-9 |
+| Alt+Shift+1-9 | Move window to tag 1-9 |
+| Alt+S | Screenshot (maim) |
* ⚙ Installation — Running with Nix Flakes
You can set up a reproducible development environment with Rust, Cargo, Xephyr, xterm, and
@@ -100,55 +129,97 @@ just test
This should open a new Xephyr window. oxwm will attach to it and log X11
events in your host terminal. Clients like xterm/xclock will appear inside Xephyr.
-* OXWM Todo List:
-** TODO Reorganization Tasks [1/2]
-- [X] Move keyboard module to folder structure:
- - [X] Create =src/keyboard/mod.rs= with re-exports
- - [X] Move constants to =src/keyboard/keycodes.rs=
- - [X] Move key handlers to =src/keyboard/handlers.rs=
- - [X] Update imports in main.rs and window_manager.rs
-- [ ] Create =src/config.rs= in root directory for future configuration system
-
-** TODO Core Window Management [1/2]
+* Current Status
+** Working Features
+- ✓ X11 event handling and window management
+- ✓ Tag system (9 workspaces) with keyboard switching
+- ✓ Window focus cycling (Alt+J/K)
+- ✓ Tiling layout with border indicators
+- ✓ Status bar showing tags
+ - Visual indicators: selected (white), occupied (gray line), empty (dim)
+ - Click-to-switch tags
+ - Performance-optimized redrawing
+- ✓ Basic keybindings (spawn, kill, focus, tags)
+- ✓ Configuration via Rust constants in config.rs
+
+** Immediate Next Steps
+- [ ] Status text in bar (date, time, system info)
+- [ ] dmenu integration for application launcher
+- [ ] Additional widgets (clock, battery, etc.)
+
+** Long Term Roadmap
+- [ ] Multi-monitor support
+- [ ] Additional layouts (monocle, floating, etc.)
+- [ ] Per-window floating behavior
+- [ ] Per-program rules (auto-tag assignment, floating rules)
+- [ ] Master area resizing
+- [ ] Window swapping in layout
+- [ ] Configurable gaps between windows
+- [ ] External bar support (polybar, lemonbar, etc.)
+
+* OXWM Development Todo
+** DONE Core Window Management [2/2]
- [X] Fix layout after program is closed (handle UnmapNotify events)
- [X] Add UnmapNotify to event handling
- [X] Remove closed windows from windows vector
- [X] Re-apply layout after window removal
-- [ ] Add keybind to swap focus between windows
- - [ ] Track focused window in WindowManager struct
- - [ ] Implement focus cycling logic
- - [ ] Add visual focus indication (borders/colors)
-
-** Key System Improvements
-- [ ] Connect config.rs to keyboard system for dynamic keybind generation
-- [ ] Add more dwm-like keybinds:
- - [ ] Window focus switching (Alt+J/K)
- - [ ] Master area resizing
- - [ ] Layout switching
- - [ ] Workspace/tag management
-- [ ] Better error handling for failed key grabs
-
-** Layout System
-- [ ] Add more layout types (monocle, floating)
+- [X] Add keybind to swap focus between windows
+ - [X] Track focused window in WindowManager struct
+ - [X] Implement focus cycling logic
+ - [X] Add visual focus indication (borders/colors)
+
+** DONE Tag System [3/3]
+- [X] Implement tag/workspace system (9 tags)
+- [X] Keybinds to switch tags (Alt+1-9)
+- [X] Keybinds to move windows to tags (Alt+Shift+1-9)
+
+** DONE Status Bar [2/2]
+- [X] Create basic bar window at screen top
+- [X] Display tag indicators with state (selected/occupied/empty)
+
+** IN PROGRESS Bar Enhancements [0/3]
+- [ ] Add status text area (right side of bar)
+- [ ] Implement clock widget
+- [ ] Add system information widgets
+
+** TODO Key System Improvements [0/2]
+- [ ] dmenu integration for application launching
+- [ ] More spawn commands in config (screenshot, volume, etc.)
+
+** TODO Layout System [0/4]
+- [ ] Add monocle layout
+- [ ] Add floating layout mode
- [ ] Handle window resize requests properly
- [ ] Add configurable gaps between windows
-- [ ] Implement layout switching keybinds
+
+** TODO Advanced Features [0/3]
+- [ ] Multi-monitor support
+- [ ] Per-window rules (floating, tag assignment)
+- [ ] Master area resizing keybinds
** Polish & Features
- [ ] Clean window destruction/cleanup
- [ ] Handle edge cases (empty window list, invalid windows)
-- [ ] Add status bar integration
- [ ] Better error messages and logging
-
-** Priority
-Reorganization and UnmapNotify handling should be immediate priorities.
-
-* Status
-- Rust + x11rb skeleton running
-- Nix flake devShell available
-- =just test= launches Xephyr, clients, and oxwm
+- [ ] Proper font rendering in bar (currently using basic X11 text)
+
+* Architecture Notes
+** Tag System
+Tags are implemented as bitmasks (TagMask = u32), allowing windows to belong to
+multiple tags simultaneously (though current UI only supports single tags).
+Each window has an associated TagMask in window_tags HashMap.
+
+** Bar Design
+The bar uses a performance-optimized approach:
+- Only redraws when invalidate() is called
+- Pre-calculates tag widths on creation
+- Uses X11 graphics context for efficient drawing
+- Click handling uses O(n) tag width lookup
+
+** Configuration Philosophy
+Following dwm's approach: all configuration is in Rust source code. No runtime
+config files. Edit config.rs and recompile. This ensures type safety and
+compile-time validation of all settings.
* License
[[https://www.gnu.org/licenses/gpl-3.0.en.html][GPL]]
-
-
diff --git a/src/bar/bar.rs b/src/bar/bar.rs
new file mode 100644
index 0000000..05970c2
--- /dev/null
+++ b/src/bar/bar.rs
@@ -0,0 +1,185 @@
+use super::BAR_HEIGHT;
+use crate::config::TAGS;
+use anyhow::Result;
+use x11rb::COPY_DEPTH_FROM_PARENT;
+use x11rb::connection::Connection;
+use x11rb::protocol::xproto::*;
+
+pub struct Bar {
+ window: Window,
+ width: u16,
+ height: u16,
+ graphics_context: Gcontext,
+
+ tag_widths: Vec<u16>,
+ needs_redraw: bool,
+}
+
+impl Bar {
+ pub fn new<C: Connection>(connection: &C, screen: &Screen) -> Result<Self> {
+ let window = connection.generate_id()?;
+ let graphics_context = connection.generate_id()?;
+
+ let width = screen.width_in_pixels;
+ let height = BAR_HEIGHT;
+
+ connection.create_window(
+ COPY_DEPTH_FROM_PARENT,
+ window,
+ screen.root,
+ 0,
+ 0,
+ width,
+ height,
+ 0,
+ WindowClass::INPUT_OUTPUT,
+ screen.root_visual,
+ &CreateWindowAux::new()
+ .background_pixel(screen.black_pixel)
+ .event_mask(EventMask::EXPOSURE | EventMask::BUTTON_PRESS)
+ .override_redirect(1),
+ )?;
+
+ connection.create_gc(
+ graphics_context,
+ window,
+ &CreateGCAux::new()
+ .foreground(screen.white_pixel)
+ .background(screen.black_pixel),
+ )?;
+
+ connection.map_window(window)?;
+ connection.flush()?;
+
+ // TODO: actual text width calculation when we add fonts
+ let tag_widths = TAGS.iter().map(|tag| (tag.len() as u16 * 8) + 10).collect();
+
+ Ok(Bar {
+ window,
+ width,
+ height,
+ graphics_context,
+ tag_widths,
+ needs_redraw: true,
+ })
+ }
+
+ pub fn window(&self) -> Window {
+ self.window
+ }
+
+ pub fn height(&self) -> u16 {
+ self.height
+ }
+
+ pub fn invalidate(&mut self) {
+ self.needs_redraw = true;
+ }
+
+ pub fn draw<C: Connection>(
+ &mut self,
+ connection: &C,
+ current_tags: u32,
+ occupied_tags: u32,
+ ) -> Result<()> {
+ if !self.needs_redraw {
+ return Ok(());
+ }
+
+ connection.poly_fill_rectangle(
+ self.window,
+ self.graphics_context,
+ &[Rectangle {
+ x: 0,
+ y: 0,
+ width: self.width,
+ height: self.height,
+ }],
+ )?;
+
+ let mut x_position: i16 = 0;
+
+ for (tag_index, tag) in TAGS.iter().enumerate() {
+ let tag_mask = 1 << tag_index;
+ let is_selected = (current_tags & tag_mask) != 0;
+ let is_occupied = (occupied_tags & tag_mask) != 0;
+
+ let tag_width = self.tag_widths[tag_index];
+
+ if is_selected {
+ connection.change_gc(
+ self.graphics_context,
+ &ChangeGCAux::new().foreground(0xFFFFFF),
+ )?;
+ connection.poly_fill_rectangle(
+ self.window,
+ self.graphics_context,
+ &[Rectangle {
+ x: x_position,
+ y: 0,
+ width: tag_width,
+ height: self.height,
+ }],
+ )?;
+
+ connection.change_gc(
+ self.graphics_context,
+ &ChangeGCAux::new().foreground(0x000000),
+ )?;
+ } else if is_occupied {
+ connection.change_gc(
+ self.graphics_context,
+ &ChangeGCAux::new().foreground(0x888888),
+ )?;
+ connection.poly_fill_rectangle(
+ self.window,
+ self.graphics_context,
+ &[Rectangle {
+ x: x_position,
+ y: 0,
+ width: tag_width,
+ height: 2,
+ }],
+ )?;
+
+ connection.change_gc(
+ self.graphics_context,
+ &ChangeGCAux::new().foreground(0xFFFFFF),
+ )?;
+ } else {
+ connection.change_gc(
+ self.graphics_context,
+ &ChangeGCAux::new().foreground(0x666666),
+ )?;
+ }
+
+ // TODO: Replace with actual font rendering later
+ connection.image_text8(
+ self.window,
+ self.graphics_context,
+ x_position + 5,
+ self.height as i16 - 5,
+ tag.as_bytes(),
+ )?;
+
+ x_position += tag_width as i16;
+ }
+
+ connection.flush()?;
+ self.needs_redraw = false;
+
+ Ok(())
+ }
+
+ pub fn handle_click(&self, click_x: i16) -> Option<usize> {
+ let mut current_x_position = 0;
+
+ for (tag_index, &tag_width) in self.tag_widths.iter().enumerate() {
+ if click_x >= current_x_position && click_x < current_x_position + tag_width as i16 {
+ return Some(tag_index);
+ }
+ current_x_position += tag_width as i16;
+ }
+ None
+ }
+}
diff --git a/src/bar/mod.rs b/src/bar/mod.rs
new file mode 100644
index 0000000..bac11c9
--- /dev/null
+++ b/src/bar/mod.rs
@@ -0,0 +1,14 @@
+mod bar;
+// mod widgets; // TODO: implement later
+
+pub use bar::Bar;
+
+// TODO: this should live in config.rs
+pub const BAR_HEIGHT: u16 = 25;
+
+// Bar position (for future use)
+#[derive(Debug, Clone, Copy)]
+pub enum BarPosition {
+ Top,
+ Bottom,
+}
diff --git a/src/config.rs b/src/config.rs
index 146405b..a09609b 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -15,10 +15,20 @@ pub const BORDER_UNFOCUSED: u32 = 0x888888;
pub const TERMINAL: &str = "alacritty";
pub const MODKEY: KeyButMask = KeyButMask::MOD1;
+// ========================================
+// Commands
+// ========================================
+const SCREENSHOT_CMD: &[&str] = &[
+ "sh",
+ "-c",
+ "maim ~/screenshots/screenshot_$(date +%Y%m%d_%H%M%S).png",
+];
+
// ========================================
// TAGS
// ========================================
pub const TAG_COUNT: usize = 9;
+pub const TAGS: [&str; TAG_COUNT] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
// ========================================
// KEYBINDINGS
@@ -26,7 +36,8 @@ pub const TAG_COUNT: usize = 9;
#[rustfmt::skip]
pub const KEYBINDINGS: &[Key] = &[
Key::new(&[MODKEY], keycodes::RETURN, KeyAction::Spawn, Arg::Str(TERMINAL)),
-
+
+ Key::new(&[MODKEY], keycodes::S, KeyAction::Spawn, Arg::Array(SCREENSHOT_CMD)),
Key::new(&[MODKEY], keycodes::Q, KeyAction::KillClient, Arg::None),
Key::new(&[MODKEY, SHIFT], keycodes::Q, KeyAction::Quit, Arg::None),
Key::new(&[MODKEY], keycodes::J, KeyAction::FocusStack, Arg::Int(-1)),
diff --git a/src/keyboard/handlers.rs b/src/keyboard/handlers.rs
index 0dbeba1..294c724 100644
--- a/src/keyboard/handlers.rs
+++ b/src/keyboard/handlers.rs
@@ -16,9 +16,10 @@ pub enum KeyAction {
#[derive(Debug)]
pub enum Arg {
- Str(&'static str),
- Int(i32),
None,
+ Int(i32),
+ Str(&'static str),
+ Array(&'static [&'static str]),
}
pub struct Key {
diff --git a/src/main.rs b/src/main.rs
index b59dc4a..2b0cfa7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,4 +1,5 @@
use anyhow::Result;
+mod bar;
mod config;
mod keyboard;
mod layout;
diff --git a/src/window_manager.rs b/src/window_manager.rs
index 0e7ec61..b53fbca 100644
--- a/src/window_manager.rs
+++ b/src/window_manager.rs
@@ -1,3 +1,4 @@
+use crate::bar::Bar;
use crate::config::{BORDER_FOCUSED, BORDER_UNFOCUSED, BORDER_WIDTH, TAG_COUNT};
use crate::keyboard::{self, Arg, KeyAction};
use crate::layout::Layout;
@@ -24,6 +25,7 @@ pub struct WindowManager {
layout: Box<dyn Layout>,
window_tags: std::collections::HashMap<Window, TagMask>,
selected_tags: TagMask,
+ bar: Bar,
}
impl WindowManager {
@@ -44,6 +46,8 @@ impl WindowManager {
)?
.check()?;
+ let bar = Bar::new(&connection, &screen)?;
+
return Ok(Self {
connection,
screen_number,
@@ -54,6 +58,7 @@ impl WindowManager {
layout: Box::new(TilingLayout),
window_tags: std::collections::HashMap::new(),
selected_tags: tag_mask(0),
+ bar,
});
}
@@ -62,19 +67,40 @@ impl WindowManager {
keyboard::setup_keybinds(&self.connection, self.root)?;
+ // Initial bar draw
+ self.update_bar()?;
+
loop {
let event = self.connection.wait_for_event()?;
self.handle_event(event)?;
}
}
+ fn update_bar(&mut self) -> Result<()> {
+ let mut occupied_tags: TagMask = 0;
+ for &tags in self.window_tags.values() {
+ occupied_tags |= tags;
+ }
+
+ self.bar.invalidate();
+ self.bar
+ .draw(&self.connection, self.selected_tags, occupied_tags)?;
+ Ok(())
+ }
+
fn handle_key_action(&mut self, action: KeyAction, arg: &Arg) -> Result<()> {
match action {
- KeyAction::Spawn => {
- if let Arg::Str(command) = arg {
+ KeyAction::Spawn => match arg {
+ Arg::Str(command) => {
std::process::Command::new(command).spawn()?;
}
- }
+ Arg::Array(cmd) => {
+ if let Some((program, args)) = cmd.split_first() {
+ std::process::Command::new(program).args(args).spawn()?;
+ }
+ }
+ _ => {}
+ },
KeyAction::KillClient => {
if let Some(focused) = self.focused_window {
match self.connection.kill_client(focused) {
@@ -148,6 +174,7 @@ impl WindowManager {
self.selected_tags = tag_mask(tag_index);
self.update_window_visibility()?;
self.apply_layout()?;
+ self.update_bar()?; // Update bar to show new tag
let visible = self.visible_windows();
self.set_focus(visible.first().copied())?;
@@ -165,6 +192,7 @@ impl WindowManager {
self.window_tags.insert(focused, mask);
self.update_window_visibility()?;
self.apply_layout()?;
+ self.update_bar()?; // Update bar to show occupied tags changed
}
Ok(())
@@ -238,6 +266,7 @@ impl WindowManager {
self.windows.push(event.window);
self.window_tags.insert(event.window, self.selected_tags);
self.apply_layout()?;
+ self.update_bar()?;
self.set_focus(Some(event.window))?;
}
Event::UnmapNotify(event) => {
@@ -245,13 +274,6 @@ impl WindowManager {
self.remove_window(event.window)?;
}
}
- // Event::UnmapNotify(event) => {
- // if self.windows.contains(&event.window) {
- // if self.is_window_visible(event.window) {
- // self.remove_window(event.window)?;
- // }
- // }
- // }
Event::DestroyNotify(event) => {
if self.windows.contains(&event.window) {
self.remove_window(event.window)?;
@@ -261,6 +283,20 @@ impl WindowManager {
let (action, arg) = keyboard::handle_key_press(event)?;
self.handle_key_action(action, arg)?;
}
+ Event::ButtonPress(event) => {
+ // Check if click was on the bar
+ if event.event == self.bar.window() {
+ if let Some(tag_index) = self.bar.handle_click(event.event_x) {
+ self.view_tag(tag_index)?;
+ }
+ }
+ }
+ Event::Expose(event) => {
+ if event.window == self.bar.window() {
+ self.bar.invalidate();
+ self.update_bar()?;
+ }
+ }
_ => {}
}
Ok(())
@@ -271,18 +307,23 @@ impl WindowManager {
let screen_height = self.screen.height_in_pixels as u32;
let border_width = BORDER_WIDTH;
+ let bar_height = self.bar.height() as u32;
+ let usable_height = screen_height.saturating_sub(bar_height);
+
let visible = self.visible_windows();
- let geometries = self.layout.arrange(&visible, screen_width, screen_height);
+ let geometries = self.layout.arrange(&visible, screen_width, usable_height);
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);
+ let adjusted_y = geometry.y_coordinate + bar_height as i32;
+
self.connection.configure_window(
*window,
&ConfigureWindowAux::new()
.x(geometry.x_coordinate)
- .y(geometry.y_coordinate)
+ .y(adjusted_y)
.width(adjusted_width)
.height(adjusted_height),
)?;
@@ -307,6 +348,7 @@ impl WindowManager {
}
self.apply_layout()?;
+ self.update_bar()?;
}
Ok(())
}