1
0
Fork 0
forked from wry/wry

Merge pull request #526 from mahkoh/jorth/marks

config: add create-mark, jump-to-mark, and copy-mark actions
This commit is contained in:
mahkoh 2025-07-20 16:03:50 +02:00 committed by GitHub
commit 441166b122
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1193 additions and 9 deletions

View file

@ -11,3 +11,8 @@ tasks:
- test: |
cd jay
cargo test
- test-tc: |
cd jay
git submodule update --init
cd toml-config
cargo test

View file

@ -987,6 +987,18 @@ impl ConfigClient {
self.send(&ClientMessage::SetMiddleClickPasteEnabled { enabled });
}
pub fn seat_create_mark(&self, seat: Seat, kc: Option<u32>) {
self.send(&ClientMessage::SeatCreateMark { seat, kc });
}
pub fn seat_jump_to_mark(&self, seat: Seat, kc: Option<u32>) {
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 });
}

View file

@ -747,6 +747,19 @@ pub enum ClientMessage<'a> {
SetMiddleClickPasteEnabled {
enabled: bool,
},
SeatCreateMark {
seat: Seat,
kc: Option<u32>,
},
SeatJumpToMark {
seat: Seat,
kc: Option<u32>,
},
SeatCopyMark {
seat: Seat,
src: u32,
dst: u32,
},
}
#[derive(Serialize, Deserialize, Debug)]

View file

@ -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<u32>) {
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<u32>) {
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.

View file

@ -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<u32>) -> 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<u32>) -> 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(())
}

View file

@ -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<u64>,
focus_history_visible_only: Cell<bool>,
focus_history_same_workspace: Cell<bool>,
mark_mode: Cell<Option<MarkMode>>,
marks: CopyHashMap<Keycode, Rc<dyn Node>>,
}
#[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 {

View file

@ -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<TabletPadId, Rc<TabletPad>, 1>,
tablet_tool_foci: SmallMap<TabletToolId, Rc<TabletTool>, 1>,
ui_drags: SmallMap<SeatId, Rc<WlSeatGlobal>, 1>,
marks: RefCell<SmallMapMut<SeatId, Marks, 1>>,
}
struct Marks {
seat: Rc<WlSeatGlobal>,
marks: SmallMapMut<Keycode, (), 1>,
}
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<Self>, kc: Keycode) {
self.create_mark_(kc, self.keyboard_node.get());
}
fn create_mark_(self: &Rc<Self>, kc: Keycode, node: Rc<dyn Node>) {
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<Self>, 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<Self>, 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;

View file

@ -198,6 +198,15 @@ impl<K: Eq, V, const N: usize> SmallMapMut<K, V, N> {
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,

View file

@ -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<F>(input: &[u8], handle_error: F) -> Option<Config>
pub fn parse_config<F>(
input: &[u8],
mark_names: &RefCell<AHashMap<String, u32>>,
handle_error: F,
) -> Option<Config>
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();
}

View file

@ -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<Used>,
pub mark_names: &'a RefCell<AHashMap<String, u32>>,
}
#[derive(Default)]

View file

@ -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,
};

View file

@ -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;

View file

@ -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<Self> {
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<Self> {
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<Self> {
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));

View file

@ -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<String>, Spanned<Value>>,
) -> ParseResult<Self> {
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)
}
}

View file

@ -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<Vec<MatcherTemp<ClientRule>>>,
client_rule_mapper: RefCell<Option<RuleMapper<ClientRule>>>,
window_rules: Cell<Vec<MatcherTemp<WindowRule>>>,
mark_names: RefCell<AHashMap<String, u32>>,
}
fn load_config(initial_load: bool, persistent: &Rc<PersistentState>) {
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();

1
toml-config/toml-test Submodule

@ -0,0 +1 @@
Subproject commit 8448bc678600561a00380dfe18c887cc72d2261d

View file

@ -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": {

View file

@ -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).
<a name="types-Brightness"></a>
### `Brightness`
@ -3071,6 +3156,42 @@ The string should have one of the following values:
<a name="types-MarkId"></a>
### `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.
<a name="types-MessageFormat"></a>
### `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.
<a name="types-Status"></a>

View file

@ -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