wayland-global-shortcut 0.0.1-debug → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,6 +68,12 @@ const ok = await shortcuts.register('CmdOrCtrl+Shift+H', () => {
68
68
 
69
69
  The `description` is shown by the desktop in the consent dialog and in the system shortcut settings. If not specified, the accelerator is used.
70
70
 
71
+ By default, the callback is called when the shortcut is **released**. The compositor keeps the key events of an active shortcut, so a window focused while the keys are still held down may never receive the key release, leaving the last key stuck (e.g. the `W` of `Ctrl+W` repeating in a text field). To call it as soon as the keys are pressed, use `trigger: 'press'`:
72
+
73
+ ```js
74
+ await shortcuts.register('CmdOrCtrl+Shift+J', callback, { trigger: 'press' })
75
+ ```
76
+
71
77
  To register multiple shortcuts with a **single consent dialog**, use `registerAll`:
72
78
 
73
79
  ```js
@@ -126,7 +132,7 @@ toXdgTrigger('Ctrl+Foo') // throws TypeError
126
132
  2. A portal session is created and all shortcuts are bound with a single `BindShortcuts` call, using the XDG trigger as both the shortcut ID and the `preferred_trigger`.
127
133
  3. The desktop may show a consent dialog. The returned promise resolves only after the user accepts or dismisses it.
128
134
  4. The portal allows a single bind per session, so every change (register/unregister) closes the session and binds the full set again. If a bind fails, the previous set is restored.
129
- 5. `Activated` signals from the current session call the matching callback.
135
+ 5. `Deactivated` (release) or `Activated` (press) signals from the current session call the matching callback, according to its `trigger`.
130
136
 
131
137
  ### Limitations
132
138
 
@@ -141,7 +147,7 @@ toXdgTrigger('Ctrl+Foo') // throws TypeError
141
147
  * Registers a global shortcut.
142
148
  * @param {string} accelerator - Accelerator string (e.g. `CmdOrCtrl+Shift+H`).
143
149
  * @param {() => void} callback - Called when the shortcut is activated.
144
- * @param {ShortcutOptions} [options] - Registration options.
150
+ * @param {ShortcutOptions} [options] - Registration options (`description`, `trigger`).
145
151
  * @returns {Promise<boolean>} Whether the shortcut was bound by the portal.
146
152
  */
147
153
  register(accelerator, callback, options)
package/dist/main.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { toXdgTrigger } from './XdgTrigger';
2
+ /** When the shortcut callback is called: when the keys are pressed or released. */
3
+ export type ShortcutTrigger = 'press' | 'release';
2
4
  /** Options for {@link WaylandGlobalShortcut}. */
3
5
  export interface WaylandGlobalShortcutOptions {
4
6
  /**
@@ -14,6 +16,15 @@ export interface WaylandGlobalShortcutOptions {
14
16
  export interface ShortcutOptions {
15
17
  /** Human readable description shown by the desktop. Defaults to the accelerator. */
16
18
  description?: string;
19
+ /**
20
+ * When the callback is called. Defaults to `release`.
21
+ *
22
+ * The compositor keeps the key events of an active shortcut, so a window focused while the
23
+ * keys are still held down may never receive the key release and the last key gets stuck
24
+ * (e.g. the `W` of `Ctrl+W` repeating in a text field). Use `press` only when the callback
25
+ * does not change the focus.
26
+ */
27
+ trigger?: ShortcutTrigger;
17
28
  }
18
29
  /**
19
30
  * Global shortcuts through the `org.freedesktop.portal.GlobalShortcuts` D-Bus portal.
package/dist/main.js CHANGED
@@ -1 +1 @@
1
- (()=>{"use strict";var e={983(e,t,s){t.DBusClient=void 0;const o=s(474),n=s(172);class i{bus;signalListener;disconnectListener;closed=!1;constructor(e){this.bus=e;const t=e.connection;t.on("message",e=>{e.type===n.messageType.signal&&this.signalListener?.({path:e.path??"",interface:e.interface??"",member:e.member??"",body:e.body??[]})}),t.on("error",e=>this.notifyDisconnect(e)),t.on("close",e=>this.notifyDisconnect(e??new Error("D-Bus connection closed")))}static async connect(){const e=(0,n.sessionBus)(),t=new i(e);try{if(await(0,o.once)(e.connection,"connect"),await e.getId(),!e.name)throw new Error("D-Bus did not assign a unique name")}catch(e){throw t.close(),e}return t}get uniqueName(){return this.bus.name}call(e){return new Promise((t,s)=>{this.bus.invoke({...e},(e,...o)=>e?s(e):t(o))})}onSignal(e){this.signalListener=e}onDisconnect(e){this.disconnectListener=e}close(){this.closed||(this.closed=!0,this.bus.close().catch(()=>{}))}notifyDisconnect(e){this.closed||(this.closed=!0,this.disconnectListener?.(e),this.bus.close().catch(()=>{}))}}t.DBusClient=i},809(e,t){t.toXdgTrigger=function(e){const t=()=>new TypeError(`Invalid accelerator: ${e}`),i=new Set;let a,u=!1;for(const o of e.split("+").map(e=>e.trim().toLowerCase())){if(s[o]){i.add(s[o]);continue}if(n.has(o)){u=!0;continue}if(void 0!==a)throw t();const e=r[o];if(e&&i.add("SHIFT"),a=c(e??o),!a)throw t()}if(void 0===a)throw t();if(u)return null;return[...o.filter(e=>i.has(e)),a].join("+")};const s={command:"LOGO",cmd:"LOGO",super:"LOGO",meta:"LOGO",commandorcontrol:"CTRL",cmdorctrl:"CTRL",control:"CTRL",ctrl:"CTRL",alt:"ALT",option:"ALT",shift:"SHIFT"},o=["CTRL","ALT","SHIFT","LOGO"],n=new Set(["altgr"]),i={space:"space",tab:"Tab",backspace:"BackSpace",delete:"Delete",insert:"Insert",return:"Return",enter:"Return",esc:"Escape",escape:"Escape",up:"Up",down:"Down",left:"Left",right:"Right",home:"Home",end:"End",pageup:"Page_Up",pagedown:"Page_Down",capslock:"Caps_Lock",numlock:"Num_Lock",scrolllock:"Scroll_Lock",printscreen:"Print",volumeup:"XF86AudioRaiseVolume",volumedown:"XF86AudioLowerVolume",volumemute:"XF86AudioMute",medianexttrack:"XF86AudioNext",mediaprevioustrack:"XF86AudioPrev",mediastop:"XF86AudioStop",mediaplaypause:"XF86AudioPlay",numdec:"KP_Decimal",numadd:"KP_Add",numsub:"KP_Subtract",nummult:"KP_Multiply",numdiv:"KP_Divide",";":"semicolon","=":"equal",",":"comma","-":"minus",".":"period","/":"slash","`":"grave","[":"bracketleft","\\":"backslash","]":"bracketright","'":"apostrophe"},r={")":"0","!":"1","@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",":":";","+":"=",plus:"=","<":",",_:"-",">":".","?":"/","~":"`","{":"[","|":"\\","}":"]",'"':"'"};function c(e){return/^[a-z0-9]$/.test(e)?e:/^f([1-9]|1[0-9]|2[0-4])$/.test(e)?e.toUpperCase():/^num[0-9]$/.test(e)?`KP_${e.slice(3)}`:i[e]}},172(e){e.exports=require("dbus-native")},474(e){e.exports=require("node:events")}};const t={};function s(o){const n=t[o];if(void 0!==n)return n.exports;const i=t[o]={exports:{}};return e[o](i,i.exports,s),i.exports}let o={};(()=>{let e=o;Object.defineProperty(e,"__esModule",{value:!0}),e.toXdgTrigger=e.WaylandGlobalShortcut=void 0;const t=s(983),n=s(809);Object.defineProperty(e,"toXdgTrigger",{enumerable:!0,get:function(){return n.toXdgTrigger}});const i="org.freedesktop.DBus",r="org.freedesktop.portal.Desktop",c="/org/freedesktop/portal/desktop",a="org.freedesktop.portal.GlobalShortcuts",u="org.freedesktop.portal.Request",h="[WaylandGlobalShortcut]";function l(e,t,s,o){return{destination:r,path:c,interface:e,member:t,signature:s,body:o}}e.WaylandGlobalShortcut=class{appId;timeout;shortcuts=new Map;pendingRequests=new Map;queue=Promise.resolve();bus;sessionPath;tokenCounter=0;constructor(e={}){this.appId=e.appId??(process.env.CHROME_DESKTOP??"").replace(/\.desktop$/,""),this.timeout=e.timeout??1e4}register(e,t,s={}){return this.registerAll([e],t,s)}registerAll(e,t,s={}){return this.enqueue(async()=>{const o=[...this.shortcuts.values()];for(const i of e){const e=(0,n.toXdgTrigger)(i);if(!e||o.some(t=>t.id===e))return!1;o.push({id:e,description:s.description||i,callback:t})}return this.commit(o)})}isRegistered(e){const t=(0,n.toXdgTrigger)(e);return!!t&&this.shortcuts.has(t)}unregister(e){return this.enqueue(async()=>{const t=(0,n.toXdgTrigger)(e);if(!t||!this.shortcuts.has(t))return;const s=[...this.shortcuts.values()].filter(e=>e.id!==t);this.setShortcuts(s),await this.bind(s)||this.setShortcuts([])})}unregisterAll(){return this.enqueue(async()=>{this.setShortcuts([]),await this.closeSession()})}destroy(){return this.enqueue(async()=>{this.setShortcuts([]),await this.closeSession(),this.bus&&this.disconnect(this.bus,new Error("Destroyed"))})}enqueue(e){const t=this.queue.then(e);return this.queue=t.catch(()=>{}),t}setShortcuts(e){this.shortcuts.clear(),e.forEach(e=>this.shortcuts.set(e.id,e))}async commit(e){const t=[...this.shortcuts.values()];return await this.bind(e)?(this.setShortcuts(e),!0):(t.length&&!await this.bind(t)&&this.setShortcuts([]),!1)}async bind(e){if(await this.closeSession(),!e.length)return!0;try{this.sessionPath=await function(e,t){let s;const o=new Promise((e,o)=>{s=setTimeout(()=>o(new Error(`Timed out after ${t}ms`)),t)});return Promise.race([e,o]).finally(()=>clearTimeout(s))}(this.connect().then(()=>this.createSession()),this.timeout);const t=await this.bindShortcuts(this.sessionPath,e),s=e.filter(e=>!t.includes(e.id)).map(e=>e.id);if(!s.length)return!0;console.warn(h,"Shortcuts not bound by the portal:",s)}catch(e){console.error(h,"Failed to bind shortcuts:",e)}return await this.closeSession(),!1}async connect(){if(this.bus)return this.bus;const e=await t.DBusClient.connect();e.onDisconnect(t=>{console.error(h,"D-Bus connection lost:",t),this.disconnect(e,t)}),e.onSignal(e=>this.onSignal(e));try{await this.addMatch(e,u,"Response"),await this.addMatch(e,a,"Activated"),await this.registerAppId(e)}catch(t){throw this.disconnect(e,t),t}return this.bus=e,e}disconnect(e,t){this.bus===e&&(this.bus=void 0,this.sessionPath=void 0),this.pendingRequests.forEach(e=>e.reject(t)),this.pendingRequests.clear(),e.close()}addMatch(e,t,s){return e.call({destination:i,path:"/org/freedesktop/DBus",interface:i,member:"AddMatch",signature:"s",body:[`type='signal',sender='${r}',interface='${t}',member='${s}'`]})}async registerAppId(e){if(this.appId)try{await e.call(l("org.freedesktop.host.portal.Registry","Register","sa{sv}",[this.appId,{}]))}catch(e){console.warn(h,`Failed to register the app ID "${this.appId}", the portal may reject shortcuts:`,e)}else console.warn(h,"No app ID available, the portal may reject shortcuts.")}async createSession(){return(await this.request("CreateSession","a{sv}",e=>[{...e,session_handle_token:this.nextToken()}])).session_handle}async bindShortcuts(e,t){const s=t.map(e=>[e.id,{description:e.description,preferred_trigger:e.id}]);return((await this.request("BindShortcuts","oa(sa{sv})sa{sv}",t=>[e,s,"",t])).shortcuts??[]).map(([e])=>e)}async closeSession(){const e=this.sessionPath;if(this.sessionPath=void 0,e&&this.bus)try{await this.bus.call({destination:r,path:e,interface:"org.freedesktop.portal.Session",member:"Close"})}catch(e){console.warn(h,"Failed to close the portal session:",e)}}async request(e,t,s){const o=this.bus,n=this.nextToken(),i=o.uniqueName.slice(1).replace(/\./g,"_");let r=`${c}/request/${i}/${n}`;const u=new Promise((e,t)=>{this.pendingRequests.set(r,{resolve:e,reject:t})});try{const[i]=await o.call(l(a,e,t,s({handle_token:n})));i!==r&&(this.pendingRequests.set(i,this.pendingRequests.get(r)),this.pendingRequests.delete(r),r=i);const{code:c,results:h}=await u;if(0!==c)throw new Error(`${e} failed with response code ${c}`);return h}finally{this.pendingRequests.delete(r)}}onSignal(e){if(e.interface===u&&"Response"===e.member){const[t,s]=e.body;return void this.pendingRequests.get(e.path)?.resolve({code:t,results:s??{}})}if(e.interface===a&&"Activated"===e.member){const[t,s]=e.body;if(t!==this.sessionPath)return;try{this.shortcuts.get(s)?.callback()}catch(e){console.error(h,`Error on shortcut "${s}" callback:`,e)}}}nextToken(){return`wgs_${process.pid}_${++this.tokenCounter}`}}})(),module.exports=o})();
1
+ (()=>{"use strict";var e={983(e,t,s){t.DBusClient=void 0;const o=s(474),i=s(172);class n{bus;signalListener;disconnectListener;closed=!1;constructor(e){this.bus=e;const t=e.connection;t.on("message",e=>{e.type===i.messageType.signal&&this.signalListener?.({path:e.path??"",interface:e.interface??"",member:e.member??"",body:e.body??[]})}),t.on("error",e=>this.notifyDisconnect(e)),t.on("close",e=>this.notifyDisconnect(e??new Error("D-Bus connection closed")))}static async connect(){const e=(0,i.sessionBus)(),t=new n(e);try{if(await(0,o.once)(e.connection,"connect"),await e.getId(),!e.name)throw new Error("D-Bus did not assign a unique name")}catch(e){throw t.close(),e}return t}get uniqueName(){return this.bus.name}call(e){return new Promise((t,s)=>{this.bus.invoke({...e},(e,...o)=>e?s(e):t(o))})}onSignal(e){this.signalListener=e}onDisconnect(e){this.disconnectListener=e}close(){this.closed||(this.closed=!0,this.bus.close().catch(()=>{}))}notifyDisconnect(e){this.closed||(this.closed=!0,this.disconnectListener?.(e),this.bus.close().catch(()=>{}))}}t.DBusClient=n},809(e,t){t.toXdgTrigger=function(e){const t=()=>new TypeError(`Invalid accelerator: ${e}`),n=new Set;let a,u=!1;for(const o of e.split("+").map(e=>e.trim().toLowerCase())){if(s[o]){n.add(s[o]);continue}if(i.has(o)){u=!0;continue}if(void 0!==a)throw t();const e=r[o];if(e&&n.add("SHIFT"),a=c(e??o),!a)throw t()}if(void 0===a)throw t();if(u)return null;return[...o.filter(e=>n.has(e)),a].join("+")};const s={command:"LOGO",cmd:"LOGO",super:"LOGO",meta:"LOGO",commandorcontrol:"CTRL",cmdorctrl:"CTRL",control:"CTRL",ctrl:"CTRL",alt:"ALT",option:"ALT",shift:"SHIFT"},o=["CTRL","ALT","SHIFT","LOGO"],i=new Set(["altgr"]),n={space:"space",tab:"Tab",backspace:"BackSpace",delete:"Delete",insert:"Insert",return:"Return",enter:"Return",esc:"Escape",escape:"Escape",up:"Up",down:"Down",left:"Left",right:"Right",home:"Home",end:"End",pageup:"Page_Up",pagedown:"Page_Down",capslock:"Caps_Lock",numlock:"Num_Lock",scrolllock:"Scroll_Lock",printscreen:"Print",volumeup:"XF86AudioRaiseVolume",volumedown:"XF86AudioLowerVolume",volumemute:"XF86AudioMute",medianexttrack:"XF86AudioNext",mediaprevioustrack:"XF86AudioPrev",mediastop:"XF86AudioStop",mediaplaypause:"XF86AudioPlay",numdec:"KP_Decimal",numadd:"KP_Add",numsub:"KP_Subtract",nummult:"KP_Multiply",numdiv:"KP_Divide",";":"semicolon","=":"equal",",":"comma","-":"minus",".":"period","/":"slash","`":"grave","[":"bracketleft","\\":"backslash","]":"bracketright","'":"apostrophe"},r={")":"0","!":"1","@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",":":";","+":"=",plus:"=","<":",",_:"-",">":".","?":"/","~":"`","{":"[","|":"\\","}":"]",'"':"'"};function c(e){return/^[a-z0-9]$/.test(e)?e:/^f([1-9]|1[0-9]|2[0-4])$/.test(e)?e.toUpperCase():/^num[0-9]$/.test(e)?`KP_${e.slice(3)}`:n[e]}},172(e){e.exports=require("dbus-native")},474(e){e.exports=require("node:events")}};const t={};function s(o){const i=t[o];if(void 0!==i)return i.exports;const n=t[o]={exports:{}};return e[o](n,n.exports,s),n.exports}let o={};(()=>{let e=o;Object.defineProperty(e,"__esModule",{value:!0}),e.toXdgTrigger=e.WaylandGlobalShortcut=void 0;const t=s(983),i=s(809);Object.defineProperty(e,"toXdgTrigger",{enumerable:!0,get:function(){return i.toXdgTrigger}});const n="org.freedesktop.DBus",r="org.freedesktop.portal.Desktop",c="/org/freedesktop/portal/desktop",a="org.freedesktop.portal.GlobalShortcuts",u="org.freedesktop.portal.Request",h="[WaylandGlobalShortcut]";function l(e,t,s,o){return{destination:r,path:c,interface:e,member:t,signature:s,body:o}}e.WaylandGlobalShortcut=class{appId;timeout;shortcuts=new Map;pendingRequests=new Map;queue=Promise.resolve();bus;sessionPath;tokenCounter=0;constructor(e={}){this.appId=e.appId??(process.env.CHROME_DESKTOP??"").replace(/\.desktop$/,""),this.timeout=e.timeout??1e4}register(e,t,s={}){return this.registerAll([e],t,s)}registerAll(e,t,s={}){return this.enqueue(async()=>{const o=[...this.shortcuts.values()];for(const n of e){const e=(0,i.toXdgTrigger)(n);if(!e||o.some(t=>t.id===e))return!1;const{description:r=n,trigger:c="release"}=s;o.push({id:e,description:r||n,trigger:c,callback:t})}return this.commit(o)})}isRegistered(e){const t=(0,i.toXdgTrigger)(e);return!!t&&this.shortcuts.has(t)}unregister(e){return this.enqueue(async()=>{const t=(0,i.toXdgTrigger)(e);if(!t||!this.shortcuts.has(t))return;const s=[...this.shortcuts.values()].filter(e=>e.id!==t);this.setShortcuts(s),await this.bind(s)||this.setShortcuts([])})}unregisterAll(){return this.enqueue(async()=>{this.setShortcuts([]),await this.closeSession()})}destroy(){return this.enqueue(async()=>{this.setShortcuts([]),await this.closeSession(),this.bus&&this.disconnect(this.bus,new Error("Destroyed"))})}enqueue(e){const t=this.queue.then(e);return this.queue=t.catch(()=>{}),t}setShortcuts(e){this.shortcuts.clear(),e.forEach(e=>this.shortcuts.set(e.id,e))}async commit(e){const t=[...this.shortcuts.values()];return await this.bind(e)?(this.setShortcuts(e),!0):(t.length&&!await this.bind(t)&&this.setShortcuts([]),!1)}async bind(e){if(await this.closeSession(),!e.length)return!0;try{this.sessionPath=await function(e,t){let s;const o=new Promise((e,o)=>{s=setTimeout(()=>o(new Error(`Timed out after ${t}ms`)),t)});return Promise.race([e,o]).finally(()=>clearTimeout(s))}(this.connect().then(()=>this.createSession()),this.timeout);const t=await this.bindShortcuts(this.sessionPath,e),s=e.filter(e=>!t.includes(e.id)).map(e=>e.id);if(!s.length)return!0;console.warn(h,"Shortcuts not bound by the portal:",s)}catch(e){console.error(h,"Failed to bind shortcuts:",e)}return await this.closeSession(),!1}async connect(){if(this.bus)return this.bus;const e=await t.DBusClient.connect();e.onDisconnect(t=>{console.error(h,"D-Bus connection lost:",t),this.disconnect(e,t)}),e.onSignal(e=>this.onSignal(e));try{await this.addMatch(e,u,"Response"),await this.addMatch(e,a,"Activated"),await this.addMatch(e,a,"Deactivated"),await this.registerAppId(e)}catch(t){throw this.disconnect(e,t),t}return this.bus=e,e}disconnect(e,t){this.bus===e&&(this.bus=void 0,this.sessionPath=void 0),this.pendingRequests.forEach(e=>e.reject(t)),this.pendingRequests.clear(),e.close()}addMatch(e,t,s){return e.call({destination:n,path:"/org/freedesktop/DBus",interface:n,member:"AddMatch",signature:"s",body:[`type='signal',sender='${r}',interface='${t}',member='${s}'`]})}async registerAppId(e){if(this.appId)try{await e.call(l("org.freedesktop.host.portal.Registry","Register","sa{sv}",[this.appId,{}]))}catch(e){console.warn(h,`Failed to register the app ID "${this.appId}", the portal may reject shortcuts:`,e)}else console.warn(h,"No app ID available, the portal may reject shortcuts.")}async createSession(){return(await this.request("CreateSession","a{sv}",e=>[{...e,session_handle_token:this.nextToken()}])).session_handle}async bindShortcuts(e,t){const s=t.map(e=>[e.id,{description:e.description,preferred_trigger:e.id}]);return((await this.request("BindShortcuts","oa(sa{sv})sa{sv}",t=>[e,s,"",t])).shortcuts??[]).map(([e])=>e)}async closeSession(){const e=this.sessionPath;if(this.sessionPath=void 0,e&&this.bus)try{await this.bus.call({destination:r,path:e,interface:"org.freedesktop.portal.Session",member:"Close"})}catch(e){console.warn(h,"Failed to close the portal session:",e)}}async request(e,t,s){const o=this.bus,i=this.nextToken(),n=o.uniqueName.slice(1).replace(/\./g,"_");let r=`${c}/request/${n}/${i}`;const u=new Promise((e,t)=>{this.pendingRequests.set(r,{resolve:e,reject:t})});try{const[n]=await o.call(l(a,e,t,s({handle_token:i})));n!==r&&(this.pendingRequests.set(n,this.pendingRequests.get(r)),this.pendingRequests.delete(r),r=n);const{code:c,results:h}=await u;if(0!==c)throw new Error(`${e} failed with response code ${c}`);return h}finally{this.pendingRequests.delete(r)}}onSignal(e){if(e.interface===u&&"Response"===e.member){const[t,s]=e.body;return void this.pendingRequests.get(e.path)?.resolve({code:t,results:s??{}})}if(e.interface===a&&("Activated"===e.member||"Deactivated"===e.member)){const[t,s]=e.body;if(t!==this.sessionPath)return;const o=this.shortcuts.get(s);if(!o||o.trigger!==("Activated"===e.member?"press":"release"))return;try{o.callback()}catch(e){console.error(h,`Error on shortcut "${s}" callback:`,e)}}}nextToken(){return`wgs_${process.pid}_${++this.tokenCounter}`}}})(),module.exports=o})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayland-global-shortcut",
3
- "version": "0.0.1-debug",
3
+ "version": "0.2.0",
4
4
  "description": "Global shortcuts on Wayland (and XWayland) through the XDG Desktop Portal.",
5
5
  "exports": {
6
6
  ".": {