Diff
diff --git a/Cargo.lock b/Cargo.lock
index 86cca37..7426a71 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -222,7 +222,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "oxwm"
-version = "0.2.0"
+version = "0.3.0"
dependencies = [
"anyhow",
"chrono",
diff --git a/src/keyboard/handlers.rs b/src/keyboard/handlers.rs
index 7389df5..bd1a447 100644
--- a/src/keyboard/handlers.rs
+++ b/src/keyboard/handlers.rs
@@ -19,6 +19,8 @@ pub enum KeyAction {
ToggleGaps,
ToggleFullScreen,
ToggleFloating,
+ ChangeLayout,
+ CycleLayout,
MoveToTag,
FocusMonitor,
None,
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index f8a9918..ab09ff8 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -1,3 +1,4 @@
+pub mod normie;
pub mod tiling;
use x11rb::protocol::xproto::Window;
@@ -9,6 +10,26 @@ pub struct GapConfig {
pub outer_vertical: u32,
}
+pub const TILING: &str = "tiling";
+pub const NORMIE: &str = "normie";
+pub const FLOATING: &str = "floating";
+
+pub fn layout_from_str(s: &str) -> Result<Box<dyn Layout>, String> {
+ match s.to_lowercase().as_str() {
+ TILING => Ok(Box::new(tiling::TilingLayout)),
+ NORMIE | FLOATING => Ok(Box::new(normie::NormieLayout)),
+ _ => Err(format!("Unknown layout: {}", s)),
+ }
+}
+
+pub fn next_layout(current_name: &str) -> &'static str {
+ match current_name {
+ TILING => NORMIE,
+ NORMIE => TILING,
+ _ => TILING,
+ }
+}
+
pub trait Layout {
fn arrange(
&self,
@@ -17,6 +38,7 @@ pub trait Layout {
screen_height: u32,
gaps: &GapConfig,
) -> Vec<WindowGeometry>;
+ fn name(&self) -> &'static str;
}
pub struct WindowGeometry {
diff --git a/src/layout/normie.rs b/src/layout/normie.rs
new file mode 100644
index 0000000..a7fd756
--- /dev/null
+++ b/src/layout/normie.rs
@@ -0,0 +1,43 @@
+use super::{GapConfig, Layout, WindowGeometry};
+use x11rb::protocol::xproto::Window;
+
+pub struct NormieLayout;
+
+impl Layout for NormieLayout {
+ fn name(&self) -> &'static str {
+ super::NORMIE
+ }
+
+ fn arrange(
+ &self,
+ windows: &[Window],
+ screen_width: u32,
+ screen_height: u32,
+ _gaps: &GapConfig,
+ ) -> Vec<WindowGeometry> {
+ // Floating layout: all windows open centered with a reasonable default size
+ // This mimics dwm's NULL layout behavior where windows float freely
+ const DEFAULT_WIDTH_RATIO: f32 = 0.6;
+ const DEFAULT_HEIGHT_RATIO: f32 = 0.6;
+
+ windows
+ .iter()
+ .map(|_| {
+ // Calculate default window dimensions (60% of screen)
+ let width = ((screen_width as f32) * DEFAULT_WIDTH_RATIO) as u32;
+ let height = ((screen_height as f32) * DEFAULT_HEIGHT_RATIO) as u32;
+
+ // Center the window on the screen
+ let x = ((screen_width - width) / 2) as i32;
+ let y = ((screen_height - height) / 2) as i32;
+
+ WindowGeometry {
+ x_coordinate: x,
+ y_coordinate: y,
+ width,
+ height,
+ }
+ })
+ .collect()
+ }
+}
diff --git a/src/layout/tiling.rs b/src/layout/tiling.rs
index 6bfef1f..a31589e 100644
--- a/src/layout/tiling.rs
+++ b/src/layout/tiling.rs
@@ -4,6 +4,10 @@ use x11rb::protocol::xproto::Window;
pub struct TilingLayout;
impl Layout for TilingLayout {
+ fn name(&self) -> &'static str {
+ super::TILING
+ }
+
fn arrange(
&self,
windows: &[Window],
diff --git a/src/lib.rs b/src/lib.rs
index e4dc293..d5086de 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -99,6 +99,7 @@ impl Default for Config {
]),
),
Key::new(vec![MODKEY], keycodes::Q, KeyAction::KillClient, Arg::None),
+ Key::new(vec![MODKEY], keycodes::N, KeyAction::CycleLayout, Arg::None),
Key::new(
vec![MODKEY, SHIFT],
keycodes::F,
diff --git a/src/monitor.rs b/src/monitor.rs
index 05602aa..b7de736 100644
--- a/src/monitor.rs
+++ b/src/monitor.rs
@@ -40,56 +40,70 @@ pub fn detect_monitors(
screen: &Screen,
_root: Window,
) -> WmResult<Vec<Monitor>> {
- let mut monitors = Vec::new();
-
- if let Ok(cookie) = connection.xinerama_is_active() {
- if let Ok(reply) = cookie.reply() {
- if reply.state != 0 {
- if let Ok(screens_cookie) = connection.xinerama_query_screens() {
- if let Ok(screens_reply) = screens_cookie.reply() {
- for screen_info in &screens_reply.screen_info {
- if screen_info.width == 0 || screen_info.height == 0 {
- continue;
- }
-
- let is_unique = !monitors.iter().any(|m: &Monitor| {
- m.x == screen_info.x_org as i32
- && m.y == screen_info.y_org as i32
- && m.width == screen_info.width as u32
- && m.height == screen_info.height as u32
- });
-
- if is_unique {
- monitors.push(Monitor::new(
- screen_info.x_org as i32,
- screen_info.y_org as i32,
- screen_info.width as u32,
- screen_info.height as u32,
- ));
- }
- }
- }
- }
+ let fallback_monitors = || {
+ vec![Monitor::new(
+ 0,
+ 0,
+ screen.width_in_pixels as u32,
+ screen.height_in_pixels as u32,
+ )]
+ };
+
+ let mut monitors = Vec::<Monitor>::new();
+
+ let xinerama_active = connection
+ .xinerama_is_active()
+ .ok()
+ .and_then(|cookie| cookie.reply().ok())
+ .map_or(false, |reply| reply.state != 0);
+
+ if xinerama_active {
+ let xinerama_cookie = match connection.xinerama_query_screens() {
+ Ok(cookie) => cookie,
+ Err(_) => return Ok(fallback_monitors()),
+ };
+
+ let xinerama_reply = match xinerama_cookie.reply() {
+ Ok(reply) => reply,
+ Err(_) => return Ok(fallback_monitors()),
+ };
+
+ for screen_info in &xinerama_reply.screen_info {
+ let has_valid_dimensions = screen_info.width > 0 && screen_info.height > 0;
+ if !has_valid_dimensions {
+ continue;
+ }
+
+ let x_position = screen_info.x_org as i32;
+ let y_position = screen_info.y_org as i32;
+ let width_in_pixels = screen_info.width as u32;
+ let height_in_pixels = screen_info.height as u32;
+
+ let is_duplicate_monitor = monitors.iter().any(|monitor| {
+ monitor.x == x_position
+ && monitor.y == y_position
+ && monitor.width == width_in_pixels
+ && monitor.height == height_in_pixels
+ });
+
+ if !is_duplicate_monitor {
+ monitors.push(Monitor::new(
+ x_position,
+ y_position,
+ width_in_pixels,
+ height_in_pixels,
+ ));
}
}
}
if monitors.is_empty() {
- monitors.push(Monitor::new(
- 0,
- 0,
- screen.width_in_pixels as u32,
- screen.height_in_pixels as u32,
- ));
+ monitors = fallback_monitors();
}
- monitors.sort_by(|a, b| {
- let y_cmp = a.y.cmp(&b.y);
- if y_cmp == std::cmp::Ordering::Equal {
- a.x.cmp(&b.x)
- } else {
- y_cmp
- }
+ monitors.sort_by(|a, b| match a.y.cmp(&b.y) {
+ std::cmp::Ordering::Equal => a.x.cmp(&b.x),
+ other => other,
});
Ok(monitors)
diff --git a/src/window_manager.rs b/src/window_manager.rs
index 3e96ccb..10f0bf3 100644
--- a/src/window_manager.rs
+++ b/src/window_manager.rs
@@ -3,8 +3,8 @@ use crate::bar::Bar;
use crate::errors::WmError;
use crate::keyboard::{self, Arg, KeyAction, handlers};
use crate::layout::GapConfig;
-use crate::layout::Layout;
use crate::layout::tiling::TilingLayout;
+use crate::layout::{Layout, layout_from_str, next_layout};
use crate::monitor::{Monitor, detect_monitors};
use std::collections::HashSet;
use x11rb::cursor::Handle as CursorHandle;
@@ -37,10 +37,7 @@ impl AtomCache {
.reply()?
.atom;
- let wm_state = connection
- .intern_atom(false, b"WM_STATE")?
- .reply()?
- .atom;
+ let wm_state = connection.intern_atom(false, b"WM_STATE")?.reply()?.atom;
Ok(Self {
net_current_desktop,
@@ -128,12 +125,7 @@ impl WindowManager {
u16::from(config.modkey).into(),
)?;
- let mut monitors = detect_monitors(&connection, &screen, root)?;
-
- let selected_tags = Self::get_saved_selected_tags(&connection, root, config.tags.len())?;
- if !monitors.is_empty() {
- monitors[0].selected_tags = selected_tags;
- }
+ let monitors = detect_monitors(&connection, &screen, root)?;
let display = unsafe { x11::xlib::XOpenDisplay(std::ptr::null()) };
if display.is_null() {
@@ -308,7 +300,11 @@ impl WindowManager {
}
}
- Ok(self.monitors.get(self.selected_monitor).map(|m| m.selected_tags).unwrap_or(tag_mask(0)))
+ Ok(self
+ .monitors
+ .get(self.selected_monitor)
+ .map(|m| m.selected_tags)
+ .unwrap_or(tag_mask(0)))
}
fn save_client_tag(&self, window: Window, tag: TagMask) -> WmResult<()> {
@@ -376,7 +372,11 @@ impl WindowManager {
}
fn toggle_floating(&mut self) -> WmResult<()> {
- if let Some(focused) = self.monitors.get(self.selected_monitor).and_then(|m| m.focused_window) {
+ if let Some(focused) = self
+ .monitors
+ .get(self.selected_monitor)
+ .and_then(|m| m.focused_window)
+ {
if self.floating_windows.contains(&focused) {
self.floating_windows.remove(&focused);
self.apply_layout()?;
@@ -385,9 +385,10 @@ impl WindowManager {
let float_height = (self.screen.height_in_pixels / 2) as u32;
let border_width = self.config.border_width;
-
- let center_width = ((self.screen.width_in_pixels - float_width as u16)/2) as i32;
- let center_height = ((self.screen.height_in_pixels - float_height as u16)/2) as i32;
+
+ let center_width = ((self.screen.width_in_pixels - float_width as u16) / 2) as i32;
+ let center_height =
+ ((self.screen.height_in_pixels - float_height as u16) / 2) as i32;
self.connection.configure_window(
focused,
@@ -409,7 +410,11 @@ impl WindowManager {
}
fn toggle_fullscreen(&mut self) -> WmResult<()> {
- if let Some(focused) = self.monitors.get(self.selected_monitor).and_then(|m| m.focused_window) {
+ if let Some(focused) = self
+ .monitors
+ .get(self.selected_monitor)
+ .and_then(|m| m.focused_window)
+ {
if self.fullscreen_window == Some(focused) {
self.fullscreen_window = None;
@@ -458,7 +463,14 @@ impl WindowManager {
let draw_blocks = monitor_index == self.selected_monitor;
bar.invalidate();
- bar.draw(&self.connection, &self.font, self.display, monitor.selected_tags, occupied_tags, draw_blocks)?;
+ bar.draw(
+ &self.connection,
+ &self.font,
+ self.display,
+ monitor.selected_tags,
+ occupied_tags,
+ draw_blocks,
+ )?;
}
}
Ok(())
@@ -468,7 +480,11 @@ impl WindowManager {
match action {
KeyAction::Spawn => handlers::handle_spawn_action(action, arg)?,
KeyAction::KillClient => {
- if let Some(focused) = self.monitors.get(self.selected_monitor).and_then(|m| m.focused_window) {
+ if let Some(focused) = self
+ .monitors
+ .get(self.selected_monitor)
+ .and_then(|m| m.focused_window)
+ {
match self.connection.kill_client(focused) {
Ok(_) => {
self.connection.flush()?;
@@ -482,6 +498,28 @@ impl WindowManager {
KeyAction::ToggleFullScreen => {
self.toggle_fullscreen()?;
}
+ KeyAction::ChangeLayout => {
+ if let Arg::Str(layout_name) = arg {
+ match layout_from_str(layout_name) {
+ Ok(layout) => {
+ self.layout = layout;
+ self.apply_layout()?;
+ }
+ Err(e) => eprintln!("Failed to change layout: {}", e),
+ }
+ }
+ }
+ KeyAction::CycleLayout => {
+ let current_name = self.layout.name();
+ let next_name = next_layout(current_name);
+ match layout_from_str(next_name) {
+ Ok(layout) => {
+ self.layout = layout;
+ self.apply_layout()?;
+ }
+ Err(e) => eprintln!("Failed to cycle layout: {}", e),
+ }
+ }
KeyAction::ToggleFloating => {
self.toggle_floating()?;
}
@@ -645,7 +683,11 @@ impl WindowManager {
fn save_selected_tags(&self) -> WmResult<()> {
let net_current_desktop = self.atoms.net_current_desktop;
- let selected_tags = self.monitors.get(self.selected_monitor).map(|m| m.selected_tags).unwrap_or(tag_mask(0));
+ let selected_tags = self
+ .monitors
+ .get(self.selected_monitor)
+ .map(|m| m.selected_tags)
+ .unwrap_or(tag_mask(0));
let desktop = selected_tags.trailing_zeros();
let bytes = (desktop as u32).to_ne_bytes();
@@ -668,7 +710,11 @@ impl WindowManager {
return Ok(());
}
- if let Some(focused) = self.monitors.get(self.selected_monitor).and_then(|m| m.focused_window) {
+ if let Some(focused) = self
+ .monitors
+ .get(self.selected_monitor)
+ .and_then(|m| m.focused_window)
+ {
let mask = tag_mask(tag_index);
self.window_tags.insert(focused, mask);
@@ -689,7 +735,10 @@ impl WindowManager {
return Ok(());
}
- let current = self.monitors.get(self.selected_monitor).and_then(|m| m.focused_window);
+ let current = self
+ .monitors
+ .get(self.selected_monitor)
+ .and_then(|m| m.focused_window);
let next_window = if let Some(current) = current {
if let Some(current_index) = visible.iter().position(|&w| w == current) {
@@ -726,7 +775,11 @@ impl WindowManager {
Ok(())
}
- fn update_focus_visuals(&self, old_focused: Option<Window>, new_focused: Window) -> WmResult<()> {
+ fn update_focus_visuals(
+ &self,
+ old_focused: Option<Window>,
+ new_focused: Window,
+ ) -> WmResult<()> {
if let Some(old_win) = old_focused {
if old_win != new_focused {
self.connection.configure_window(
@@ -894,11 +947,16 @@ impl WindowManager {
&ChangeWindowAttributesAux::new().event_mask(EventMask::ENTER_WINDOW),
)?;
- let selected_tags = self.monitors.get(self.selected_monitor).map(|m| m.selected_tags).unwrap_or(tag_mask(0));
+ let selected_tags = self
+ .monitors
+ .get(self.selected_monitor)
+ .map(|m| m.selected_tags)
+ .unwrap_or(tag_mask(0));
self.windows.push(event.window);
self.window_tags.insert(event.window, selected_tags);
- self.window_monitor.insert(event.window, self.selected_monitor);
+ self.window_monitor
+ .insert(event.window, self.selected_monitor);
self.set_wm_state(event.window, 1)?;
let _ = self.save_client_tag(event.window, selected_tags);
@@ -929,7 +987,9 @@ impl WindowManager {
return Ok(None);
}
- if let Some(monitor_index) = self.get_monitor_at_point(event.root_x as i32, event.root_y as i32) {
+ if let Some(monitor_index) =
+ self.get_monitor_at_point(event.root_x as i32, event.root_y as i32)
+ {
if monitor_index != self.selected_monitor {
self.selected_monitor = monitor_index;
self.update_bar()?;
@@ -950,7 +1010,9 @@ impl WindowManager {
}
}
Event::ButtonPress(event) => {
- let is_bar_click = self.bars.iter()
+ let is_bar_click = self
+ .bars
+ .iter()
.enumerate()
.find(|(_, bar)| bar.window() == event.event);
@@ -1029,7 +1091,11 @@ impl WindowManager {
.copied()
.collect();
- let bar_height = self.bars.get(monitor_index).map(|b| b.height() as u32).unwrap_or(0);
+ let bar_height = self
+ .bars
+ .get(monitor_index)
+ .map(|b| b.height() as u32)
+ .unwrap_or(0);
let usable_height = monitor.height.saturating_sub(bar_height);
let geometries = self
@@ -1058,6 +1124,12 @@ impl WindowManager {
Ok(())
}
+ pub fn change_layout<L: Layout + 'static>(&mut self, new_layout: L) -> WmResult<()> {
+ self.layout = Box::new(new_layout);
+ self.apply_layout()?;
+ Ok(())
+ }
+
fn remove_window(&mut self, window: Window) -> WmResult<()> {
let initial_count = self.windows.len();
self.windows.retain(|&w| w != window);
@@ -1073,7 +1145,10 @@ impl WindowManager {
}
if self.windows.len() < initial_count {
- let focused = self.monitors.get(self.selected_monitor).and_then(|m| m.focused_window);
+ let focused = self
+ .monitors
+ .get(self.selected_monitor)
+ .and_then(|m| m.focused_window);
if focused == Some(window) {
let visible = self.visible_windows_on_monitor(self.selected_monitor);
if let Some(&new_win) = visible.last() {
diff --git a/templates/config.ron b/templates/config.ron
index 3a62643..18dcd89 100644
--- a/templates/config.ron
+++ b/templates/config.ron
@@ -26,6 +26,8 @@
(modifiers: [Mod4], key: Q, action: KillClient),
(modifiers: [Mod4, Shift], key: F, action: ToggleFullScreen),
(modifiers: [Mod4, Shift], key: Space, action: ToggleFloating),
+ (modifiers: [Mod4], key: F, action: ChangeLayout, arg: "normie"),
+ (modifiers: [Mod4], key: C, action: ChangeLayout, arg: "tiling"),
(modifiers: [Mod4], key: A, action: ToggleGaps),
(modifiers: [Mod4, Shift], key: Q, action: Quit),
(modifiers: [Mod4, Shift], key: R, action: Restart),
diff --git a/test-config.ron b/test-config.ron
index 5425704..e264036 100644
--- a/test-config.ron
+++ b/test-config.ron
@@ -32,6 +32,9 @@
(modifiers: [Mod1], key: Q, action: KillClient),
(modifiers: [Mod1, Shift], key: F, action: ToggleFullScreen),
(modifiers: [Mod1, Shift], key: Space, action: ToggleFloating),
+ (modifiers: [Mod1], key: F, action: ChangeLayout, arg: "normie"),
+ (modifiers: [Mod1], key: C, action: ChangeLayout, arg: "tiling"),
+ (modifiers: [Mod1], key: N, action: CycleLayout),
(modifiers: [Mod1], key: A, action: ToggleGaps),
(modifiers: [Mod1, Shift], key: Q, action: Quit),
(modifiers: [Mod1, Shift], key: R, action: Restart),