diff --git a/.builds/unit-tests.yml b/.builds/unit-tests.yml index f1c16c02..18f5cba5 100644 --- a/.builds/unit-tests.yml +++ b/.builds/unit-tests.yml @@ -11,3 +11,8 @@ tasks: - test: | cd jay cargo test + - test-tc: | + cd jay + git submodule update --init + cd toml-config + cargo test diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 8bcbf815..fbed99a5 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -987,6 +987,18 @@ impl ConfigClient { self.send(&ClientMessage::SetMiddleClickPasteEnabled { enabled }); } + pub fn seat_create_mark(&self, seat: Seat, kc: Option) { + self.send(&ClientMessage::SeatCreateMark { seat, kc }); + } + + pub fn seat_jump_to_mark(&self, seat: Seat, kc: Option) { + self.send(&ClientMessage::SeatJumpToMark { seat, kc }); + } + + pub fn seat_copy_mark(&self, seat: Seat, src: u32, dst: u32) { + self.send(&ClientMessage::SeatCopyMark { seat, src, dst }); + } + pub fn set_show_float_pin_icon(&self, show: bool) { self.send(&ClientMessage::SetShowFloatPinIcon { show }); } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 71a12816..bde2aacb 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -747,6 +747,19 @@ pub enum ClientMessage<'a> { SetMiddleClickPasteEnabled { enabled: bool, }, + SeatCreateMark { + seat: Seat, + kc: Option, + }, + SeatJumpToMark { + seat: Seat, + kc: Option, + }, + SeatCopyMark { + seat: Seat, + src: u32, + dst: u32, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index f1319203..cebf72f5 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -566,6 +566,34 @@ impl Seat { pub fn set_pointer_revert_key(self, sym: KeySym) { get!().set_pointer_revert_key(self, sym); } + + /// Creates a mark for the currently focused window. + /// + /// `kc` should be an evdev keycode. If `kc` is none, then the keycode will be + /// inferred from the next key press. Pressing escape during this interactive + /// selection aborts the process. + /// + /// Currently very few `u32` are valid keycodes. Large numbers can therefore be used + /// to create marks that do not correspond to a key. However, `kc` should always be + /// less than `u32::MAX - 8`. + pub fn create_mark(self, kc: Option) { + get!().seat_create_mark(self, kc); + } + + /// Moves the keyboard focus to a window identified by a mark. + /// + /// See [`Seat::create_mark`] for information about the `kc` parameter. + pub fn jump_to_mark(self, kc: Option) { + get!().seat_jump_to_mark(self, kc); + } + + /// Copies a mark from one keycode to another. + /// + /// If the `src` keycode identifies a mark before this function is called, the `dst` + /// keycode will identify the same mark afterwards. + pub fn copy_mark(self, src: u32, dst: u32) { + get!().seat_copy_mark(self, src, dst); + } } /// A focus-follows-mouse mode. diff --git a/src/config/handler.rs b/src/config/handler.rs index e9c86240..2a6c0511 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -76,6 +76,7 @@ use { window::{TileState, Window, WindowMatcher}, xwayland::XScalingMode, }, + kbvm::Keycode, libloading::Library, log::Level, regex::Regex, @@ -2208,6 +2209,32 @@ impl ConfigProxyHandler { self.state.enable_primary_selection.set(enabled); } + fn handle_seat_create_mark(&self, seat: Seat, kc: Option) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + if let Some(kc) = kc { + seat.create_mark(Keycode::from_evdev(kc)); + } else { + seat.create_mark_interactive(); + } + Ok(()) + } + + fn handle_seat_jump_to_mark(&self, seat: Seat, kc: Option) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + if let Some(kc) = kc { + seat.jump_to_mark(Keycode::from_evdev(kc)); + } else { + seat.jump_to_mark_interactive(); + } + Ok(()) + } + + fn handle_seat_copy_mark(&self, seat: Seat, src: u32, dst: u32) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + seat.copy_mark(Keycode::from_evdev(src), Keycode::from_evdev(dst)); + Ok(()) + } + fn spaces_change(&self) { struct V; impl NodeVisitorBase for V { @@ -3071,6 +3098,15 @@ impl ConfigProxyHandler { ClientMessage::SetMiddleClickPasteEnabled { enabled } => { self.handle_set_middle_click_paste_enabled(enabled) } + ClientMessage::SeatCreateMark { seat, kc } => self + .handle_seat_create_mark(seat, kc) + .wrn("seat_create_mark")?, + ClientMessage::SeatJumpToMark { seat, kc } => self + .handle_seat_jump_to_mark(seat, kc) + .wrn("seat_jump_to_mark")?, + ClientMessage::SeatCopyMark { seat, src, dst } => self + .handle_seat_copy_mark(seat, src, dst) + .wrn("seat_copy_mark")?, } Ok(()) } diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index abda9a9e..87b5fd2e 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -106,6 +106,7 @@ use { }, ahash::AHashMap, jay_config::keyboard::syms::{KeySym, SYM_Escape}, + kbvm::Keycode, smallvec::SmallVec, std::{ cell::{Cell, RefCell}, @@ -232,6 +233,14 @@ pub struct WlSeatGlobal { focus_history_rotate: NumCell, focus_history_visible_only: Cell, focus_history_same_workspace: Cell, + mark_mode: Cell>, + marks: CopyHashMap>, +} + +#[derive(Copy, Clone)] +enum MarkMode { + Mark, + Jump, } const CHANGE_CURSOR_MOVED: u32 = 1 << 0; @@ -311,6 +320,8 @@ impl WlSeatGlobal { focus_history_rotate: Default::default(), focus_history_visible_only: Cell::new(false), focus_history_same_workspace: Cell::new(false), + mark_mode: Default::default(), + marks: Default::default(), }); slf.pointer_cursor.set_owner(slf.clone()); let seat = slf.clone(); @@ -1186,6 +1197,7 @@ impl WlSeatGlobal { self.cursor_user_group.detach(); self.tablet_clear(); self.ei_seats.clear(); + self.marks.clear(); } pub fn id(&self) -> SeatId { diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index f0229097..42586ed2 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -17,7 +17,7 @@ use { }, }, wl_seat::{ - CHANGE_CURSOR_MOVED, CHANGE_TREE, Dnd, SeatId, WlSeat, WlSeatGlobal, + CHANGE_CURSOR_MOVED, CHANGE_TREE, Dnd, MarkMode, SeatId, WlSeat, WlSeatGlobal, tablet::{TabletPad, TabletPadId, TabletTool, TabletToolId}, text_input::TextDisconnectReason, wl_keyboard::WlKeyboard, @@ -56,7 +56,7 @@ use { syms::KeySym, }, }, - kbvm::{ModifierMask, state_machine::Event}, + kbvm::{Keycode, ModifierMask, evdev, state_machine::Event}, linearize::LinearizeExt, smallvec::SmallVec, std::{ @@ -80,6 +80,12 @@ pub struct NodeSeatState { tablet_pad_foci: SmallMap, 1>, tablet_tool_foci: SmallMap, 1>, ui_drags: SmallMap, 1>, + marks: RefCell>, +} + +struct Marks { + seat: Rc, + marks: SmallMapMut, } pub struct FocusHistoryData { @@ -217,6 +223,12 @@ impl NodeSeatState { entry.visible.set(false); entry.detach(); } + for (_, marks) in self.marks.borrow_mut().iter_mut() { + for (kc, _) in &marks.marks { + marks.seat.marks.remove(kc); + } + marks.marks.clear(); + } self.destroy_node2(node, true); } @@ -870,6 +882,23 @@ impl WlSeatGlobal { KeyState::Pressed => pk.insert(kc.to_evdev()), } }; + if key_state == KeyState::Pressed + && let Some(mode) = self.mark_mode.take() + { + update_pressed_keys(&mut kbvm_state); + if kc == evdev::ESC { + continue; + } + match mode { + MarkMode::Mark => self.create_mark(kc), + MarkMode::Jump => { + drop(kbvm_state); + self.jump_to_mark(kc); + kbvm_state = kbvm_state_rc.borrow_mut(); + } + } + continue; + } shortcuts.clear(); { let mut mods = kbvm_state.kb_state.mods.mods.0 & !(CAPS.0 | NUM.0); @@ -949,6 +978,60 @@ impl WlSeatGlobal { self.send_components(&mut components_changed, &kbvm_state); } + pub fn create_mark_interactive(&self) { + self.mark_mode.set(Some(MarkMode::Mark)); + } + + pub fn create_mark(self: &Rc, kc: Keycode) { + self.create_mark_(kc, self.keyboard_node.get()); + } + + fn create_mark_(self: &Rc, kc: Keycode, node: Rc) { + let prev = self.marks.set(kc, node.clone()); + if let Some(prev) = prev { + if prev.node_id() == node.node_id() { + return; + } + if let Some(marks) = prev.node_seat_state().marks.borrow_mut().get_mut(&self.id) { + marks.marks.remove(&kc); + } + } + node.node_seat_state() + .marks + .borrow_mut() + .get_or_insert_with(self.id, || Marks { + seat: self.clone(), + marks: Default::default(), + }) + .marks + .insert(kc, ()); + } + + pub fn jump_to_mark_interactive(&self) { + self.mark_mode.set(Some(MarkMode::Jump)); + } + + pub fn jump_to_mark(self: &Rc, kc: Keycode) { + if let Some(node) = self.marks.get(&kc) + && node.node_accepts_focus() + && node.node_id() != self.keyboard_node.get().node_id() + { + if !node.node_visible() { + node.clone().node_make_visible(); + if !node.node_visible() { + return; + } + } + self.focus_node(node); + } + } + + pub fn copy_mark(self: &Rc, src: Keycode, dst: Keycode) { + if let Some(node) = self.marks.get(&src) { + self.create_mark_(dst, node); + } + } + fn send_components(&self, components_changed: &mut bool, kbvm_state: &KbvmState) { if !mem::take(components_changed) { return; diff --git a/src/utils/smallmap.rs b/src/utils/smallmap.rs index c4ea872e..cd2f155a 100644 --- a/src/utils/smallmap.rs +++ b/src/utils/smallmap.rs @@ -198,6 +198,15 @@ impl SmallMapMut { None } + pub fn get_mut(&mut self, k: &K) -> Option<&mut V> { + for (ek, ev) in &mut self.m { + if ek == k { + return Some(ev); + } + } + None + } + pub fn get_or_default_mut(&mut self, k: K) -> &mut V where V: Default, diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index a23d2d58..54eebb7d 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -1,6 +1,7 @@ mod context; pub mod error; mod extractor; +mod keycodes; mod keysyms; mod parser; mod parsers; @@ -36,6 +37,7 @@ use { xwayland::XScalingMode, }, std::{ + cell::RefCell, error::Error, fmt::{Display, Formatter}, rc::Rc, @@ -77,6 +79,8 @@ pub enum SimpleCommand { FocusHistory(Timeline), FocusLayerRel(LayerDirection), FocusTiles, + CreateMark, + JumpToMark, } #[derive(Debug, Clone)] @@ -160,6 +164,9 @@ pub enum Action { NamedAction { name: String, }, + CreateMark(u32), + JumpToMark(u32), + CopyMark(u32, u32), } #[derive(Debug, Clone, Default)] @@ -505,13 +512,18 @@ pub enum ConfigError { Parser(#[from] ConfigParserError), } -pub fn parse_config(input: &[u8], handle_error: F) -> Option +pub fn parse_config( + input: &[u8], + mark_names: &RefCell>, + handle_error: F, +) -> Option where F: FnOnce(&dyn Error), { let cx = Context { input, used: Default::default(), + mark_names, }; macro_rules! fatal { ($e:expr) => {{ @@ -554,5 +566,5 @@ where #[test] fn default_config_parses() { let input = include_bytes!("default-config.toml"); - parse_config(input, |_| ()).unwrap(); + parse_config(input, &Default::default(), |_| ()).unwrap(); } diff --git a/toml-config/src/config/context.rs b/toml-config/src/config/context.rs index 8dd95d3d..5ffb015c 100644 --- a/toml-config/src/config/context.rs +++ b/toml-config/src/config/context.rs @@ -6,7 +6,7 @@ use { toml_span::{Span, Spanned}, }, }, - ahash::AHashSet, + ahash::{AHashMap, AHashSet}, error_reporter::Report, std::{cell::RefCell, convert::Infallible, error::Error}, }; @@ -14,6 +14,7 @@ use { pub struct Context<'a> { pub input: &'a [u8], pub used: RefCell, + pub mark_names: &'a RefCell>, } #[derive(Default)] diff --git a/toml-config/src/config/keycodes.rs b/toml-config/src/config/keycodes.rs new file mode 100644 index 00000000..83deb2b5 --- /dev/null +++ b/toml-config/src/config/keycodes.rs @@ -0,0 +1,520 @@ +use phf::phf_map; + +pub static KEYCODES: phf::Map<&'static str, u32> = phf_map! { + "esc" => 1, + "1" => 2, + "2" => 3, + "3" => 4, + "4" => 5, + "5" => 6, + "6" => 7, + "7" => 8, + "8" => 9, + "9" => 10, + "0" => 11, + "minus" => 12, + "equal" => 13, + "backspace" => 14, + "tab" => 15, + "q" => 16, + "w" => 17, + "e" => 18, + "r" => 19, + "t" => 20, + "y" => 21, + "u" => 22, + "i" => 23, + "o" => 24, + "p" => 25, + "leftbrace" => 26, + "rightbrace" => 27, + "enter" => 28, + "leftctrl" => 29, + "a" => 30, + "s" => 31, + "d" => 32, + "f" => 33, + "g" => 34, + "h" => 35, + "j" => 36, + "k" => 37, + "l" => 38, + "semicolon" => 39, + "apostrophe" => 40, + "grave" => 41, + "leftshift" => 42, + "backslash" => 43, + "z" => 44, + "x" => 45, + "c" => 46, + "v" => 47, + "b" => 48, + "n" => 49, + "m" => 50, + "comma" => 51, + "dot" => 52, + "slash" => 53, + "rightshift" => 54, + "kpasterisk" => 55, + "leftalt" => 56, + "space" => 57, + "capslock" => 58, + "f1" => 59, + "f2" => 60, + "f3" => 61, + "f4" => 62, + "f5" => 63, + "f6" => 64, + "f7" => 65, + "f8" => 66, + "f9" => 67, + "f10" => 68, + "numlock" => 69, + "scrolllock" => 70, + "kp7" => 71, + "kp8" => 72, + "kp9" => 73, + "kpminus" => 74, + "kp4" => 75, + "kp5" => 76, + "kp6" => 77, + "kpplus" => 78, + "kp1" => 79, + "kp2" => 80, + "kp3" => 81, + "kp0" => 82, + "kpdot" => 83, + "zenkakuhankaku" => 85, + "102nd" => 86, + "f11" => 87, + "f12" => 88, + "ro" => 89, + "katakana" => 90, + "hiragana" => 91, + "henkan" => 92, + "katakanahiragana" => 93, + "muhenkan" => 94, + "kpjpcomma" => 95, + "kpenter" => 96, + "rightctrl" => 97, + "kpslash" => 98, + "sysrq" => 99, + "rightalt" => 100, + "linefeed" => 101, + "home" => 102, + "up" => 103, + "pageup" => 104, + "left" => 105, + "right" => 106, + "end" => 107, + "down" => 108, + "pagedown" => 109, + "insert" => 110, + "delete" => 111, + "macro" => 112, + "mute" => 113, + "volumedown" => 114, + "volumeup" => 115, + "power" => 116, + "kpequal" => 117, + "kpplusminus" => 118, + "pause" => 119, + "scale" => 120, + "kpcomma" => 121, + "hangeul" => 122, + "hanguel" => 122, + "hanja" => 123, + "yen" => 124, + "leftmeta" => 125, + "rightmeta" => 126, + "compose" => 127, + "stop" => 128, + "again" => 129, + "props" => 130, + "undo" => 131, + "front" => 132, + "copy" => 133, + "open" => 134, + "paste" => 135, + "find" => 136, + "cut" => 137, + "help" => 138, + "menu" => 139, + "calc" => 140, + "setup" => 141, + "sleep" => 142, + "wakeup" => 143, + "file" => 144, + "sendfile" => 145, + "deletefile" => 146, + "xfer" => 147, + "prog1" => 148, + "prog2" => 149, + "www" => 150, + "msdos" => 151, + "coffee" => 152, + "screenlock" => 152, + "rotate_display" => 153, + "direction" => 153, + "cyclewindows" => 154, + "mail" => 155, + "bookmarks" => 156, + "computer" => 157, + "back" => 158, + "forward" => 159, + "closecd" => 160, + "ejectcd" => 161, + "ejectclosecd" => 162, + "nextsong" => 163, + "playpause" => 164, + "previoussong" => 165, + "stopcd" => 166, + "record" => 167, + "rewind" => 168, + "phone" => 169, + "iso" => 170, + "config" => 171, + "homepage" => 172, + "refresh" => 173, + "exit" => 174, + "move" => 175, + "edit" => 176, + "scrollup" => 177, + "scrolldown" => 178, + "kpleftparen" => 179, + "kprightparen" => 180, + "new" => 181, + "redo" => 182, + "f13" => 183, + "f14" => 184, + "f15" => 185, + "f16" => 186, + "f17" => 187, + "f18" => 188, + "f19" => 189, + "f20" => 190, + "f21" => 191, + "f22" => 192, + "f23" => 193, + "f24" => 194, + "playcd" => 200, + "pausecd" => 201, + "prog3" => 202, + "prog4" => 203, + "all_applications" => 204, + "dashboard" => 204, + "suspend" => 205, + "close" => 206, + "play" => 207, + "fastforward" => 208, + "bassboost" => 209, + "print" => 210, + "hp" => 211, + "camera" => 212, + "sound" => 213, + "question" => 214, + "email" => 215, + "chat" => 216, + "search" => 217, + "connect" => 218, + "finance" => 219, + "sport" => 220, + "shop" => 221, + "alterase" => 222, + "cancel" => 223, + "brightnessdown" => 224, + "brightnessup" => 225, + "media" => 226, + "switchvideomode" => 227, + "kbdillumtoggle" => 228, + "kbdillumdown" => 229, + "kbdillumup" => 230, + "send" => 231, + "reply" => 232, + "forwardmail" => 233, + "save" => 234, + "documents" => 235, + "battery" => 236, + "bluetooth" => 237, + "wlan" => 238, + "uwb" => 239, + "unknown" => 240, + "video_next" => 241, + "video_prev" => 242, + "brightness_cycle" => 243, + "brightness_auto" => 244, + "brightness_zero" => 244, + "display_off" => 245, + "wwan" => 246, + "wimax" => 246, + "rfkill" => 247, + "micmute" => 248, + "ok" => 0x160, + "select" => 0x161, + "goto" => 0x162, + "clear" => 0x163, + "power2" => 0x164, + "option" => 0x165, + "info" => 0x166, + "time" => 0x167, + "vendor" => 0x168, + "archive" => 0x169, + "program" => 0x16a, + "channel" => 0x16b, + "favorites" => 0x16c, + "epg" => 0x16d, + "pvr" => 0x16e, + "mhp" => 0x16f, + "language" => 0x170, + "title" => 0x171, + "subtitle" => 0x172, + "angle" => 0x173, + "full_screen" => 0x174, + "zoom" => 0x174, + "mode" => 0x175, + "keyboard" => 0x176, + "aspect_ratio" => 0x177, + "screen" => 0x177, + "pc" => 0x178, + "tv" => 0x179, + "tv2" => 0x17a, + "vcr" => 0x17b, + "vcr2" => 0x17c, + "sat" => 0x17d, + "sat2" => 0x17e, + "cd" => 0x17f, + "tape" => 0x180, + "radio" => 0x181, + "tuner" => 0x182, + "player" => 0x183, + "text" => 0x184, + "dvd" => 0x185, + "aux" => 0x186, + "mp3" => 0x187, + "audio" => 0x188, + "video" => 0x189, + "directory" => 0x18a, + "list" => 0x18b, + "memo" => 0x18c, + "calendar" => 0x18d, + "red" => 0x18e, + "green" => 0x18f, + "yellow" => 0x190, + "blue" => 0x191, + "channelup" => 0x192, + "channeldown" => 0x193, + "first" => 0x194, + "last" => 0x195, + "ab" => 0x196, + "next" => 0x197, + "restart" => 0x198, + "slow" => 0x199, + "shuffle" => 0x19a, + "break" => 0x19b, + "previous" => 0x19c, + "digits" => 0x19d, + "teen" => 0x19e, + "twen" => 0x19f, + "videophone" => 0x1a0, + "games" => 0x1a1, + "zoomin" => 0x1a2, + "zoomout" => 0x1a3, + "zoomreset" => 0x1a4, + "wordprocessor" => 0x1a5, + "editor" => 0x1a6, + "spreadsheet" => 0x1a7, + "graphicseditor" => 0x1a8, + "presentation" => 0x1a9, + "database" => 0x1aa, + "news" => 0x1ab, + "voicemail" => 0x1ac, + "addressbook" => 0x1ad, + "messenger" => 0x1ae, + "displaytoggle" => 0x1af, + "brightness_toggle" => 0x1af, + "spellcheck" => 0x1b0, + "logoff" => 0x1b1, + "dollar" => 0x1b2, + "euro" => 0x1b3, + "frameback" => 0x1b4, + "frameforward" => 0x1b5, + "context_menu" => 0x1b6, + "media_repeat" => 0x1b7, + "10channelsup" => 0x1b8, + "10channelsdown" => 0x1b9, + "images" => 0x1ba, + "notification_center" => 0x1bc, + "pickup_phone" => 0x1bd, + "hangup_phone" => 0x1be, + "del_eol" => 0x1c0, + "del_eos" => 0x1c1, + "ins_line" => 0x1c2, + "del_line" => 0x1c3, + "fn" => 0x1d0, + "fn_esc" => 0x1d1, + "fn_f1" => 0x1d2, + "fn_f2" => 0x1d3, + "fn_f3" => 0x1d4, + "fn_f4" => 0x1d5, + "fn_f5" => 0x1d6, + "fn_f6" => 0x1d7, + "fn_f7" => 0x1d8, + "fn_f8" => 0x1d9, + "fn_f9" => 0x1da, + "fn_f10" => 0x1db, + "fn_f11" => 0x1dc, + "fn_f12" => 0x1dd, + "fn_1" => 0x1de, + "fn_2" => 0x1df, + "fn_d" => 0x1e0, + "fn_e" => 0x1e1, + "fn_f" => 0x1e2, + "fn_s" => 0x1e3, + "fn_b" => 0x1e4, + "fn_right_shift" => 0x1e5, + "brl_dot1" => 0x1f1, + "brl_dot2" => 0x1f2, + "brl_dot3" => 0x1f3, + "brl_dot4" => 0x1f4, + "brl_dot5" => 0x1f5, + "brl_dot6" => 0x1f6, + "brl_dot7" => 0x1f7, + "brl_dot8" => 0x1f8, + "brl_dot9" => 0x1f9, + "brl_dot10" => 0x1fa, + "numeric_0" => 0x200, + "numeric_1" => 0x201, + "numeric_2" => 0x202, + "numeric_3" => 0x203, + "numeric_4" => 0x204, + "numeric_5" => 0x205, + "numeric_6" => 0x206, + "numeric_7" => 0x207, + "numeric_8" => 0x208, + "numeric_9" => 0x209, + "numeric_star" => 0x20a, + "numeric_pound" => 0x20b, + "numeric_a" => 0x20c, + "numeric_b" => 0x20d, + "numeric_c" => 0x20e, + "numeric_d" => 0x20f, + "camera_focus" => 0x210, + "wps_button" => 0x211, + "touchpad_toggle" => 0x212, + "touchpad_on" => 0x213, + "touchpad_off" => 0x214, + "camera_zoomin" => 0x215, + "camera_zoomout" => 0x216, + "camera_up" => 0x217, + "camera_down" => 0x218, + "camera_left" => 0x219, + "camera_right" => 0x21a, + "attendant_on" => 0x21b, + "attendant_off" => 0x21c, + "attendant_toggle" => 0x21d, + "lights_toggle" => 0x21e, + "als_toggle" => 0x230, + "rotate_lock_toggle" => 0x231, + "refresh_rate_toggle" => 0x232, + "buttonconfig" => 0x240, + "taskmanager" => 0x241, + "journal" => 0x242, + "controlpanel" => 0x243, + "appselect" => 0x244, + "screensaver" => 0x245, + "voicecommand" => 0x246, + "assistant" => 0x247, + "kbd_layout_next" => 0x248, + "emoji_picker" => 0x249, + "dictate" => 0x24a, + "camera_access_enable" => 0x24b, + "camera_access_disable" => 0x24c, + "camera_access_toggle" => 0x24d, + "accessibility" => 0x24e, + "do_not_disturb" => 0x24f, + "brightness_min" => 0x250, + "brightness_max" => 0x251, + "kbdinputassist_prev" => 0x260, + "kbdinputassist_next" => 0x261, + "kbdinputassist_prevgroup" => 0x262, + "kbdinputassist_nextgroup" => 0x263, + "kbdinputassist_accept" => 0x264, + "kbdinputassist_cancel" => 0x265, + "right_up" => 0x266, + "right_down" => 0x267, + "left_up" => 0x268, + "left_down" => 0x269, + "root_menu" => 0x26a, + "media_top_menu" => 0x26b, + "numeric_11" => 0x26c, + "numeric_12" => 0x26d, + "audio_desc" => 0x26e, + "3d_mode" => 0x26f, + "next_favorite" => 0x270, + "stop_record" => 0x271, + "pause_record" => 0x272, + "vod" => 0x273, + "unmute" => 0x274, + "fastreverse" => 0x275, + "slowreverse" => 0x276, + "data" => 0x277, + "onscreen_keyboard" => 0x278, + "privacy_screen_toggle" => 0x279, + "selective_screenshot" => 0x27a, + "next_element" => 0x27b, + "previous_element" => 0x27c, + "autopilot_engage_toggle" => 0x27d, + "mark_waypoint" => 0x27e, + "sos" => 0x27f, + "nav_chart" => 0x280, + "fishing_chart" => 0x281, + "single_range_radar" => 0x282, + "dual_range_radar" => 0x283, + "radar_overlay" => 0x284, + "traditional_sonar" => 0x285, + "clearvu_sonar" => 0x286, + "sidevu_sonar" => 0x287, + "nav_info" => 0x288, + "brightness_menu" => 0x289, + "macro1" => 0x290, + "macro2" => 0x291, + "macro3" => 0x292, + "macro4" => 0x293, + "macro5" => 0x294, + "macro6" => 0x295, + "macro7" => 0x296, + "macro8" => 0x297, + "macro9" => 0x298, + "macro10" => 0x299, + "macro11" => 0x29a, + "macro12" => 0x29b, + "macro13" => 0x29c, + "macro14" => 0x29d, + "macro15" => 0x29e, + "macro16" => 0x29f, + "macro17" => 0x2a0, + "macro18" => 0x2a1, + "macro19" => 0x2a2, + "macro20" => 0x2a3, + "macro21" => 0x2a4, + "macro22" => 0x2a5, + "macro23" => 0x2a6, + "macro24" => 0x2a7, + "macro25" => 0x2a8, + "macro26" => 0x2a9, + "macro27" => 0x2aa, + "macro28" => 0x2ab, + "macro29" => 0x2ac, + "macro30" => 0x2ad, + "macro_record_start" => 0x2b0, + "macro_record_stop" => 0x2b1, + "macro_preset_cycle" => 0x2b2, + "macro_preset1" => 0x2b3, + "macro_preset2" => 0x2b4, + "macro_preset3" => 0x2b5, + "kbd_lcd_menu1" => 0x2b8, + "kbd_lcd_menu2" => 0x2b9, + "kbd_lcd_menu3" => 0x2ba, + "kbd_lcd_menu4" => 0x2bb, + "kbd_lcd_menu5" => 0x2bc, +}; diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index 15952e3c..c04ecaaa 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -30,6 +30,7 @@ mod input_match; pub mod keymap; mod libei; mod log_level; +pub mod mark_id; mod mode; pub mod modified_keysym; mod output; diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index 48428cf1..3797e063 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -1,7 +1,7 @@ use { crate::{ config::{ - Action, + Action, SimpleCommand, context::Context, extractor::{Extractor, ExtractorError, arr, bol, n32, opt, str, val}, parser::{DataType, ParseResult, Parser, UnexpectedDataType}, @@ -17,6 +17,7 @@ use { input::{InputParser, InputParserError}, keymap::{KeymapParser, KeymapParserError}, log_level::{LogLevelParser, LogLevelParserError}, + mark_id::{MarkIdParser, MarkIdParserError}, output::{OutputParser, OutputParserError}, output_match::{OutputMatchParser, OutputMatchParserError}, repeat_rate::{RepeatRateParser, RepeatRateParserError}, @@ -81,6 +82,12 @@ pub enum ActionParserError { MoveToOutput(#[source] OutputMatchParserError), #[error("Could not parse a set-repeat-rate action")] RepeatRate(#[source] RepeatRateParserError), + #[error("Could not parse a create-mark action")] + CreateMark(#[source] MarkIdParserError), + #[error("Could not parse a jump-to-mark action")] + JumpToMark(#[source] MarkIdParserError), + #[error("Could not parse a copy-mark action")] + CopyMark(#[source] MarkIdParserError), } pub struct ActionParser<'a>(pub &'a Context<'a>); @@ -142,6 +149,8 @@ impl ActionParser<'_> { "focus-below" => FocusLayerRel(LayerDirection::Below), "focus-above" => FocusLayerRel(LayerDirection::Above), "focus-tiles" => FocusTiles, + "create-mark" => CreateMark, + "jump-to-mark" => JumpToMark, _ => { return Err( ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span) @@ -368,6 +377,43 @@ impl ActionParser<'_> { name: name.value.to_string(), }) } + + fn parse_create_mark(&mut self, ext: &mut Extractor<'_>) -> ParseResult { + let (id,) = ext.extract((opt(val("id")),))?; + let Some(id) = id else { + return Ok(Action::SimpleCommand { + cmd: SimpleCommand::CreateMark, + }); + }; + let id = id + .parse(&mut MarkIdParser(self.0)) + .map_spanned_err(ActionParserError::CreateMark)?; + Ok(Action::CreateMark(id)) + } + + fn parse_jump_to_mark(&mut self, ext: &mut Extractor<'_>) -> ParseResult { + let (id,) = ext.extract((opt(val("id")),))?; + let Some(id) = id else { + return Ok(Action::SimpleCommand { + cmd: SimpleCommand::JumpToMark, + }); + }; + let id = id + .parse(&mut MarkIdParser(self.0)) + .map_spanned_err(ActionParserError::JumpToMark)?; + Ok(Action::JumpToMark(id)) + } + + fn parse_copy_mark(&mut self, ext: &mut Extractor<'_>) -> ParseResult { + let (src, dst) = ext.extract((val("src"), val("dst")))?; + let src = src + .parse(&mut MarkIdParser(self.0)) + .map_spanned_err(ActionParserError::CopyMark)?; + let dst = dst + .parse(&mut MarkIdParser(self.0)) + .map_spanned_err(ActionParserError::CopyMark)?; + Ok(Action::CopyMark(src, dst)) + } } impl Parser for ActionParser<'_> { @@ -422,6 +468,9 @@ impl Parser for ActionParser<'_> { "define-action" => self.parse_define_action(&mut ext), "undefine-action" => self.parse_undefine_action(&mut ext), "named" => self.parse_named_action(&mut ext), + "create-mark" => self.parse_create_mark(&mut ext), + "jump-to-mark" => self.parse_jump_to_mark(&mut ext), + "copy-mark" => self.parse_copy_mark(&mut ext), v => { ext.ignore_unused(); return Err(ActionParserError::UnknownType(v.to_string()).spanned(ty.span)); diff --git a/toml-config/src/config/parsers/mark_id.rs b/toml-config/src/config/parsers/mark_id.rs new file mode 100644 index 00000000..e3dc0688 --- /dev/null +++ b/toml-config/src/config/parsers/mark_id.rs @@ -0,0 +1,61 @@ +use { + crate::{ + config::{ + context::Context, + extractor::{Extractor, ExtractorError, opt, str}, + keycodes::KEYCODES, + parser::{DataType, ParseResult, Parser, UnexpectedDataType}, + }, + toml::{ + toml_span::{Span, Spanned, SpannedExt}, + toml_value::Value, + }, + }, + indexmap::IndexMap, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum MarkIdParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error(transparent)] + Extract(#[from] ExtractorError), + #[error("MarkId must have exactly one field set")] + ExactlyOneField, + #[error("Unknown key {0}")] + UnknownKey(String), +} + +pub struct MarkIdParser<'a>(pub &'a Context<'a>); + +impl Parser for MarkIdParser<'_> { + type Value = u32; + type Error = MarkIdParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + span: Span, + table: &IndexMap, Spanned>, + ) -> ParseResult { + let mut ext = Extractor::new(self.0, span, table); + let (key, name) = ext.extract((opt(str("key")), opt(str("name"))))?; + let id = match (key, name) { + (None, None) | (Some(_), Some(_)) => { + return Err(MarkIdParserError::ExactlyOneField.spanned(span)); + } + (Some(key), _) => match KEYCODES.get(key.value) { + Some(c) => *c, + _ => return Err(key.map(|s| MarkIdParserError::UnknownKey(s.to_string()))), + }, + (_, Some(name)) => { + let mn = &mut *self.0.mark_names.borrow_mut(); + let len = mn.len() as u32; + *mn.entry(name.value.to_string()) + .or_insert(u32::MAX - 8 - len) + } + }; + Ok(id) + } +} diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 64c0daec..41cc7327 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -168,6 +168,14 @@ impl Action { let persistent = state.persistent.clone(); B::new(move || persistent.seat.focus_tiles()) } + SimpleCommand::CreateMark => { + let persistent = state.persistent.clone(); + B::new(move || persistent.seat.create_mark(None)) + } + SimpleCommand::JumpToMark => { + let persistent = state.persistent.clone(); + B::new(move || persistent.seat.jump_to_mark(None)) + } }, Action::Multi { actions } => { let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); @@ -324,6 +332,18 @@ impl Action { action(); }) } + Action::CreateMark(m) => { + let persistent = state.persistent.clone(); + B::new(move || persistent.seat.create_mark(Some(m))) + } + Action::JumpToMark(m) => { + let persistent = state.persistent.clone(); + B::new(move || persistent.seat.jump_to_mark(Some(m))) + } + Action::CopyMark(s, d) => { + let persistent = state.persistent.clone(); + B::new(move || persistent.seat.copy_mark(s, d)) + } } } } @@ -973,13 +993,14 @@ struct PersistentState { client_rules: Cell>>, client_rule_mapper: RefCell>>, window_rules: Cell>>, + mark_names: RefCell>, } fn load_config(initial_load: bool, persistent: &Rc) { let mut path = PathBuf::from(config_dir()); path.push("config.toml"); let mut config = match std::fs::read(&path) { - Ok(input) => match parse_config(&input, |e| { + Ok(input) => match parse_config(&input, &persistent.mark_names, |e| { log::warn!("Error while parsing {}: {}", path.display(), Report::new(e)) }) { None if initial_load => { @@ -1294,7 +1315,8 @@ fn create_command(exec: &Exec) -> Command { const DEFAULT: &[u8] = include_bytes!("default-config.toml"); pub fn configure() { - let default = parse_config(DEFAULT, |e| { + let mark_names = Default::default(); + let default = parse_config(DEFAULT, &mark_names, |e| { panic!("Could not parse the default config: {}", Report::new(e)) }); let persistent = Rc::new(PersistentState { @@ -1306,6 +1328,7 @@ pub fn configure() { client_rules: Default::default(), client_rule_mapper: Default::default(), window_rules: Default::default(), + mark_names, }); { let p = persistent.clone(); diff --git a/toml-config/toml-test b/toml-config/toml-test new file mode 160000 index 00000000..8448bc67 --- /dev/null +++ b/toml-config/toml-test @@ -0,0 +1 @@ +Subproject commit 8448bc678600561a00380dfe18c887cc72d2261d diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 798f4eb2..d4ab01c0 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -479,6 +479,60 @@ "type", "name" ] + }, + { + "description": "Creates a mark for the currently focused window.\n\n- Example 1:\n\n This example interactively selects a key that identifies the mark.\n\n ```toml\n [shortcuts]\n alt-x = { type = \"create-mark\" }\n ```\n\n- Example 2:\n\n This example hard-codes a key.\n\n ```toml\n [shortcuts]\n alt-x = { type = \"create-mark\", id.key = \"a\" }\n ```\n", + "type": "object", + "properties": { + "type": { + "const": "create-mark" + }, + "id": { + "description": "The identifier of the mark.\n\nIf this field is omitted, the next pressed key identifies the mark.\n", + "$ref": "#/$defs/MarkId" + } + }, + "required": [ + "type" + ] + }, + { + "description": "Moves the keyboard focus to a window identified by a mark.\n\n- Example 1:\n\n This example interactively selects a key that identifies the mark.\n\n ```toml\n [shortcuts]\n alt-x = { type = \"jump-to-mark\" }\n ```\n\n- Example 2:\n\n This example hard-codes a key.\n\n ```toml\n [shortcuts]\n alt-x = { type = \"jump-to-mark\", id.key = \"a\" }\n ```\n", + "type": "object", + "properties": { + "type": { + "const": "jump-to-mark" + }, + "id": { + "description": "The identifier of the mark.\n\nIf this field is omitted, the next pressed key identifies the mark.\n", + "$ref": "#/$defs/MarkId" + } + }, + "required": [ + "type" + ] + }, + { + "description": "Copies a mark.\n\nIf the `src` id identifies a mark before this function is called, the `dst`\nid will identify the same mark afterwards.\n", + "type": "object", + "properties": { + "type": { + "const": "copy-mark" + }, + "src": { + "description": "The source id to copy from.", + "$ref": "#/$defs/MarkId" + }, + "dst": { + "description": "The destination id to copy to.", + "$ref": "#/$defs/MarkId" + } + }, + "required": [ + "type", + "src", + "dst" + ] } ] } @@ -1416,6 +1470,21 @@ "error" ] }, + "MarkId": { + "description": "Identifies a mark.\n\nExactly one of the fields must be set.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-x = { type = \"create-mark\", id.key = \"a\" }\n ```\n", + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Identifies a mark by a key press.\n\nThe names of the keys can be found in [1] with the `KEY_` prefix removed. The key\nnames must be written all lowercase.\n\n[1]: https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h\n" + }, + "name": { + "type": "string", + "description": "Identifies a mark with an arbitrary string.\n" + } + }, + "required": [] + }, "MessageFormat": { "type": "string", "description": "A message format used by status programs.", @@ -1621,7 +1690,9 @@ "focus-next", "focus-below", "focus-above", - "focus-tiles" + "focus-tiles", + "create-mark", + "jump-to-mark" ] }, "Status": { diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index f0acdda6..4e0d672d 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -671,6 +671,91 @@ This table is a tagged union. The variant is determined by the `type` field. It The value of this field should be a string. +- `create-mark`: + + Creates a mark for the currently focused window. + + - Example 1: + + This example interactively selects a key that identifies the mark. + + ```toml + [shortcuts] + alt-x = { type = "create-mark" } + ``` + + - Example 2: + + This example hard-codes a key. + + ```toml + [shortcuts] + alt-x = { type = "create-mark", id.key = "a" } + ``` + + The table has the following fields: + + - `id` (optional): + + The identifier of the mark. + + If this field is omitted, the next pressed key identifies the mark. + + The value of this field should be a [MarkId](#types-MarkId). + +- `jump-to-mark`: + + Moves the keyboard focus to a window identified by a mark. + + - Example 1: + + This example interactively selects a key that identifies the mark. + + ```toml + [shortcuts] + alt-x = { type = "jump-to-mark" } + ``` + + - Example 2: + + This example hard-codes a key. + + ```toml + [shortcuts] + alt-x = { type = "jump-to-mark", id.key = "a" } + ``` + + The table has the following fields: + + - `id` (optional): + + The identifier of the mark. + + If this field is omitted, the next pressed key identifies the mark. + + The value of this field should be a [MarkId](#types-MarkId). + +- `copy-mark`: + + Copies a mark. + + If the `src` id identifies a mark before this function is called, the `dst` + id will identify the same mark afterwards. + + The table has the following fields: + + - `src` (required): + + The source id to copy from. + + The value of this field should be a [MarkId](#types-MarkId). + + - `dst` (required): + + The destination id to copy to. + + The value of this field should be a [MarkId](#types-MarkId). + ### `Brightness` @@ -3071,6 +3156,42 @@ The string should have one of the following values: + +### `MarkId` + +Identifies a mark. + +Exactly one of the fields must be set. + +- Example: + + ```toml + [shortcuts] + alt-x = { type = "create-mark", id.key = "a" } + ``` + +Values of this type should be tables. + +The table has the following fields: + +- `key` (optional): + + Identifies a mark by a key press. + + The names of the keys can be found in [1] with the `KEY_` prefix removed. The key + names must be written all lowercase. + + [1]: https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h + + The value of this field should be a string. + +- `name` (optional): + + Identifies a mark with an arbitrary string. + + The value of this field should be a string. + + ### `MessageFormat` @@ -3697,6 +3818,18 @@ The string should have one of the following values: Focuses the tile layer. +- `create-mark`: + + Interactively creates a mark. + + The next pressed key becomes the identifier for the mark. + +- `jump-to-mark`: + + Interactively jumps to a mark. + + The next pressed key identifies the mark to jump to. + diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 581084b4..0a551532 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -602,6 +602,79 @@ Action: kind: string description: The name of the action. required: true + create-mark: + description: | + Creates a mark for the currently focused window. + + - Example 1: + + This example interactively selects a key that identifies the mark. + + ```toml + [shortcuts] + alt-x = { type = "create-mark" } + ``` + + - Example 2: + + This example hard-codes a key. + + ```toml + [shortcuts] + alt-x = { type = "create-mark", id.key = "a" } + ``` + fields: + id: + description: | + The identifier of the mark. + + If this field is omitted, the next pressed key identifies the mark. + required: false + ref: MarkId + jump-to-mark: + description: | + Moves the keyboard focus to a window identified by a mark. + + - Example 1: + + This example interactively selects a key that identifies the mark. + + ```toml + [shortcuts] + alt-x = { type = "jump-to-mark" } + ``` + + - Example 2: + + This example hard-codes a key. + + ```toml + [shortcuts] + alt-x = { type = "jump-to-mark", id.key = "a" } + ``` + fields: + id: + description: | + The identifier of the mark. + + If this field is omitted, the next pressed key identifies the mark. + required: false + ref: MarkId + copy-mark: + description: | + Copies a mark. + + If the `src` id identifies a mark before this function is called, the `dst` + id will identify the same mark afterwards. + fields: + src: + description: The source id to copy from. + required: true + ref: MarkId + dst: + description: The destination id to copy to. + required: true + ref: MarkId Exec: @@ -870,6 +943,16 @@ SimpleActionName: description: Focuses the layer above the currently focused layer. - value: focus-tiles description: Focuses the tile layer. + - value: create-mark + description: | + Interactively creates a mark. + + The next pressed key becomes the identifier for the mark. + - value: jump-to-mark + description: | + Interactively jumps to a mark. + + The next pressed key identifies the mark to jump to. Color: @@ -3779,3 +3862,34 @@ FocusHistory: The default is `false`. kind: boolean required: false + + +MarkId: + kind: table + description: | + Identifies a mark. + + Exactly one of the fields must be set. + + - Example: + + ```toml + [shortcuts] + alt-x = { type = "create-mark", id.key = "a" } + ``` + fields: + key: + description: | + Identifies a mark by a key press. + + The names of the keys can be found in [1] with the `KEY_` prefix removed. The key + names must be written all lowercase. + + [1]: https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h + kind: string + required: false + name: + description: | + Identifies a mark with an arbitrary string. + kind: string + required: false