summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--Cargo.lock104
-rw-r--r--Cargo.toml8
-rw-r--r--shell.nix4
-rw-r--r--src/atoms.rs27
-rw-r--r--src/clients.rs104
-rw-r--r--src/main.rs103
7 files changed, 353 insertions, 0 deletions
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 <nixpkgs> { },
+}:
+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<Self> {
+ // 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<MonitorInfo>);
+
+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<Client>,
+ 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<T, E = Error> = std::result::Result<T, E>;
+
+#[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<WM<'a>> {
+ // 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()
+ }
+}