Diff
diff --git a/resources/test-config.lua b/resources/test-config.lua
index 3a931f6..0b5522b 100644
--- a/resources/test-config.lua
+++ b/resources/test-config.lua
@@ -77,13 +77,17 @@ oxwm.key.bind({ modkey }, "N", oxwm.layout.cycle())
oxwm.key.bind({ modkey }, "A", oxwm.toggle_gaps())
--- Master area controls (dwm-style)
--- NOTE: Mod+H/L are currently used for directional focus
--- Using Comma/Period instead. To match dwm exactly, replace H/L bindings above
-oxwm.key.bind({ modkey }, "Comma", oxwm.set_master_factor(-5)) -- Decrease master area
-oxwm.key.bind({ modkey }, "Period", oxwm.set_master_factor(5)) -- Increase master area
-oxwm.key.bind({ modkey }, "I", oxwm.inc_num_master(1)) -- More master windows
-oxwm.key.bind({ modkey }, "P", oxwm.inc_num_master(-1)) -- Fewer master windows
+-- Master area controls
+oxwm.key.bind({ modkey }, "BracketLeft", oxwm.set_master_factor(-5)) -- Decrease master area
+oxwm.key.bind({ modkey }, "BracketRight", oxwm.set_master_factor(5)) -- Increase master area
+oxwm.key.bind({ modkey }, "I", oxwm.inc_num_master(1)) -- More master windows
+oxwm.key.bind({ modkey }, "P", oxwm.inc_num_master(-1)) -- Fewer master windows
+
+-- Multi-monitor controls (dwm-style)
+oxwm.key.bind({ modkey }, "Comma", oxwm.monitor.focus(-1)) -- Focus previous monitor
+oxwm.key.bind({ modkey }, "Period", oxwm.monitor.focus(1)) -- Focus next monitor
+oxwm.key.bind({ modkey, "Shift" }, "Comma", oxwm.monitor.tag(-1)) -- Send window to previous monitor
+oxwm.key.bind({ modkey, "Shift" }, "Period", oxwm.monitor.tag(1)) -- Send window to next monitor
oxwm.key.bind({ modkey, "Shift" }, "Q", oxwm.quit())
oxwm.key.bind({ modkey, "Shift" }, "R", oxwm.restart())
diff --git a/src/config/lua_api.rs b/src/config/lua_api.rs
index b89d6e9..65132a8 100644
--- a/src/config/lua_api.rs
+++ b/src/config/lua_api.rs
@@ -88,6 +88,7 @@ pub fn register_api(lua: &Lua) -> Result<SharedBuilder, ConfigError> {
register_client_module(&lua, &oxwm_table)?;
register_layout_module(&lua, &oxwm_table)?;
register_tag_module(&lua, &oxwm_table)?;
+ register_monitor_module(&lua, &oxwm_table)?;
register_rule_module(&lua, &oxwm_table, builder.clone())?;
register_bar_module(&lua, &oxwm_table, builder.clone())?;
register_misc(&lua, &oxwm_table, builder.clone())?;
@@ -324,6 +325,23 @@ fn register_tag_module(lua: &Lua, parent: &Table) -> Result<(), ConfigError> {
Ok(())
}
+fn register_monitor_module(lua: &Lua, parent: &Table) -> Result<(), ConfigError> {
+ let monitor_table = lua.create_table()?;
+
+ let focus = lua.create_function(|lua, direction: i64| {
+ create_action_table(lua, "FocusMonitor", Value::Integer(direction))
+ })?;
+
+ let tag = lua.create_function(|lua, direction: i64| {
+ create_action_table(lua, "TagMonitor", Value::Integer(direction))
+ })?;
+
+ monitor_table.set("focus", focus)?;
+ monitor_table.set("tag", tag)?;
+ parent.set("monitor", monitor_table)?;
+ Ok(())
+}
+
fn register_rule_module(lua: &Lua, parent: &Table, builder: SharedBuilder) -> Result<(), ConfigError> {
let rule_table = lua.create_table()?;
@@ -789,6 +807,7 @@ fn string_to_action(s: &str) -> mlua::Result<KeyAction> {
"ChangeLayout" => Ok(KeyAction::ChangeLayout),
"CycleLayout" => Ok(KeyAction::CycleLayout),
"FocusMonitor" => Ok(KeyAction::FocusMonitor),
+ "TagMonitor" => Ok(KeyAction::TagMonitor),
"ExchangeClient" => Ok(KeyAction::ExchangeClient),
"ShowKeybindOverlay" => Ok(KeyAction::ShowKeybindOverlay),
_ => Err(mlua::Error::RuntimeError(format!("unknown action '{}'. this is an internal error, please report it", s))),
diff --git a/src/keyboard/handlers.rs b/src/keyboard/handlers.rs
index 7d867ec..a21fb91 100644
--- a/src/keyboard/handlers.rs
+++ b/src/keyboard/handlers.rs
@@ -30,6 +30,7 @@ pub enum KeyAction {
ChangeLayout,
CycleLayout,
FocusMonitor,
+ TagMonitor,
ExchangeClient,
ShowKeybindOverlay,
SetMasterFactor,
diff --git a/src/overlay/keybind.rs b/src/overlay/keybind.rs
index 9789510..391b848 100644
--- a/src/overlay/keybind.rs
+++ b/src/overlay/keybind.rs
@@ -221,6 +221,7 @@ impl KeybindOverlay {
KeyAction::ChangeLayout => "Change Layout".to_string(),
KeyAction::CycleLayout => "Cycle Through Layouts".to_string(),
KeyAction::FocusMonitor => "Focus Next Monitor".to_string(),
+ KeyAction::TagMonitor => "Send Window to Monitor".to_string(),
KeyAction::ExchangeClient => "Exchange Client Windows".to_string(),
KeyAction::SetMasterFactor => "Adjust Master Area Size".to_string(),
KeyAction::IncNumMaster => "Adjust Number of Master Windows".to_string(),
diff --git a/src/window_manager.rs b/src/window_manager.rs
index e58eb9c..559325b 100644
--- a/src/window_manager.rs
+++ b/src/window_manager.rs
@@ -743,9 +743,9 @@ impl WindowManager {
for (monitor_index, monitor) in self.monitors.iter().enumerate() {
if let Some(bar) = self.bars.get_mut(monitor_index) {
let mut occupied_tags: TagMask = 0;
- for (&window, &tags) in &self.window_tags {
- if self.window_monitor.get(&window).copied().unwrap_or(0) == monitor_index {
- occupied_tags |= tags;
+ for client in self.clients.values() {
+ if client.monitor_index == monitor_index {
+ occupied_tags |= client.tags;
}
}
@@ -773,15 +773,14 @@ impl WindowManager {
.windows
.iter()
.filter(|&&window| {
- let window_monitor_index = self.window_monitor.get(&window).copied().unwrap_or(0);
- if window_monitor_index != monitor_index
- || self.floating_windows.contains(&window)
- || self.fullscreen_windows.contains(&window)
- {
- return false;
- }
- if let Some(&tags) = self.window_tags.get(&window) {
- (tags & monitor.tagset[monitor.selected_tags_index]) != 0
+ if let Some(client) = self.clients.get(&window) {
+ if client.monitor_index != monitor_index
+ || self.floating_windows.contains(&window)
+ || self.fullscreen_windows.contains(&window)
+ {
+ return false;
+ }
+ (client.tags & monitor.tagset[monitor.selected_tags_index]) != 0
} else {
false
}
@@ -925,6 +924,11 @@ impl WindowManager {
self.focus_monitor(*direction)?;
}
}
+ KeyAction::TagMonitor => {
+ if let Arg::Int(direction) = arg {
+ self.tag_monitor(*direction)?;
+ }
+ }
KeyAction::ShowKeybindOverlay => {
let monitor = &self.monitors[self.selected_monitor];
self.keybind_overlay.toggle(
@@ -950,40 +954,12 @@ impl WindowManager {
Ok(())
}
- fn focus_monitor(&mut self, direction: i32) -> WmResult<()> {
- if self.monitors.is_empty() {
- return Ok(());
- }
-
- let new_monitor = if direction > 0 {
- (self.selected_monitor + 1) % self.monitors.len()
- } else {
- (self.selected_monitor + self.monitors.len() - 1) % self.monitors.len()
- };
-
- if new_monitor == self.selected_monitor {
- return Ok(());
- }
-
- self.selected_monitor = new_monitor;
-
- self.update_bar()?;
-
- let visible = self.visible_windows_on_monitor(new_monitor);
- if let Some(&win) = visible.first() {
- self.focus(Some(win))?;
- }
-
- Ok(())
- }
fn is_window_visible(&self, window: Window) -> bool {
- let window_mon = self.window_monitor.get(&window).copied().unwrap_or(0);
-
- if let Some(&tags) = self.window_tags.get(&window) {
- let monitor = self.monitors.get(window_mon);
+ if let Some(client) = self.clients.get(&window) {
+ let monitor = self.monitors.get(client.monitor_index);
let selected_tags = monitor.map(|m| m.tagset[m.selected_tags_index]).unwrap_or(0);
- (tags & selected_tags) != 0
+ (client.tags & selected_tags) != 0
} else {
false
}
@@ -1033,6 +1009,44 @@ impl WindowManager {
.position(|mon| mon.contains_point(x, y))
}
+ fn rect_to_monitor(&self, x: i32, y: i32, w: i32, h: i32) -> usize {
+ let mut best_monitor = self.selected_monitor;
+ let mut max_area = 0;
+
+ for (idx, monitor) in self.monitors.iter().enumerate() {
+ let intersect_width = 0.max((x + w).min(monitor.window_area_x + monitor.window_area_width) - x.max(monitor.window_area_x));
+ let intersect_height = 0.max((y + h).min(monitor.window_area_y + monitor.window_area_height) - y.max(monitor.window_area_y));
+ let area = intersect_width * intersect_height;
+
+ if area > max_area {
+ max_area = area;
+ best_monitor = idx;
+ }
+ }
+
+ best_monitor
+ }
+
+ fn dir_to_monitor(&self, direction: i32) -> Option<usize> {
+ if self.monitors.len() <= 1 {
+ return None;
+ }
+
+ if direction > 0 {
+ if self.selected_monitor + 1 < self.monitors.len() {
+ Some(self.selected_monitor + 1)
+ } else {
+ Some(0)
+ }
+ } else {
+ if self.selected_monitor == 0 {
+ Some(self.monitors.len() - 1)
+ } else {
+ Some(self.selected_monitor - 1)
+ }
+ }
+ }
+
// Dwm's g-loaded approach to handling the spam alternating crash.
fn update_window_visibility(&self) -> WmResult<()> {
for &window in &self.windows {
@@ -1579,7 +1593,9 @@ impl WindowManager {
.collect();
for window in floating_windows {
- let monitor_idx = *self.window_monitor.get(&window).unwrap_or(&self.selected_monitor);
+ let monitor_idx = self.clients.get(&window)
+ .map(|c| c.monitor_index)
+ .unwrap_or(self.selected_monitor);
let monitor = &self.monitors[monitor_idx];
let (outer_gap_h, outer_gap_v) = if self.gaps_enabled {
@@ -1679,7 +1695,9 @@ impl WindowManager {
client.is_fullscreen = true;
}
- let monitor_idx = *self.window_monitor.get(&window).unwrap_or(&self.selected_monitor);
+ let monitor_idx = self.clients.get(&window)
+ .map(|c| c.monitor_index)
+ .unwrap_or(self.selected_monitor);
let monitor = &self.monitors[monitor_idx];
if self.show_bar {
@@ -1723,7 +1741,9 @@ impl WindowManager {
.border_width(self.config.border_width),
)?;
- let monitor_idx = *self.window_monitor.get(&window).unwrap_or(&self.selected_monitor);
+ let monitor_idx = self.clients.get(&window)
+ .map(|c| c.monitor_index)
+ .unwrap_or(self.selected_monitor);
if self.show_bar {
if let Some(bar) = self.bars.get(monitor_idx) {
self.connection.map_window(bar.window())?;
@@ -1906,14 +1926,15 @@ impl WindowManager {
let transient_parent = self.get_transient_parent(window);
let (window_tags, monitor_index) = if let Some(parent) = transient_parent {
- let parent_tags = self.window_tags.get(&parent).copied().unwrap_or_else(|| {
- self.monitors
+ if let Some(parent_client) = self.clients.get(&parent) {
+ (parent_client.tags, parent_client.monitor_index)
+ } else {
+ let tags = self.monitors
.get(self.selected_monitor)
.map(|m| m.tagset[m.selected_tags_index])
- .unwrap_or(tag_mask(0))
- });
- let parent_monitor = self.window_monitor.get(&parent).copied().unwrap_or(self.selected_monitor);
- (parent_tags, parent_monitor)
+ .unwrap_or(tag_mask(0));
+ (tags, self.selected_monitor)
+ }
} else {
let selected_tags = self.monitors
.get(self.selected_monitor)
@@ -2190,7 +2211,9 @@ impl WindowManager {
return Ok(());
}
- let monitor_idx = *self.window_monitor.get(&win).unwrap_or(&self.selected_monitor);
+ let monitor_idx = self.clients.get(&win)
+ .map(|c| c.monitor_index)
+ .unwrap_or(self.selected_monitor);
if monitor_idx != self.selected_monitor {
self.selected_monitor = monitor_idx;
}
@@ -2340,6 +2363,54 @@ impl WindowManager {
Ok(())
}
+ pub fn focus_monitor(&mut self, direction: i32) -> WmResult<()> {
+ if self.monitors.len() <= 1 {
+ return Ok(());
+ }
+
+ let target_monitor = match self.dir_to_monitor(direction) {
+ Some(idx) if idx != self.selected_monitor => idx,
+ _ => return Ok(()),
+ };
+
+ let old_selected = self.monitors
+ .get(self.selected_monitor)
+ .and_then(|m| m.selected_client);
+
+ if let Some(win) = old_selected {
+ self.unfocus(win)?;
+ }
+
+ self.selected_monitor = target_monitor;
+ self.focus(None)?;
+
+ Ok(())
+ }
+
+ pub fn tag_monitor(&mut self, direction: i32) -> WmResult<()> {
+ if self.monitors.len() <= 1 {
+ return Ok(());
+ }
+
+ let selected_window = self.monitors
+ .get(self.selected_monitor)
+ .and_then(|m| m.selected_client);
+
+ let window = match selected_window {
+ Some(win) => win,
+ None => return Ok(()),
+ };
+
+ let target_monitor = match self.dir_to_monitor(direction) {
+ Some(idx) => idx,
+ None => return Ok(()),
+ };
+
+ self.send_to_monitor(window, target_monitor)?;
+
+ Ok(())
+ }
+
fn update_focus_visuals(
&self,
old_focused: Option<Window>,
@@ -2599,9 +2670,9 @@ impl WindowManager {
return Ok(None);
}
if self.windows.contains(&event.event) {
- if let Some(&window_monitor_index) = self.window_monitor.get(&event.event) {
- if window_monitor_index != self.selected_monitor {
- self.selected_monitor = window_monitor_index;
+ if let Some(client) = self.clients.get(&event.event) {
+ if client.monitor_index != self.selected_monitor {
+ self.selected_monitor = client.monitor_index;
self.update_bar()?;
}
}
@@ -2727,16 +2798,15 @@ impl WindowManager {
.windows
.iter()
.filter(|&&window| {
- let window_monitor_index = self.window_monitor.get(&window).copied().unwrap_or(0);
- if window_monitor_index != monitor_index
- || self.floating_windows.contains(&window)
- || self.fullscreen_windows.contains(&window)
- {
- return false;
- }
- let monitor_tags = self.monitors.get(monitor_index).map(|m| m.tagset[m.selected_tags_index]).unwrap_or(0);
- if let Some(&tags) = self.window_tags.get(&window) {
- (tags & monitor_tags) != 0
+ if let Some(client) = self.clients.get(&window) {
+ if client.monitor_index != monitor_index
+ || self.floating_windows.contains(&window)
+ || self.fullscreen_windows.contains(&window)
+ {
+ return false;
+ }
+ let monitor_tags = self.monitors.get(monitor_index).map(|m| m.tagset[m.selected_tags_index]).unwrap_or(0);
+ (client.tags & monitor_tags) != 0
} else {
false
}
@@ -2780,7 +2850,9 @@ impl WindowManager {
}
Event::ConfigureRequest(event) => {
if self.windows.contains(&event.window) {
- let monitor_index = *self.window_monitor.get(&event.window).unwrap_or(&self.selected_monitor);
+ let monitor_index = self.clients.get(&event.window)
+ .map(|c| c.monitor_index)
+ .unwrap_or(self.selected_monitor);
let monitor = &self.monitors[monitor_index];
let is_floating = self.floating_windows.contains(&event.window);
let is_tiling_layout = self.layout.name() != "normie";
@@ -2854,13 +2926,24 @@ impl WindowManager {
cached_geom.map(|g| g.y_position).unwrap_or(0)
};
+ let final_width = if value_mask.contains(ConfigWindow::WIDTH) { event.width } else { cached_geom.map(|g| g.width).unwrap_or(1) };
+ let final_height = if value_mask.contains(ConfigWindow::HEIGHT) { event.height } else { cached_geom.map(|g| g.height).unwrap_or(1) };
+
self.update_geometry_cache(event.window, CachedGeometry {
x_position: final_x,
y_position: final_y,
- width: if value_mask.contains(ConfigWindow::WIDTH) { event.width } else { cached_geom.map(|g| g.width).unwrap_or(1) },
- height: if value_mask.contains(ConfigWindow::HEIGHT) { event.height } else { cached_geom.map(|g| g.height).unwrap_or(1) },
+ width: final_width,
+ height: final_height,
border_width: if value_mask.contains(ConfigWindow::BORDER_WIDTH) { event.border_width } else { border_width },
});
+
+ if is_floating {
+ let new_monitor = self.rect_to_monitor(final_x as i32, final_y as i32, final_width as i32, final_height as i32);
+
+ if new_monitor != monitor_index {
+ self.send_to_monitor(event.window, new_monitor)?;
+ }
+ }
} else {
self.send_configure_notify(event.window)?;
}
@@ -3068,16 +3151,15 @@ impl WindowManager {
.windows
.iter()
.any(|&window| {
- let window_monitor_index = self.window_monitor.get(&window).copied().unwrap_or(0);
- if window_monitor_index != monitor_index
- || self.floating_windows.contains(&window)
- || 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.tagset[monitor.selected_tags_index]) != 0;
+ if let Some(client) = self.clients.get(&window) {
+ if client.monitor_index != monitor_index
+ || self.floating_windows.contains(&window)
+ || self.fullscreen_windows.contains(&window)
+ {
+ return false;
+ }
+ if let Some(monitor) = self.monitors.get(monitor_index) {
+ return (client.tags & monitor.tagset[monitor.selected_tags_index]) != 0;
}
}
false
@@ -3495,6 +3577,42 @@ impl WindowManager {
}
}
+ fn send_to_monitor(&mut self, window: Window, target_monitor: usize) -> WmResult<()> {
+ if target_monitor >= self.monitors.len() {
+ return Ok(());
+ }
+
+ let current_monitor = self.clients.get(&window).map(|c| c.monitor_index);
+ if current_monitor == Some(target_monitor) {
+ return Ok(());
+ }
+
+ self.unfocus(window)?;
+
+ self.detach(window);
+ self.detach_stack(window);
+
+ if let Some(client) = self.clients.get_mut(&window) {
+ client.monitor_index = target_monitor;
+ let new_tags = self.monitors
+ .get(target_monitor)
+ .map(|m| m.tagset[m.selected_tags_index])
+ .unwrap_or(1);
+ client.tags = new_tags;
+ self.window_tags.insert(window, new_tags);
+ }
+
+ self.window_monitor.insert(window, target_monitor);
+
+ self.attach_aside(window, target_monitor);
+ self.attach_stack(window, target_monitor);
+
+ self.focus(None)?;
+ self.apply_layout()?;
+
+ Ok(())
+ }
+
fn remove_window(&mut self, window: Window) -> WmResult<()> {
let initial_count = self.windows.len();