From dc8094726026c5b56b5600f95da8475561b1073d Mon Sep 17 00:00:00 2001 From: tcmal Date: Mon, 3 Jun 2024 17:06:15 +0100 Subject: initial commit: basic xorg initialisation --- .gitignore | 3 ++ Cargo.lock | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 8 +++++ shell.nix | 4 +++ src/atoms.rs | 27 +++++++++++++++ src/clients.rs | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 353 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 shell.nix create mode 100644 src/atoms.rs create mode 100644 src/clients.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..264a638 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +.direnv +.envrc diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0f1ca2f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,104 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "blow" +version = "0.1.0" +dependencies = [ + "thiserror", + "xcb", +] + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "proc-macro2" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "xcb" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e75181b5a62b6eeaa72f303d3cef7dbb841e22885bf6d3e66fe23e88c55dc6" +dependencies = [ + "bitflags", + "libc", + "quick-xml", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..76ab7d6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "blow" +version = "0.1.0" +edition = "2021" + +[dependencies] +thiserror = "1.0.61" +xcb = { version = "1.4.0", features = ["xinerama"] } diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..3413d9c --- /dev/null +++ b/shell.nix @@ -0,0 +1,4 @@ +{ + pkgs ? import { }, +}: +pkgs.mkShell { buildInputs = with pkgs; [ xorg.libxcb ]; } diff --git a/src/atoms.rs b/src/atoms.rs new file mode 100644 index 0000000..363e07c --- /dev/null +++ b/src/atoms.rs @@ -0,0 +1,27 @@ +use crate::Result; +use xcb::Connection; + +#[derive(Debug)] +pub struct InternedAtoms {} + +impl InternedAtoms { + pub fn new_with(conn: &Connection) -> Result { + // TODO: intern atoms + + // utf8string = XInternAtom(dpy, "UTF8_STRING", False); + // wmatom[WMProtocols] = XInternAtom(dpy, "WM_PROTOCOLS", False); + // wmatom[WMDelete] = XInternAtom(dpy, "WM_DELETE_WINDOW", False); + // wmatom[WMState] = XInternAtom(dpy, "WM_STATE", False); + // wmatom[WMTakeFocus] = XInternAtom(dpy, "WM_TAKE_FOCUS", False); + // netatom[NetActiveWindow] = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); + // netatom[NetSupported] = XInternAtom(dpy, "_NET_SUPPORTED", False); + // netatom[NetWMName] = XInternAtom(dpy, "_NET_WM_NAME", False); + // netatom[NetWMState] = XInternAtom(dpy, "_NET_WM_STATE", False); + // netatom[NetWMCheck] = XInternAtom(dpy, "_NET_SUPPORTING_WM_CHECK", False); + // netatom[NetWMFullscreen] = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", False); + // netatom[NetWMWindowType] = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False); + // netatom[NetWMWindowTypeDialog] = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DIALOG", False); + // netatom[NetClientList] = XInternAtom(dpy, "_NET_CLIENT_LIST", False); + Ok(Self {}) + } +} diff --git a/src/clients.rs b/src/clients.rs new file mode 100644 index 0000000..70a44fa --- /dev/null +++ b/src/clients.rs @@ -0,0 +1,104 @@ +use xcb::{ + xinerama::{self, ScreenInfo}, + Extension, +}; + +use crate::{Error, Result, WM}; + +#[derive(Debug, Default)] +pub struct ClientList(Vec); + +impl ClientList { + /// Set the new amount of screens, moving clients away if necessary + pub fn truncate_screens(&mut self, new_size: usize) { + // hack: double borrow stuff + let mut moved_clients = vec![]; + for old in self.0.drain(new_size - self.0.len()..self.0.len()) { + moved_clients.extend(old.clients.into_iter()); + } + self.0[0].clients.extend(moved_clients.into_iter()); + } + + pub fn set_screen_info(&mut self, i: usize, info: ScreenInfo) { + while i >= self.0.len() { + self.0.push(MonitorInfo::default()) + } + self.0[i].screen_info = info; + } + + pub fn len(&self) -> usize { + self.0.len() + } +} + +#[derive(Debug)] +pub struct MonitorInfo { + clients: Vec, + screen_info: ScreenInfo, +} + +impl Default for MonitorInfo { + fn default() -> Self { + Self { + clients: vec![], + screen_info: ScreenInfo { + x_org: 0, + y_org: 0, + width: 0, + height: 0, + }, + } + } +} + +#[derive(Debug)] +pub struct Client {} + +impl WM<'_> { + /// Update the client list's recorded monitors and monitor sizes + pub(crate) fn update_geometry(&mut self) -> Result<()> { + if self + .conn + .active_extensions() + .any(|e| e == Extension::Xinerama) + { + let reply = self + .conn + .wait_for_reply(self.conn.send_request(&xinerama::QueryScreens {}))?; + + // Monitor removed, move its clients away + if reply.screen_info().len() > self.clients.len() { + self.clients.truncate_screens(reply.screen_info().len()); + } + + // Update screen info & add new client lists if needed + for (i, monitor) in reply.screen_info().iter().enumerate() { + self.clients.set_screen_info(i, monitor.clone()); + } + } else { + // Only one screen + if self.clients.len() > 1 { + self.clients.truncate_screens(1); + } + + // TODO: it looks like this won't actually update when the screen size changes? + let setup = self.conn.get_setup(); + let screen = setup + .roots() + .nth(self.screen_num as usize) + .ok_or(Error::NoSuchScreen)?; + + self.clients.set_screen_info( + 0, + ScreenInfo { + x_org: 0, + y_org: 0, + width: screen.width_in_pixels(), + height: screen.height_in_pixels(), + }, + ); + } + + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ea42009 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,103 @@ +use atoms::InternedAtoms; +use clients::ClientList; +use thiserror::Error; +use xcb::{ + x::{self, ChangeWindowAttributes, Screen, Window}, + Connection, Extension, +}; + +mod atoms; +mod clients; + +type Result = std::result::Result; + +#[derive(Debug, Error)] +pub enum Error { + #[error("xcb returned screen that doesn't exist")] + NoSuchScreen, + + #[error("other wm is running")] + OtherWMRunning, + + #[error("connection error: {0}")] + ConnectionError(#[from] xcb::ConnError), + + #[error("generic xcb error: {0}")] + XCBError(#[from] xcb::Error), +} + +fn main() -> Result<()> { + // todo: cli stuff + let display_name = ":1"; + + cleanup_process_children(); + + let (conn, screen_num) = + Connection::connect_with_extensions(Some(display_name), &[], &[Extension::Xinerama])?; + + let mut wm = WM::new(&conn, screen_num)?; + wm.event_loop()?; + + Ok(()) +} + +struct WM<'a> { + conn: &'a Connection, + screen_num: i32, + + root: Window, + clients: ClientList, + atoms: InternedAtoms, +} + +impl WM<'_> { + fn new<'a>(conn: &'a Connection, screen_num: i32) -> Result> { + // Fetch root window + let setup = conn.get_setup(); + let screen = setup + .roots() + .nth(screen_num as usize) + .ok_or(Error::NoSuchScreen)?; + + // Check no other WM is running + let cookie = conn.send_request_checked(&ChangeWindowAttributes { + window: screen.root(), + value_list: &[ + x::Cw::BackPixel(screen.white_pixel()), + x::Cw::EventMask(x::EventMask::SUBSTRUCTURE_REDIRECT), + ], + }); + + conn.check_request(cookie) + .map_err(|_| Error::OtherWMRunning)?; + + Ok(WM { + atoms: InternedAtoms::new_with(conn)?, + clients: Default::default(), + conn, + screen_num, + root: screen.root(), + }) + } + + fn event_loop(&mut self) -> Result<()> { + self.update_geometry()?; + + Ok(()) + } +} +fn cleanup_process_children() { + // todo: dont transform children into zombies when they terminate + // todo: cleanup zombies +} + +impl<'a> std::fmt::Debug for WM<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WM") + .field("screen_num", &self.screen_num) + .field("root", &self.root) + .field("clients", &self.clients) + .field("atoms", &self.atoms) + .finish() + } +} -- cgit v1.2.3