Upload files to "/"

This commit is contained in:
Martin Dvořák 2026-04-28 23:48:13 +02:00
commit 41a4341e71
7 changed files with 1024 additions and 0 deletions

21
.gitignore vendored Normal file
View file

@ -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/

93
Cargo.lock generated Normal file
View file

@ -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"

12
Cargo.toml Normal file
View file

@ -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"] }

12
LICENSE Normal file
View file

@ -0,0 +1,12 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
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

88
README.md Normal file
View file

@ -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
```

46
scripts/run_xephyr.sh Normal file
View file

@ -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"

752
src/main.rs Normal file
View file

@ -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<Window>,
super_keycodes: Vec<u8>,
terminal_keycodes: Vec<u8>,
close_keycodes: Vec<u8>,
cycle_keycodes: Vec<u8>,
toggle_mode_keycodes: Vec<u8>,
num_lock_mask: u16,
managed_order: Vec<Window>,
focused: Option<Window>,
layout_mode: LayoutMode,
drag: Option<DragState>,
}
impl Wm {
fn new() -> Result<Self, Box<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
// 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<dyn Error>> {
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<dyn Error>> {
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<Vec<u8>, Box<dyn Error>> {
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<u16, Box<dyn Error>> {
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<ModMask> {
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<dyn Error>> {
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<dyn Error>> {
// 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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
ensure_display_set()?;
ensure_and_run_startup_script()?;
let mut wm = Wm::new()?;
wm.run()
}
fn ensure_and_run_startup_script() -> Result<(), Box<dyn Error>> {
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<dyn Error>> {
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(())
}