From 41a4341e714b3ab96ec7e3c951cbd4ae195dfb09 Mon Sep 17 00:00:00 2001 From: potkan Date: Tue, 28 Apr 2026 23:48:13 +0200 Subject: [PATCH] Upload files to "/" --- .gitignore | 21 ++ Cargo.lock | 93 ++++++ Cargo.toml | 12 + LICENSE | 12 + README.md | 88 +++++ scripts/run_xephyr.sh | 46 +++ src/main.rs | 752 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1024 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 scripts/run_xephyr.sh create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad67955 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Generated by Cargo +# will have compiled files and executables +debug +target + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Generated by cargo mutants +# Contains mutation testing data +**/mutants.out*/ + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0c9ec8a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,93 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "dxdwm" +version = "0.1.0" +dependencies = [ + "x11rb", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..061b047 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "dxdwm" +version = "0.1.0" +edition = "2024" +license-file = "LICENSE" +description = "A small X11 window manager in Rust" + +[dependencies] +x11rb = { version = "0.13", default-features = false, features = ["xkb"] } + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6029513 --- /dev/null +++ b/LICENSE @@ -0,0 +1,12 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +This project is licensed under the GNU General Public License v3.0 only. + +You may obtain a copy of the full license text at: +https://www.gnu.org/licenses/gpl-3.0.txt + diff --git a/README.md b/README.md new file mode 100644 index 0000000..a22aa49 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# dxdwm - BE AWARE OF VIBECODING + +`dxdwm` is a small X11 window manager written in Rust. + +## What it does + +- Becomes the active X11 window manager via `SubstructureRedirect`. +- Manages top-level windows and keeps them focusable/raiseable. +- Default launcher keybind: **Super (Windows key)** runs: + - `rofi -show drun` +- Terminal keybind: **Alt+Enter** runs: + - `alacritty` +- Window control keybinds: + - `Alt+Q` closes the focused window. + - `Alt+W` cycles focus through managed windows (tab-like). + - `Alt+Space` toggles between floating mode and single-window tabbed mode. +- Mouse bindings: + - `Alt + Left Mouse Drag` moves windows. + - `Alt + Right Mouse Drag` resizes windows. + +## Project layout + +- `Cargo.toml` - dependency manifest. +- `src/main.rs` - window manager implementation. +- `scripts/run_xephyr.sh` - optional nested X11 runner for Linux. + +## Notes + +- This is intended for X11 Linux sessions. +- On macOS, this can be edited but not realistically run as an X11 session WM. +- `rofi` must be installed and in `PATH` for the launcher binding. +- `DXDWM_LAUNCHER` can override launcher command (default: `rofi -show drun`). +- `DXDWM_TERMINAL` can override terminal command (default: `alacritty`). +- On startup, dxdwm runs `$HOME/.config/dxdwm/dxdwm.sh`; it is auto-created if missing. + +## License + +GPL-3.0-only + +## Build (Linux/X11 host) + +```bash +cargo build --release +``` + +## Run from tty via `startx` (standalone WM) + +Create `~/.xinitrc`: + +```bash +#!/usr/bin/env sh +export PATH="$HOME/.cargo/bin:/usr/local/bin:/usr/bin:/bin" +# Optional overrides: +# export DXDWM_TERMINAL="kitty" +# export DXDWM_LAUNCHER="rofi -show drun" +exec /absolute/path/to/dxdwm/target/release/dxdwm +``` + +Then from a Linux tty (not from inside another WM): + +```bash +chmod +x ~/.xinitrc +startx +``` + +## Run as WM from an existing X session + +```bash +exec /absolute/path/to/dxdwm/target/release/dxdwm +``` + +## Quick input checks + +Inside dxdwm, verify: + +- `Alt + Left Mouse Drag` moves windows. +- `Alt + Right Mouse Drag` resizes windows. +- `Alt + Enter` launches your terminal. +- Pressing `Super` launches your app launcher. + +## Optional nested run (Linux with Xephyr) + +```bash +./scripts/run_xephyr.sh +``` + + + diff --git a/scripts/run_xephyr.sh b/scripts/run_xephyr.sh new file mode 100644 index 0000000..e9bf27f --- /dev/null +++ b/scripts/run_xephyr.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +DISPLAY_NUM=":2" + +if [ -z "${DISPLAY:-}" ]; then + echo "Host DISPLAY is not set. Start this from an active X11 session (for example, a terminal inside your desktop session)." + exit 1 +fi + +if ! command -v Xephyr >/dev/null 2>&1; then + echo "Xephyr not found. Install xorg-server-xephyr first." + exit 1 +fi + +if ! command -v xterm >/dev/null 2>&1; then + echo "xterm not found. Install xterm first." + exit 1 +fi + +echo "Building dxdwm..." +cargo build --manifest-path "$ROOT_DIR/Cargo.toml" + +echo "Starting Xephyr on $DISPLAY_NUM..." +Xephyr "$DISPLAY_NUM" -screen 1280x720 -ac & +XEPHYR_PID=$! + +cleanup() { + kill "$XEPHYR_PID" >/dev/null 2>&1 || true +} +trap cleanup EXIT INT TERM + +sleep 1 + +echo "Starting dxdwm in nested display..." +DISPLAY="$DISPLAY_NUM" "$ROOT_DIR/target/debug/dxdwm" & +DWM_PID=$! + +sleep 1 + +DISPLAY="$DISPLAY_NUM" xterm & +DISPLAY="$DISPLAY_NUM" xterm & + +wait "$DWM_PID" + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b5efcea --- /dev/null +++ b/src/main.rs @@ -0,0 +1,752 @@ +use std::collections::HashSet; +use std::env; +use std::error::Error; +use std::fs; +use std::io; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +use std::path::PathBuf; +use std::process::{Command, Stdio}; + +use x11rb::connection::Connection; +use x11rb::protocol::xproto::{ + ButtonIndex, ButtonPressEvent, ButtonReleaseEvent, ChangeWindowAttributesAux, + ClientMessageData, ClientMessageEvent, ConfigWindow, ConfigureRequestEvent, + ConfigureWindowAux, ConnectionExt, EventMask, GrabMode, InputFocus, KeyPressEvent, + MapRequestEvent, ModMask, MotionNotifyEvent, StackMode, UnmapNotifyEvent, Window, +}; +use x11rb::rust_connection::RustConnection; +use x11rb::{CURRENT_TIME, NONE}; + +const KEYSYM_SUPER_L: u32 = 0xFFEB; +const KEYSYM_SUPER_R: u32 = 0xFFEC; +const KEYSYM_RETURN: u32 = 0xFF0D; +const KEYSYM_Q: u32 = 0x0071; +const KEYSYM_W: u32 = 0x0077; +const KEYSYM_SPACE: u32 = 0x0020; +const KEYSYM_NUM_LOCK: u32 = 0xFF7F; +const MIN_WIN_SIZE: u16 = 64; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum LayoutMode { + Floating, + Tabbed, +} + +#[derive(Clone, Copy)] +enum DragMode { + Move, + Resize, +} + +#[derive(Clone, Copy)] +struct DragState { + window: Window, + mode: DragMode, + start_root_x: i16, + start_root_y: i16, + start_x: i32, + start_y: i32, + start_width: u16, + start_height: u16, +} + +struct Wm { + conn: RustConnection, + root: Window, + managed: HashSet, + super_keycodes: Vec, + terminal_keycodes: Vec, + close_keycodes: Vec, + cycle_keycodes: Vec, + toggle_mode_keycodes: Vec, + num_lock_mask: u16, + managed_order: Vec, + focused: Option, + layout_mode: LayoutMode, + drag: Option, +} + +impl Wm { + fn new() -> Result> { + let display = env::var("DISPLAY") + .ok() + .filter(|v| !v.trim().is_empty()) + .unwrap_or_else(|| "unset".to_string()); + + let (conn, screen_num) = x11rb::connect(None).map_err(|err| { + io::Error::other(format!( + "Failed to connect to X server using DISPLAY={display}. Original error: {err}" + )) + })?; + let root = conn.setup().roots[screen_num].root; + + conn.change_window_attributes( + root, + &ChangeWindowAttributesAux::default().event_mask( + EventMask::SUBSTRUCTURE_REDIRECT + | EventMask::SUBSTRUCTURE_NOTIFY + | EventMask::BUTTON_PRESS + | EventMask::BUTTON_RELEASE + | EventMask::POINTER_MOTION + | EventMask::STRUCTURE_NOTIFY, + ), + )? + .check() + .map_err(|_| { + "Unable to become window manager. Another WM probably owns SubstructureRedirect." + })?; + + let mut wm = Self { + conn, + root, + managed: HashSet::new(), + super_keycodes: Vec::new(), + terminal_keycodes: Vec::new(), + close_keycodes: Vec::new(), + cycle_keycodes: Vec::new(), + toggle_mode_keycodes: Vec::new(), + num_lock_mask: 0, + managed_order: Vec::new(), + focused: None, + layout_mode: LayoutMode::Floating, + drag: None, + }; + + wm.setup_input_grabs()?; + wm.take_existing_windows()?; + wm.advertise_wm_selection()?; + wm.conn.flush()?; + + Ok(wm) + } + + fn run(&mut self) -> Result<(), Box> { + loop { + let event = self.conn.wait_for_event()?; + match event { + x11rb::protocol::Event::MapRequest(e) => self.on_map_request(e)?, + x11rb::protocol::Event::UnmapNotify(e) => self.on_unmap(e), + x11rb::protocol::Event::DestroyNotify(e) => self.on_unmanage(e.window), + x11rb::protocol::Event::ConfigureRequest(e) => self.on_configure_request(e)?, + x11rb::protocol::Event::KeyPress(e) => self.on_key_press(e)?, + x11rb::protocol::Event::ButtonPress(e) => self.on_button_press(e)?, + x11rb::protocol::Event::MotionNotify(e) => self.on_motion(e)?, + x11rb::protocol::Event::ButtonRelease(e) => self.on_button_release(e)?, + _ => {} + } + self.conn.flush()?; + } + } + + fn advertise_wm_selection(&self) -> Result<(), Box> { + // WM_S0 is the canonical selection atom for screen 0. + let atom = self + .conn + .intern_atom(false, b"WM_S0")? + .reply()? + .atom; + let _ = self.conn.set_selection_owner(self.root, atom, CURRENT_TIME)?; + Ok(()) + } + + fn take_existing_windows(&mut self) -> Result<(), Box> { + let children = self.conn.query_tree(self.root)?.reply()?.children; + for win in children { + let attrs = self.conn.get_window_attributes(win)?.reply()?; + if attrs.override_redirect { + continue; + } + self.manage_window(win)?; + } + Ok(()) + } + + fn setup_input_grabs(&mut self) -> Result<(), Box> { + self.num_lock_mask = self.detect_num_lock_mask()?; + + self.super_keycodes = self.keycodes_for_keysyms(&[KEYSYM_SUPER_L, KEYSYM_SUPER_R])?; + self.terminal_keycodes = self.keycodes_for_keysyms(&[KEYSYM_RETURN])?; + self.close_keycodes = self.keycodes_for_keysyms(&[KEYSYM_Q])?; + self.cycle_keycodes = self.keycodes_for_keysyms(&[KEYSYM_W])?; + self.toggle_mode_keycodes = self.keycodes_for_keysyms(&[KEYSYM_SPACE])?; + + for keycode in &self.super_keycodes { + for mods in self.modifier_variants(ModMask::from(0u16)) { + self.conn.grab_key( + false, + self.root, + mods, + *keycode, + GrabMode::ASYNC, + GrabMode::ASYNC, + )?; + } + } + + for keycode in &self.terminal_keycodes { + for mods in self.modifier_variants(ModMask::M1) { + self.conn.grab_key( + false, + self.root, + mods, + *keycode, + GrabMode::ASYNC, + GrabMode::ASYNC, + )?; + } + } + + for keycode in &self.close_keycodes { + for mods in self.modifier_variants(ModMask::M1) { + self.conn.grab_key( + false, + self.root, + mods, + *keycode, + GrabMode::ASYNC, + GrabMode::ASYNC, + )?; + } + } + + for keycode in &self.cycle_keycodes { + for mods in self.modifier_variants(ModMask::M1) { + self.conn.grab_key( + false, + self.root, + mods, + *keycode, + GrabMode::ASYNC, + GrabMode::ASYNC, + )?; + } + } + + for keycode in &self.toggle_mode_keycodes { + for mods in self.modifier_variants(ModMask::M1) { + self.conn.grab_key( + false, + self.root, + mods, + *keycode, + GrabMode::ASYNC, + GrabMode::ASYNC, + )?; + } + } + + Ok(()) + } + + fn keycodes_for_keysyms(&self, keysyms: &[u32]) -> Result, Box> { + let mapping = self.conn.get_keyboard_mapping(8, 248)?.reply()?; + let mut keycodes = Vec::new(); + + for (idx, syms) in mapping + .keysyms + .chunks(mapping.keysyms_per_keycode as usize) + .enumerate() + { + if syms.iter().any(|s| keysyms.contains(s)) { + keycodes.push((idx + 8) as u8); + } + } + + Ok(keycodes) + } + + fn detect_num_lock_mask(&self) -> Result> { + let modifier_map = self.conn.get_modifier_mapping()?.reply()?; + let keycodes_per_modifier = modifier_map.keycodes_per_modifier() as usize; + let modifier_masks = [ + u16::from(ModMask::SHIFT), + u16::from(ModMask::LOCK), + u16::from(ModMask::CONTROL), + u16::from(ModMask::M1), + u16::from(ModMask::M2), + u16::from(ModMask::M3), + u16::from(ModMask::M4), + u16::from(ModMask::M5), + ]; + + for (idx, mask_bits) in modifier_masks.iter().copied().enumerate() { + let start = idx * keycodes_per_modifier; + let end = start + keycodes_per_modifier; + for keycode in &modifier_map.keycodes[start..end] { + if *keycode == 0 { + continue; + } + + let reply = self.conn.get_keyboard_mapping(*keycode, 1)?.reply()?; + if reply.keysyms.contains(&KEYSYM_NUM_LOCK) { + return Ok(mask_bits); + } + } + } + + Ok(0) + } + + fn modifier_variants(&self, base: ModMask) -> Vec { + let base_bits = u16::from(base); + let lock_bits = u16::from(ModMask::LOCK); + let mut variants = vec![ + ModMask::from(base_bits), + ModMask::from(base_bits | lock_bits), + ]; + if self.num_lock_mask != 0 { + variants.push(ModMask::from(base_bits | self.num_lock_mask)); + variants.push(ModMask::from(base_bits | self.num_lock_mask | lock_bits)); + } + variants.sort_by_key(|mask| u16::from(*mask)); + variants.dedup_by_key(|mask| u16::from(*mask)); + variants + } + + fn normalize_modifiers(&self, state_bits: u16) -> u16 { + let mut bits = state_bits; + bits &= !u16::from(ModMask::LOCK); + bits &= !self.num_lock_mask; + bits + } + + fn on_map_request(&mut self, e: MapRequestEvent) -> Result<(), Box> { + self.conn.map_window(e.window)?; + self.manage_window(e.window)?; + self.focus(e.window)?; + self.apply_layout()?; + Ok(()) + } + + fn on_unmap(&mut self, e: UnmapNotifyEvent) { + if e.event != self.root { + self.on_unmanage(e.window); + } + } + + fn on_unmanage(&mut self, window: Window) { + self.managed.remove(&window); + self.managed_order.retain(|w| *w != window); + if self.focused == Some(window) { + self.focused = self.managed_order.last().copied(); + } + if matches!(self.drag, Some(d) if d.window == window) { + self.drag = None; + } + + let _ = self.apply_layout(); + } + + fn on_configure_request(&self, e: ConfigureRequestEvent) -> Result<(), Box> { + // This WM is floating by default, so client configure requests are honored. + let mut aux = ConfigureWindowAux::default(); + if e.value_mask.contains(ConfigWindow::X) { + aux = aux.x(i32::from(e.x)); + } + if e.value_mask.contains(ConfigWindow::Y) { + aux = aux.y(i32::from(e.y)); + } + if e.value_mask.contains(ConfigWindow::WIDTH) { + aux = aux.width(u32::from(e.width)); + } + if e.value_mask.contains(ConfigWindow::HEIGHT) { + aux = aux.height(u32::from(e.height)); + } + if e.value_mask.contains(ConfigWindow::BORDER_WIDTH) { + aux = aux.border_width(u32::from(e.border_width)); + } + if e.value_mask.contains(ConfigWindow::SIBLING) { + aux = aux.sibling(e.sibling); + } + if e.value_mask.contains(ConfigWindow::STACK_MODE) { + aux = aux.stack_mode(e.stack_mode); + } + self.conn.configure_window(e.window, &aux)?; + Ok(()) + } + + fn on_key_press(&mut self, e: KeyPressEvent) -> Result<(), Box> { + let state_bits = self.normalize_modifiers(u16::from(e.state)); + + if self.terminal_keycodes.contains(&e.detail) + && (state_bits & u16::from(ModMask::M1)) != 0 + { + if let Err(err) = spawn_terminal() { + eprintln!("failed to launch terminal: {err}"); + } + return Ok(()); + } + + if self.close_keycodes.contains(&e.detail) && (state_bits & u16::from(ModMask::M1)) != 0 { + self.close_focused_window()?; + return Ok(()); + } + + if self.cycle_keycodes.contains(&e.detail) && (state_bits & u16::from(ModMask::M1)) != 0 { + self.focus_next_window()?; + return Ok(()); + } + + if self.toggle_mode_keycodes.contains(&e.detail) + && (state_bits & u16::from(ModMask::M1)) != 0 + { + self.toggle_layout_mode()?; + return Ok(()); + } + + if self.super_keycodes.contains(&e.detail) && state_bits == 0 { + if let Err(err) = spawn_rofi() { + eprintln!("failed to launch rofi: {err}"); + } + } + + Ok(()) + } + + fn on_button_press(&mut self, e: ButtonPressEvent) -> Result<(), Box> { + if self.layout_mode == LayoutMode::Tabbed { + return Ok(()); + } + + let is_alt = u16::from(e.state) & u16::from(ModMask::M1) != 0; + if !is_alt { + return Ok(()); + } + + if !self.managed.contains(&e.event) { + return Ok(()); + } + + let geom = self.conn.get_geometry(e.event)?.reply()?; + let mode = match e.detail { + x if x == ButtonIndex::M1.into() => DragMode::Move, + x if x == ButtonIndex::M3.into() => DragMode::Resize, + _ => return Ok(()), + }; + + self.focus(e.event)?; + + self.conn.grab_pointer( + false, + self.root, + EventMask::BUTTON_RELEASE | EventMask::BUTTON_MOTION, + GrabMode::ASYNC, + GrabMode::ASYNC, + NONE, + NONE, + CURRENT_TIME, + )?; + + self.drag = Some(DragState { + window: e.event, + mode, + start_root_x: e.root_x, + start_root_y: e.root_y, + start_x: i32::from(geom.x), + start_y: i32::from(geom.y), + start_width: geom.width, + start_height: geom.height, + }); + + Ok(()) + } + + fn on_motion(&mut self, e: MotionNotifyEvent) -> Result<(), Box> { + let Some(drag) = self.drag else { + return Ok(()); + }; + + let dx = i32::from(e.root_x) - i32::from(drag.start_root_x); + let dy = i32::from(e.root_y) - i32::from(drag.start_root_y); + + let aux = match drag.mode { + DragMode::Move => ConfigureWindowAux::default().x(drag.start_x + dx).y(drag.start_y + dy), + DragMode::Resize => { + let w = (i32::from(drag.start_width) + dx).max(i32::from(MIN_WIN_SIZE)) as u32; + let h = (i32::from(drag.start_height) + dy).max(i32::from(MIN_WIN_SIZE)) as u32; + ConfigureWindowAux::default().width(w).height(h) + } + }; + + self.conn.configure_window(drag.window, &aux)?; + Ok(()) + } + + fn on_button_release(&mut self, e: ButtonReleaseEvent) -> Result<(), Box> { + let should_stop = match (self.drag, e.detail) { + (Some(DragState { mode: DragMode::Move, .. }), d) if d == ButtonIndex::M1.into() => { + true + } + (Some(DragState { mode: DragMode::Resize, .. }), d) + if d == ButtonIndex::M3.into() => + { + true + } + _ => false, + }; + + if should_stop { + self.drag = None; + self.conn.ungrab_pointer(CURRENT_TIME)?; + } + + Ok(()) + } + + fn manage_window(&mut self, window: Window) -> Result<(), Box> { + if self.managed.contains(&window) { + return Ok(()); + } + + let attrs = self.conn.get_window_attributes(window)?.reply()?; + if attrs.override_redirect { + return Ok(()); + } + + // Passive grabs allow Alt+Button drag even when focus is elsewhere. + for mods in self.modifier_variants(ModMask::M1) { + self.conn.grab_button( + false, + window, + EventMask::BUTTON_PRESS, + GrabMode::ASYNC, + GrabMode::ASYNC, + NONE, + NONE, + ButtonIndex::M1, + mods, + )?; + self.conn.grab_button( + false, + window, + EventMask::BUTTON_PRESS, + GrabMode::ASYNC, + GrabMode::ASYNC, + NONE, + NONE, + ButtonIndex::M3, + mods, + )?; + } + + self.managed.insert(window); + if !self.managed_order.contains(&window) { + self.managed_order.push(window); + } + Ok(()) + } + + fn focus(&mut self, window: Window) -> Result<(), Box> { + if let Some(pos) = self.managed_order.iter().position(|w| *w == window) { + let w = self.managed_order.remove(pos); + self.managed_order.push(w); + } + self.focused = Some(window); + + self.conn + .set_input_focus(InputFocus::POINTER_ROOT, window, CURRENT_TIME)?; + self.conn.configure_window( + window, + &ConfigureWindowAux::default().stack_mode(StackMode::ABOVE), + )?; + Ok(()) + } + + fn close_focused_window(&self) -> Result<(), Box> { + let Some(window) = self.focused else { + return Ok(()); + }; + + let wm_protocols = self + .conn + .intern_atom(false, b"WM_PROTOCOLS")? + .reply()? + .atom; + let wm_delete_window = self + .conn + .intern_atom(false, b"WM_DELETE_WINDOW")? + .reply()? + .atom; + + let protocols = self + .conn + .get_property(false, window, wm_protocols, x11rb::NONE, 0, u32::MAX)? + .reply()?; + + let supports_delete = protocols + .value32() + .map(|atoms| atoms.into_iter().any(|atom| atom == wm_delete_window)) + .unwrap_or(false); + + if supports_delete { + let data = ClientMessageData::from([wm_delete_window, CURRENT_TIME, 0, 0, 0]); + let event = ClientMessageEvent::new(32, window, wm_protocols, data); + self.conn.send_event(false, window, EventMask::NO_EVENT, event)?; + } else { + self.conn.kill_client(window)?; + } + + Ok(()) + } + + fn focus_next_window(&mut self) -> Result<(), Box> { + if self.managed_order.is_empty() { + return Ok(()); + } + + let next_window = match self.focused { + Some(current) => { + let idx = self + .managed_order + .iter() + .position(|w| *w == current) + .unwrap_or(0); + let next_idx = (idx + 1) % self.managed_order.len(); + self.managed_order[next_idx] + } + None => self.managed_order[0], + }; + + self.focus(next_window)?; + self.apply_layout()?; + Ok(()) + } + + fn toggle_layout_mode(&mut self) -> Result<(), Box> { + self.layout_mode = match self.layout_mode { + LayoutMode::Floating => LayoutMode::Tabbed, + LayoutMode::Tabbed => LayoutMode::Floating, + }; + self.apply_layout()?; + Ok(()) + } + + fn apply_layout(&mut self) -> Result<(), Box> { + if self.managed_order.is_empty() { + self.focused = None; + return Ok(()); + } + + if self.focused.is_none() { + self.focused = self.managed_order.last().copied(); + } + + match self.layout_mode { + LayoutMode::Floating => { + for window in &self.managed_order { + let _ = self.conn.map_window(*window); + } + } + LayoutMode::Tabbed => { + let focused = self.focused.unwrap_or(self.managed_order[0]); + let root_geom = self.conn.get_geometry(self.root)?.reply()?; + + for window in &self.managed_order { + if *window == focused { + let _ = self.conn.map_window(*window); + self.conn.configure_window( + *window, + &ConfigureWindowAux::default() + .x(0) + .y(0) + .width(u32::from(root_geom.width)) + .height(u32::from(root_geom.height)), + )?; + } else { + let _ = self.conn.unmap_window(*window); + } + } + + self.focus(focused)?; + } + } + + Ok(()) + } +} + +fn spawn_rofi() -> Result<(), Box> { + let launcher = env::var("DXDWM_LAUNCHER").unwrap_or_else(|_| "rofi -show drun".to_string()); + Command::new("sh") + .arg("-c") + .arg(launcher) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + Ok(()) +} + +fn spawn_terminal() -> Result<(), Box> { + let terminal = env::var("DXDWM_TERMINAL").unwrap_or_else(|_| "alacritty".to_string()); + Command::new("sh") + .arg("-c") + .arg(terminal) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + Ok(()) +} + +fn main() { + if let Err(err) = run() { + eprintln!("dxdwm exited with error: {err}"); + std::process::exit(1); + } +} + +fn run() -> Result<(), Box> { + ensure_display_set()?; + ensure_and_run_startup_script()?; + let mut wm = Wm::new()?; + wm.run() +} + +fn ensure_and_run_startup_script() -> Result<(), Box> { + let home = env::var("HOME").map_err(|_| io::Error::other("HOME is not set"))?; + let config_dir = PathBuf::from(home).join(".config/dxdwm"); + let script_path = config_dir.join("dxdwm.sh"); + + fs::create_dir_all(&config_dir)?; + + if !script_path.exists() { + fs::write( + &script_path, + "#!/usr/bin/env sh\n# dxdwm startup hook\n# Add your startup apps here, one per line.\n\n", + )?; + } + + #[cfg(unix)] + { + let perms = fs::Permissions::from_mode(0o755); + fs::set_permissions(&script_path, perms)?; + } + + Command::new("sh") + .arg(script_path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + + Ok(()) +} + +fn ensure_display_set() -> Result<(), Box> { + let display_is_set = env::var("DISPLAY") + .map(|v| !v.trim().is_empty()) + .unwrap_or(false); + + if !display_is_set { + return Err(io::Error::other( + "DISPLAY is not set. Start this inside an X11 session or use scripts/run_xephyr.sh from an existing X session.", + ) + .into()); + } + + Ok(()) +} + +