Upload files to "/"
This commit is contained in:
commit
41a4341e71
7 changed files with 1024 additions and 0 deletions
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal 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
93
Cargo.lock
generated
Normal 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
12
Cargo.toml
Normal 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
12
LICENSE
Normal 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
88
README.md
Normal 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
46
scripts/run_xephyr.sh
Normal 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
752
src/main.rs
Normal 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(())
|
||||
}
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue