From 3427d86d361014e313722c9c02e7c0c337bf237c Mon Sep 17 00:00:00 2001 From: Hilmar Wiegand Date: Sun, 7 Apr 2019 18:58:34 +0200 Subject: [PATCH] :tada: Initial commit --- .gitignore | 2 + Cargo.lock | 74 ++++++++++++ Cargo.toml | 12 ++ src/client.rs | 314 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/keys.rs | 113 ++++++++++++++++++ src/main.rs | 43 +++++++ 6 files changed, 558 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/client.rs create mode 100644 src/keys.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f0e3bca --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b676048 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,74 @@ +[[package]] +name = "cfg-if" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.51" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "log" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pkg-config" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "tidy" +version = "0.1.0" +dependencies = [ + "x11 2.18.1 (registry+https://github.com/rust-lang/crates.io-index)", + "xcb 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", + "xcb-util 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "xdg 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "x11" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "xcb" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "xcb-util" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", + "xcb 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "xdg" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11d43355396e872eefb45ce6342e4374ed7bc2b3a502d1b28e36d6e23c05d1f4" +"checksum libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "bedcc7a809076656486ffe045abeeac163da1b558e963a31e29fbfbeba916917" +"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" +"checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c" +"checksum x11 2.18.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39697e3123f715483d311b5826e254b6f3cfebdd83cf7ef3358f579c3d68e235" +"checksum xcb 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de" +"checksum xcb-util 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5b6ee166167b2d8dbc758fb8fe06757c02e54517ee668831427253bc41e44c83" +"checksum xdg 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8a13305 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "tidy" +version = "0.1.0" +authors = ["Hilmar Wiegand "] +edition = "2018" + +[dependencies] +x11 = "2.18" +xdg = "2.2" +xcb = { version = "0.8", features = ["randr"] } +xcb-util = { version = "0.2", features = ["icccm", "ewmh", "keysyms"] } + diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..0951520 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,314 @@ +use std::collections::HashMap; +use xcb_util::{ewmh, icccm, keysyms::KeySymbols}; +use crate::keys::{self, Command, KeyCombo}; + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum WindowType { + Desktop, + Dock, + Toolbar, + Menu, + Utility, + Splash, + Dialog, + DropdownMenu, + PopupMenu, + Tooltip, + Notification, + Combo, + Dnd, + Normal, +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum WindowState { + Modal, + Sticky, + MaximizedVert, + MaximizedHorz, + Shaded, + SkipTaskbar, + SkipPager, + Hidden, + Fullscreen, + Above, + Below, + DemandsAttention, +} + +macro_rules! atoms { + ( $( $name:ident ),+ ) => { + #[allow(non_snake_case)] + pub struct Atoms { + $( + pub $name: xcb::Atom, + )* + } + + impl Atoms { + pub fn intern(c: &xcb::Connection) -> Self { + $( + let request = xcb::intern_atom(c, false, stringify!($name)); + #[allow(non_snake_case)] + let $name = request.get_reply().expect("Could not intern atom").atom(); + )* + + Self {$( + $name, + )*} + } + } + }; +} + +pub type AtomTable = HashMap; +atoms![WM_DELETE_WINDOW, WM_PROTOCOLS]; + +pub struct Client { + pub connection: ewmh::Connection, + + pub root_window: xcb::Window, + pub screen: Screen, + pub screen_idx: i32, + + pub atoms: Atoms, + pub window_types: AtomTable, + pub window_states: AtomTable, + pub keymap: HashMap, + + pub focused: Option, + pub screens: Vec, +} + +pub struct Screen { + idx: i32, + x: u16, + y: u16, + width: u16, + height: u16, +} + +impl Client { + pub fn open_connection() -> Self { + let (connection, screen_idx) = xcb::Connection::connect(None).unwrap(); + let connection = ewmh::Connection::connect(connection) + .map_err(|(e, _)| e).unwrap(); + + let screen = { + let root = connection.get_setup() + .roots().nth(screen_idx as usize) + .ok_or("Invalid screen").unwrap(); + + Screen { + idx: screen_idx.into(), + x: 0, y: 0, + width: root.width_in_pixels(), + height: root.height_in_pixels(), + } + }; + + let root_window = connection.get_setup() + .roots().nth(screen_idx as usize) + .ok_or("Invalid screen").unwrap().root(); + + let atoms = Atoms::intern(&connection); + + let mut types = HashMap::new(); + let mut states = HashMap::new(); + + types.insert(connection.WM_WINDOW_TYPE_DESKTOP(), WindowType::Desktop); + types.insert(connection.WM_WINDOW_TYPE_DOCK(), WindowType::Dock); + types.insert(connection.WM_WINDOW_TYPE_TOOLBAR(), WindowType::Toolbar); + types.insert(connection.WM_WINDOW_TYPE_MENU(), WindowType::Menu); + types.insert(connection.WM_WINDOW_TYPE_UTILITY(), WindowType::Utility); + types.insert(connection.WM_WINDOW_TYPE_SPLASH(), WindowType::Splash); + types.insert(connection.WM_WINDOW_TYPE_DIALOG(), WindowType::Dialog); + types.insert(connection.WM_WINDOW_TYPE_DROPDOWN_MENU(), WindowType::DropdownMenu); + types.insert(connection.WM_WINDOW_TYPE_POPUP_MENU(), WindowType::PopupMenu); + types.insert(connection.WM_WINDOW_TYPE_TOOLTIP(), WindowType::Tooltip); + types.insert(connection.WM_WINDOW_TYPE_NOTIFICATION(), WindowType::Notification); + types.insert(connection.WM_WINDOW_TYPE_COMBO(), WindowType::Combo); + types.insert(connection.WM_WINDOW_TYPE_DND(), WindowType::Dnd); + types.insert(connection.WM_WINDOW_TYPE_NORMAL(), WindowType::Normal); + + states.insert(connection.WM_STATE_MODAL(), WindowState::Modal); + states.insert(connection.WM_STATE_STICKY(), WindowState::Sticky); + states.insert(connection.WM_STATE_MAXIMIZED_VERT(), WindowState::MaximizedVert); + states.insert(connection.WM_STATE_MAXIMIZED_HORZ(), WindowState::MaximizedHorz); + states.insert(connection.WM_STATE_SHADED(), WindowState::Shaded); + states.insert(connection.WM_STATE_SKIP_TASKBAR(), WindowState::SkipTaskbar); + states.insert(connection.WM_STATE_SKIP_PAGER(), WindowState::SkipPager); + states.insert(connection.WM_STATE_HIDDEN(), WindowState::Hidden); + states.insert(connection.WM_STATE_FULLSCREEN(), WindowState::Fullscreen); + states.insert(connection.WM_STATE_ABOVE(), WindowState::Above); + states.insert(connection.WM_STATE_BELOW(), WindowState::Below); + states.insert(connection.WM_STATE_DEMANDS_ATTENTION(), WindowState::DemandsAttention); + + Self { + connection, + screen, + screen_idx, + root_window, + atoms, + window_types: types, + window_states: states, + keymap: HashMap::new(), + focused: None, + screens: vec![], + } + } + + pub fn register_redirect(&self) { + let values = [( + xcb::CW_EVENT_MASK, + (xcb::EVENT_MASK_SUBSTRUCTURE_NOTIFY | + xcb::EVENT_MASK_SUBSTRUCTURE_REDIRECT | + xcb::EVENT_MASK_PROPERTY_CHANGE | + xcb::EVENT_MASK_BUTTON_PRESS), + )]; + + xcb::change_window_attributes_checked( + &self.connection, + self.root_window, + &values, + ) + .request_check() + .expect("Could not register redirect"); + } + + pub fn register_keybinds( + &mut self, + keybinds: Vec<(KeyCombo, Command)> + ) { + let symbols = KeySymbols::new(&self.connection); + for (combo, command) in keybinds { + self.keymap.insert(combo, command); + + if let Some(code) = symbols.get_keycode(combo.key).next() { + xcb::grab_key( + &self.connection, + false, + self.root_window, + combo.mods as u16, code, + xcb::GRAB_MODE_ASYNC as u8, + xcb::GRAB_MODE_ASYNC as u8, + ); + } + } + } + + pub fn get_window_types(&self, window: xcb::Window) -> Vec { + ewmh::get_wm_window_type(&self.connection, window) + .get_reply() + .map(|it| { + it.atoms() + .iter() + .filter_map(|a| { + self.window_types.get(a).cloned() + }) + .collect() + }) + .unwrap_or_else(|_| vec![]) + } + + pub fn get_window_states(&self, window: xcb::Window) -> Vec { + ewmh::get_wm_state(&self.connection, window) + .get_reply() + .map(|it| { + it.atoms() + .iter() + .filter_map(|a| { + self.window_states.get(a).cloned() + }) + .collect() + }) + .unwrap_or_else(|_| vec![]) + } + + pub fn poll(&mut self) -> Event { + loop { + self.connection.flush(); + let event = self.connection.wait_for_event() + .expect("Wait for event returned none"); + + match event.response_type() { + xcb::CONFIGURE_REQUEST => { + // We can skip this request, just have to pass it on unchanged + let event = unsafe { xcb::cast_event::(&event) }; + + let values = vec![ + (xcb::CONFIG_WINDOW_X as u16, event.x() as u32), + (xcb::CONFIG_WINDOW_Y as u16, event.y() as u32), + (xcb::CONFIG_WINDOW_WIDTH as u16, u32::from(event.width())), + (xcb::CONFIG_WINDOW_HEIGHT as u16, u32::from(event.height())), + (xcb::CONFIG_WINDOW_BORDER_WIDTH as u16, u32::from(event.border_width())), + (xcb::CONFIG_WINDOW_SIBLING as u16, event.sibling() as u32), + (xcb::CONFIG_WINDOW_STACK_MODE as u16, u32::from(event.stack_mode())), + ]; + + let filtered_values: Vec<_> = values + .into_iter() + .filter(|&(mask, _)| mask & event.value_mask() != 0) + .collect(); + + xcb::configure_window(&self.connection, event.window(), &filtered_values); + }, + xcb::MAP_REQUEST => { + let event = unsafe { xcb::cast_event::(&event) }; + return Event::MapRequest(event.window()); + }, + xcb::UNMAP_NOTIFY => { + let event = unsafe { xcb::cast_event::(&event) }; + + if event.event() == self.root_window { + // If it's an event on the root window, ignore it + continue; + } + + return Event::UnmapNotify(event.window()); + }, + xcb::DESTROY_NOTIFY => { + let event = unsafe { xcb::cast_event::(&event) }; + return Event::DestroyNotify(event.event()); + }, + xcb::ENTER_NOTIFY => { + let event = unsafe { xcb::cast_event::(&event) }; + return Event::EnterNotify(event.event()); + }, + + xcb::KEY_PRESS => { + let event = unsafe { xcb::cast_event::(&event) }; + let syms = KeySymbols::new(&self.connection); + let key = syms.press_lookup_keysym(event, 0); + let mods = u32::from(event.state()); + + let combo = KeyCombo { mods, key, event: keys::Event::KeyDown }; + if let Some(command) = self.keymap.get(&combo) { + return Event::Command(command.clone()); + } + }, + xcb::KEY_RELEASE => { + let event = unsafe { xcb::cast_event::(&event) }; + let syms = KeySymbols::new(&self.connection); + let key = syms.press_lookup_keysym(event, 0); + let mods = u32::from(event.state()); + + let combo = KeyCombo { mods, key, event: keys::Event::KeyUp }; + if let Some(command) = self.keymap.get(&combo) { + return Event::Command(command.clone()); + } + }, + _ => {}, + } + } + } +} + +pub enum Event { + MapRequest(xcb::Window), + UnmapNotify(xcb::Window), + DestroyNotify(xcb::Window), + EnterNotify(xcb::Window), + Command(keys::Command), +} + diff --git a/src/keys.rs b/src/keys.rs new file mode 100644 index 0000000..0014230 --- /dev/null +++ b/src/keys.rs @@ -0,0 +1,113 @@ +use xcb; +use x11::keysym::*; +use std::{os::raw::c_uint, collections::HashMap}; + +pub type ModMaskCode = c_uint; +pub type KeyCode = c_uint; + +#[derive(Debug, Clone)] +pub enum Command { + Spawn(&'static str), + CloseWindow, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct KeyCombo { + pub mods: ModMaskCode, + pub key: KeyCode, + pub event: Event, +} + +impl KeyCombo { + pub fn new(event: Event, mods: &[Mod], key: Key) -> Self { + let mods = mods.iter().fold(0, |acc, it| acc | **it); + let key = key as KeyCode; + Self { mods, key, event } + } +} + +impl std::convert::From<(u32, u32)> for KeyCombo { + fn from(item: (u32, u32)) -> Self { + KeyCombo { + mods: item.0, + key: item.1, + event: Event::KeyDown, + } + } +} + +pub type KeyBinds = HashMap; + +#[macro_export] +macro_rules! keybinds { + ( $( $bind:tt )+ ) => { + { + use crate::keys::{KeyCombo, Command}; + + let binds: Vec<(KeyCombo, Command)> = vec![ + $( + keybind!($bind) + ),* + ]; + + binds + } + }; +} + +#[macro_export] +macro_rules! keybind { + ( (on $ev:expr => $key:ident | $cmd:expr) ) => { + (KeyCombo::new($ev, &[], $key), $cmd) + }; + + ( (on $ev:expr => [$( $mod:ident ),*] + $key:ident | $cmd:expr) ) => { + (KeyCombo::new($ev, &[$($mod),*], $key), $cmd) + }; + + ( ($key:ident | $cmd:expr) ) => { + (KeyCombo::new($crate::keys::Event::KeyDown, &[], $key), $cmd) + }; + + ( ([$( $mod:ident ),*] + $key:ident | $cmd:expr) ) => { + (KeyCombo::new($crate::keys::Event::KeyDown, &[$($mod),*], $key), $cmd) + }; +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Mod { + Shift, + Caps, + Control, + Alt, + Super, +} + +impl std::ops::Deref for Mod { + type Target = ModMaskCode; + fn deref(&self) -> &ModMaskCode { + use Mod::*; + match *self { + Shift => &xcb::MOD_MASK_SHIFT, + Caps => &xcb::MOD_MASK_LOCK, + Control => &xcb::MOD_MASK_CONTROL, + Alt => &xcb::MOD_MASK_1, + Super => &xcb::MOD_MASK_4, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum Event { + KeyDown, + KeyUp, +} + +pub enum Key { + Q = XK_q as isize, + X = XK_x as isize, + C = XK_c as isize, + H = XK_h as isize, + L = XK_l as isize, +} + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6b7bf6f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,43 @@ +#![allow(dead_code)] +#![allow(unused_variables)] +#![allow(unused_imports)] + +use x11; +use xcb; + +mod keys; +mod client; + +use client::Client; + +fn main() { + let mut client = Client::open_connection(); + client.register_redirect(); + client.register_keybinds(keybinds()); + + loop { + use client::Event::*; + + match client.poll() { + MapRequest(w) => {}, + UnmapNotify(w) => {}, + DestroyNotify(w) => {}, + EnterNotify(w) => {}, + Command(cmd) => {}, + } + } +} + +fn keybinds() -> Vec<(keys::KeyCombo, keys::Command)> { + use keys::{ + Command::*, + Key::*, + Event::*, + }; + + keybinds! { + (C | Spawn("alacritty")) + (Q | CloseWindow) + } +} +