1
0
Fork 0
forked from wry/wry

Make Super_L chordable and implement hyprland-global-shortcuts-v1

This commit is contained in:
entailz 2026-04-30 23:21:35 -07:00
parent 8ff17aca1e
commit 6d3bff952e
16 changed files with 363 additions and 16 deletions

View file

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

View file

@ -912,6 +912,10 @@ pub enum ClientMessage<'a> {
seat: Seat,
right: bool,
},
TriggerGlobalShortcut {
app_id: &'a str,
id: &'a str,
},
}
#[derive(Serialize, Deserialize, Debug)]

View file

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

View file

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

View file

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

View file

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

View file

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

View 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);

View 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);

View file

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

View file

@ -204,6 +204,10 @@ pub enum Action {
dx2: i32,
dy2: i32,
},
TriggerGlobalShortcut {
app_id: String,
id: String,
},
}
#[derive(Debug, Clone, Default)]

View file

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

View file

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

View file

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

View 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,
}

View 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) {
}