Make Super_L chordable and implement hyprland-global-shortcuts-v1
This commit is contained in:
parent
8ff17aca1e
commit
6d3bff952e
16 changed files with 363 additions and 16 deletions
|
|
@ -868,6 +868,10 @@ impl ConfigClient {
|
|||
self.send(&ClientMessage::Quit)
|
||||
}
|
||||
|
||||
pub fn trigger_global_shortcut(&self, app_id: &str, id: &str) {
|
||||
self.send(&ClientMessage::TriggerGlobalShortcut { app_id, id })
|
||||
}
|
||||
|
||||
pub fn switch_to_vt(&self, vtnr: u32) {
|
||||
self.send(&ClientMessage::SwitchTo { vtnr })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -912,6 +912,10 @@ pub enum ClientMessage<'a> {
|
|||
seat: Seat,
|
||||
right: bool,
|
||||
},
|
||||
TriggerGlobalShortcut {
|
||||
app_id: &'a str,
|
||||
id: &'a str,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
|
|
|||
|
|
@ -108,6 +108,14 @@ pub fn quit() {
|
|||
get!().quit()
|
||||
}
|
||||
|
||||
/// Sends a `pressed` event to the client that has registered a Hyprland global
|
||||
/// shortcut with the given `app_id` and `id`.
|
||||
///
|
||||
/// Has no effect if no client is currently registered for that combination.
|
||||
pub fn trigger_global_shortcut(app_id: &str, id: &str) {
|
||||
get!().trigger_global_shortcut(app_id, id)
|
||||
}
|
||||
|
||||
/// Switches to a different VT.
|
||||
pub fn switch_to_vt(n: u32) {
|
||||
get!().switch_to_vt(n)
|
||||
|
|
|
|||
|
|
@ -396,6 +396,7 @@ fn start_compositor2(
|
|||
bo_drop_queue: Rc::new(ObjectDropQueue::new(&ring)),
|
||||
virtual_outputs: Default::default(),
|
||||
clean_logs_older_than: Default::default(),
|
||||
hyprland_global_shortcuts: Default::default(),
|
||||
});
|
||||
state.tracker.register(ClientId::from_raw(0));
|
||||
create_dummy_output(&state);
|
||||
|
|
|
|||
|
|
@ -3528,10 +3528,24 @@ impl ConfigProxyHandler {
|
|||
ClientMessage::SeatMoveTab { seat, right } => self
|
||||
.handle_seat_move_tab(seat, right)
|
||||
.wrn("seat_move_tab")?,
|
||||
ClientMessage::TriggerGlobalShortcut { app_id, id } => {
|
||||
self.handle_trigger_global_shortcut(app_id, id);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_trigger_global_shortcut(&self, app_id: &str, id: &str) {
|
||||
let key = (app_id.to_string(), id.to_string());
|
||||
let Some(shortcut) = self.state.hyprland_global_shortcuts.get(&key) else {
|
||||
log::debug!(
|
||||
"no client has registered hyprland global shortcut {app_id:?}:{id:?}"
|
||||
);
|
||||
return;
|
||||
};
|
||||
shortcut.send_pressed_now();
|
||||
}
|
||||
|
||||
pub fn auto_focus(&self, data: &ToplevelData) -> bool {
|
||||
for matcher in self.window_matcher_no_auto_focus.lock().values() {
|
||||
if matcher.node.pull(data) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use {
|
|||
ext_session_lock_manager_v1::ExtSessionLockManagerV1Global,
|
||||
head_management::jay_head_manager_v1::JayHeadManagerV1Global,
|
||||
hyprland_focus_grab_manager_v1::HyprlandFocusGrabManagerV1Global,
|
||||
hyprland_global_shortcuts_manager_v1::HyprlandGlobalShortcutsManagerV1Global,
|
||||
ipc::{
|
||||
data_control::{
|
||||
ext_data_control_manager_v1::ExtDataControlManagerV1Global,
|
||||
|
|
@ -208,6 +209,7 @@ singletons! {
|
|||
ZwpRelativePointerManagerV1,
|
||||
ExtSessionLockManagerV1,
|
||||
HyprlandFocusGrabManagerV1,
|
||||
HyprlandGlobalShortcutsManagerV1,
|
||||
WpViewporter,
|
||||
WpFractionalScaleManagerV1,
|
||||
ZwpPointerConstraintsV1,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ pub mod ext_session_lock_v1;
|
|||
pub mod head_management;
|
||||
pub mod hyprland_focus_grab_manager_v1;
|
||||
pub mod hyprland_focus_grab_v1;
|
||||
pub mod hyprland_global_shortcut_v1;
|
||||
pub mod hyprland_global_shortcuts_manager_v1;
|
||||
pub mod ipc;
|
||||
pub mod jay_acceptor_request;
|
||||
pub mod jay_client_query;
|
||||
|
|
|
|||
97
src/ifs/hyprland_global_shortcut_v1.rs
Normal file
97
src/ifs/hyprland_global_shortcut_v1.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
use {
|
||||
crate::{
|
||||
client::{Client, ClientError},
|
||||
leaks::Tracker,
|
||||
object::{Object, Version},
|
||||
wire::{HyprlandGlobalShortcutV1Id, hyprland_global_shortcut_v1::*},
|
||||
},
|
||||
std::{rc::Rc, time::SystemTime},
|
||||
thiserror::Error,
|
||||
};
|
||||
|
||||
pub struct HyprlandGlobalShortcutV1 {
|
||||
pub id: HyprlandGlobalShortcutV1Id,
|
||||
pub client: Rc<Client>,
|
||||
pub tracker: Tracker<Self>,
|
||||
pub version: Version,
|
||||
pub shortcut_id: String,
|
||||
pub app_id: String,
|
||||
}
|
||||
|
||||
impl HyprlandGlobalShortcutV1 {
|
||||
pub fn new(
|
||||
id: HyprlandGlobalShortcutV1Id,
|
||||
client: &Rc<Client>,
|
||||
version: Version,
|
||||
shortcut_id: String,
|
||||
app_id: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
client: client.clone(),
|
||||
tracker: Default::default(),
|
||||
version,
|
||||
shortcut_id,
|
||||
app_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_pressed_now(&self) {
|
||||
let (sec_hi, sec_lo, nsec) = now_split();
|
||||
self.client.event(Pressed {
|
||||
self_id: self.id,
|
||||
tv_sec_hi: sec_hi,
|
||||
tv_sec_lo: sec_lo,
|
||||
tv_nsec: nsec,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn now_split() -> (u32, u32, u32) {
|
||||
let dur = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap_or_default();
|
||||
let secs = dur.as_secs();
|
||||
((secs >> 32) as u32, secs as u32, dur.subsec_nanos())
|
||||
}
|
||||
|
||||
impl HyprlandGlobalShortcutV1RequestHandler for HyprlandGlobalShortcutV1 {
|
||||
type Error = HyprlandGlobalShortcutV1Error;
|
||||
|
||||
fn destroy(&self, _req: Destroy, _slf: &Rc<Self>) -> Result<(), Self::Error> {
|
||||
let key = (self.app_id.clone(), self.shortcut_id.clone());
|
||||
if let Some(existing) = self.client.state.hyprland_global_shortcuts.get(&key)
|
||||
&& Rc::as_ptr(&existing) as *const _ == self as *const _
|
||||
{
|
||||
self.client.state.hyprland_global_shortcuts.remove(&key);
|
||||
}
|
||||
self.client.remove_obj(self)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
object_base! {
|
||||
self = HyprlandGlobalShortcutV1;
|
||||
version = self.version;
|
||||
}
|
||||
|
||||
impl Object for HyprlandGlobalShortcutV1 {
|
||||
fn break_loops(&self) {
|
||||
let key = (self.app_id.clone(), self.shortcut_id.clone());
|
||||
if let Some(existing) = self.client.state.hyprland_global_shortcuts.get(&key)
|
||||
&& Rc::as_ptr(&existing) as *const _ == self as *const _
|
||||
{
|
||||
self.client.state.hyprland_global_shortcuts.remove(&key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
simple_add_obj!(HyprlandGlobalShortcutV1);
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum HyprlandGlobalShortcutV1Error {
|
||||
#[error(transparent)]
|
||||
ClientError(Box<ClientError>),
|
||||
}
|
||||
|
||||
efrom!(HyprlandGlobalShortcutV1Error, ClientError);
|
||||
122
src/ifs/hyprland_global_shortcuts_manager_v1.rs
Normal file
122
src/ifs/hyprland_global_shortcuts_manager_v1.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
use {
|
||||
crate::{
|
||||
client::{Client, ClientError},
|
||||
globals::{Global, GlobalName},
|
||||
ifs::hyprland_global_shortcut_v1::HyprlandGlobalShortcutV1,
|
||||
leaks::Tracker,
|
||||
object::{Object, Version},
|
||||
wire::{HyprlandGlobalShortcutsManagerV1Id, hyprland_global_shortcuts_manager_v1::*},
|
||||
},
|
||||
std::rc::Rc,
|
||||
thiserror::Error,
|
||||
};
|
||||
|
||||
const ALREADY_TAKEN: u32 = 0;
|
||||
|
||||
pub struct HyprlandGlobalShortcutsManagerV1Global {
|
||||
pub name: GlobalName,
|
||||
}
|
||||
|
||||
impl HyprlandGlobalShortcutsManagerV1Global {
|
||||
pub fn new(name: GlobalName) -> Self {
|
||||
Self { name }
|
||||
}
|
||||
|
||||
fn bind_(
|
||||
self: Rc<Self>,
|
||||
id: HyprlandGlobalShortcutsManagerV1Id,
|
||||
client: &Rc<Client>,
|
||||
version: Version,
|
||||
) -> Result<(), HyprlandGlobalShortcutsManagerV1Error> {
|
||||
let obj = Rc::new(HyprlandGlobalShortcutsManagerV1 {
|
||||
id,
|
||||
client: client.clone(),
|
||||
tracker: Default::default(),
|
||||
version,
|
||||
});
|
||||
track!(client, obj);
|
||||
client.add_client_obj(&obj)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HyprlandGlobalShortcutsManagerV1 {
|
||||
pub id: HyprlandGlobalShortcutsManagerV1Id,
|
||||
pub client: Rc<Client>,
|
||||
pub tracker: Tracker<Self>,
|
||||
pub version: Version,
|
||||
}
|
||||
|
||||
impl HyprlandGlobalShortcutsManagerV1RequestHandler for HyprlandGlobalShortcutsManagerV1 {
|
||||
type Error = HyprlandGlobalShortcutsManagerV1Error;
|
||||
|
||||
fn destroy(&self, _req: Destroy, _slf: &Rc<Self>) -> Result<(), Self::Error> {
|
||||
self.client.remove_obj(self)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_shortcut(
|
||||
&self,
|
||||
req: RegisterShortcut,
|
||||
_slf: &Rc<Self>,
|
||||
) -> Result<(), Self::Error> {
|
||||
let shortcut_id = req.id.to_string();
|
||||
let app_id = req.app_id.to_string();
|
||||
let key = (app_id.clone(), shortcut_id.clone());
|
||||
let registry = &self.client.state.hyprland_global_shortcuts;
|
||||
if registry.get(&key).is_some() {
|
||||
self.client.protocol_error(
|
||||
self,
|
||||
ALREADY_TAKEN,
|
||||
&format!(
|
||||
"global shortcut with app_id={app_id:?} id={shortcut_id:?} is already registered"
|
||||
),
|
||||
);
|
||||
return Err(HyprlandGlobalShortcutsManagerV1Error::AlreadyTaken);
|
||||
}
|
||||
let shortcut = Rc::new(HyprlandGlobalShortcutV1::new(
|
||||
req.shortcut,
|
||||
&self.client,
|
||||
self.version,
|
||||
shortcut_id,
|
||||
app_id,
|
||||
));
|
||||
track!(self.client, shortcut);
|
||||
self.client.add_client_obj(&shortcut)?;
|
||||
registry.set(key, shortcut);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
global_base!(
|
||||
HyprlandGlobalShortcutsManagerV1Global,
|
||||
HyprlandGlobalShortcutsManagerV1,
|
||||
HyprlandGlobalShortcutsManagerV1Error
|
||||
);
|
||||
|
||||
impl Global for HyprlandGlobalShortcutsManagerV1Global {
|
||||
fn version(&self) -> u32 {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
simple_add_global!(HyprlandGlobalShortcutsManagerV1Global);
|
||||
|
||||
object_base! {
|
||||
self = HyprlandGlobalShortcutsManagerV1;
|
||||
version = self.version;
|
||||
}
|
||||
|
||||
impl Object for HyprlandGlobalShortcutsManagerV1 {}
|
||||
|
||||
simple_add_obj!(HyprlandGlobalShortcutsManagerV1);
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum HyprlandGlobalShortcutsManagerV1Error {
|
||||
#[error(transparent)]
|
||||
ClientError(Box<ClientError>),
|
||||
#[error("the app_id + id combination has already been registered")]
|
||||
AlreadyTaken,
|
||||
}
|
||||
|
||||
efrom!(HyprlandGlobalShortcutsManagerV1Error, ClientError);
|
||||
|
|
@ -51,6 +51,7 @@ use {
|
|||
HeadManagers, HeadNames,
|
||||
jay_head_manager_session_v1::{HeadManagerEvent, JayHeadManagerSessionV1},
|
||||
},
|
||||
hyprland_global_shortcut_v1::HyprlandGlobalShortcutV1,
|
||||
ipc::{
|
||||
DataOfferIds, DataSourceIds, data_control::DataControlDeviceIds,
|
||||
x_data_device::XIpcDeviceIds,
|
||||
|
|
@ -302,6 +303,8 @@ pub struct State {
|
|||
pub bo_drop_queue: Rc<ObjectDropQueue<Rc<dyn BufferObject>>>,
|
||||
pub virtual_outputs: VirtualOutputs,
|
||||
pub clean_logs_older_than: Cell<Option<SystemTime>>,
|
||||
pub hyprland_global_shortcuts:
|
||||
CopyHashMap<(String, String), Rc<HyprlandGlobalShortcutV1>>,
|
||||
}
|
||||
|
||||
// impl Drop for State {
|
||||
|
|
|
|||
|
|
@ -204,6 +204,10 @@ pub enum Action {
|
|||
dx2: i32,
|
||||
dy2: i32,
|
||||
},
|
||||
TriggerGlobalShortcut {
|
||||
app_id: String,
|
||||
id: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
|
|
|||
|
|
@ -93,6 +93,10 @@ pub enum ActionParserError {
|
|||
UnknownDirection(String),
|
||||
#[error("Exactly one of `output` or `direction` must be specified")]
|
||||
OutputAndDirectionMutuallyExclusive,
|
||||
#[error("Specify either `name = \"app_id:id\"` or both `app_id` and `id`")]
|
||||
GlobalShortcutNeedsName,
|
||||
#[error("Global shortcut `name` must be of the form `app_id:id`, got `{0}`")]
|
||||
GlobalShortcutBadName(String),
|
||||
}
|
||||
|
||||
pub struct ActionParser<'a>(pub &'a Context<'a>);
|
||||
|
|
@ -516,6 +520,36 @@ impl ActionParser<'_> {
|
|||
dy2: dy2.despan().unwrap_or(0),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_global_shortcut(
|
||||
&mut self,
|
||||
span: Span,
|
||||
ext: &mut Extractor<'_>,
|
||||
) -> ParseResult<Self> {
|
||||
let (name_opt, app_id_opt, id_opt) = ext.extract((
|
||||
opt(str("name")),
|
||||
opt(str("app_id")),
|
||||
opt(str("id")),
|
||||
))?;
|
||||
let (app_id, id) = match (app_id_opt, id_opt, name_opt) {
|
||||
(Some(a), Some(i), _) => (a.value.to_string(), i.value.to_string()),
|
||||
(None, None, Some(n)) => match n.value.split_once(':') {
|
||||
Some((a, i)) if !a.is_empty() && !i.is_empty() => {
|
||||
(a.to_string(), i.to_string())
|
||||
}
|
||||
_ => {
|
||||
return Err(
|
||||
ActionParserError::GlobalShortcutBadName(n.value.to_string())
|
||||
.spanned(n.span),
|
||||
);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err(ActionParserError::GlobalShortcutNeedsName.spanned(span));
|
||||
}
|
||||
};
|
||||
Ok(Action::TriggerGlobalShortcut { app_id, id })
|
||||
}
|
||||
}
|
||||
|
||||
impl Parser for ActionParser<'_> {
|
||||
|
|
@ -578,6 +612,7 @@ impl Parser for ActionParser<'_> {
|
|||
"create-virtual-output" => self.parse_create_virtual_output(&mut ext),
|
||||
"remove-virtual-output" => self.parse_remove_virtual_output(&mut ext),
|
||||
"resize" => self.parse_resize(&mut ext),
|
||||
"global-shortcut" => self.parse_global_shortcut(span, &mut ext),
|
||||
v => {
|
||||
ext.ignore_unused();
|
||||
return Err(ActionParserError::UnknownType(v.to_string()).spanned(ty.span));
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ use {
|
|||
ALT, CAPS, CTRL, LOCK, LOGO, MOD1, MOD2, MOD3, MOD4, MOD5, Modifiers, NUM, RELEASE,
|
||||
SHIFT,
|
||||
},
|
||||
syms::KeySym,
|
||||
syms::{
|
||||
KeySym, SYM_Alt_L, SYM_Alt_R, SYM_Control_L, SYM_Control_R, SYM_Hyper_L, SYM_Hyper_R,
|
||||
SYM_Meta_L, SYM_Meta_R, SYM_Shift_L, SYM_Shift_R, SYM_Super_L, SYM_Super_R,
|
||||
},
|
||||
},
|
||||
kbvm::Keysym,
|
||||
thiserror::Error,
|
||||
|
|
@ -38,23 +41,28 @@ impl Parser for ModifiedKeysymParser {
|
|||
|
||||
fn parse_string(&mut self, span: Span, string: &str) -> ParseResult<Self> {
|
||||
let mut modifiers = Modifiers(0);
|
||||
let mut sym = None;
|
||||
let mut sym: Option<KeySym> = None;
|
||||
for part in string.split("-") {
|
||||
let modifier = match parse_mod(part) {
|
||||
Some(m) => m,
|
||||
_ => match Keysym::from_str(part) {
|
||||
Some(new) if sym.is_none() => {
|
||||
sym = Some(KeySym(new.0));
|
||||
continue;
|
||||
}
|
||||
Some(_) => return Err(ModifiedKeysymParserError::MoreThanOneSym.spanned(span)),
|
||||
_ => {
|
||||
return Err(ModifiedKeysymParserError::UnknownKeysym(part.to_string())
|
||||
.spanned(span));
|
||||
}
|
||||
},
|
||||
if let Some(m) = parse_mod(part) {
|
||||
modifiers |= m;
|
||||
continue;
|
||||
}
|
||||
let Some(new) = Keysym::from_str(part) else {
|
||||
return Err(
|
||||
ModifiedKeysymParserError::UnknownKeysym(part.to_string()).spanned(span),
|
||||
);
|
||||
};
|
||||
modifiers |= modifier;
|
||||
let new = KeySym(new.0);
|
||||
match sym {
|
||||
None => sym = Some(new),
|
||||
Some(prev) => {
|
||||
let Some(m) = modifier_key_to_mod(prev) else {
|
||||
return Err(ModifiedKeysymParserError::MoreThanOneSym.spanned(span));
|
||||
};
|
||||
modifiers |= m;
|
||||
sym = Some(new);
|
||||
}
|
||||
}
|
||||
}
|
||||
match sym {
|
||||
Some(s) => Ok(modifiers | s),
|
||||
|
|
@ -63,6 +71,20 @@ impl Parser for ModifiedKeysymParser {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
fn modifier_key_to_mod(sym: KeySym) -> Option<Modifiers> {
|
||||
let m = match sym {
|
||||
SYM_Super_L | SYM_Super_R => LOGO,
|
||||
SYM_Alt_L | SYM_Alt_R => ALT,
|
||||
SYM_Control_L | SYM_Control_R => CTRL,
|
||||
SYM_Shift_L | SYM_Shift_R => SHIFT,
|
||||
SYM_Meta_L | SYM_Meta_R => MOD1,
|
||||
SYM_Hyper_L | SYM_Hyper_R => MOD4,
|
||||
_ => return None,
|
||||
};
|
||||
Some(m)
|
||||
}
|
||||
|
||||
pub struct ModifiersParser;
|
||||
|
||||
impl Parser for ModifiersParser {
|
||||
|
|
|
|||
|
|
@ -508,6 +508,11 @@ impl Action {
|
|||
Action::Resize { dx1, dy1, dx2, dy2 } => {
|
||||
window_or_seat!(s, s.resize(dx1, dy1, dx2, dy2))
|
||||
}
|
||||
Action::TriggerGlobalShortcut { app_id, id } => {
|
||||
b.new(move || {
|
||||
jay_config::trigger_global_shortcut(&app_id, &id);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
14
wire/hyprland_global_shortcut_v1.txt
Normal file
14
wire/hyprland_global_shortcut_v1.txt
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
request destroy (destructor) {
|
||||
}
|
||||
|
||||
event pressed {
|
||||
tv_sec_hi: u32,
|
||||
tv_sec_lo: u32,
|
||||
tv_nsec: u32,
|
||||
}
|
||||
|
||||
event released {
|
||||
tv_sec_hi: u32,
|
||||
tv_sec_lo: u32,
|
||||
tv_nsec: u32,
|
||||
}
|
||||
10
wire/hyprland_global_shortcuts_manager_v1.txt
Normal file
10
wire/hyprland_global_shortcuts_manager_v1.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
request register_shortcut {
|
||||
shortcut: id(hyprland_global_shortcut_v1) (new),
|
||||
id: str,
|
||||
app_id: str,
|
||||
description: str,
|
||||
trigger_description: str,
|
||||
}
|
||||
|
||||
request destroy (destructor) {
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue