Created
June 26, 2026 10:22
-
-
Save ananace/d76f2f2bfbdfd0e13444373040d12fee to your computer and use it in GitHub Desktop.
XDG Global Shortcuts example - Mumble push-to-talk
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/env ruby | |
| # frozen_string_literal: true | |
| require 'bundler/setup' | |
| require 'dbus' | |
| require_relative 'xdg_globalshortcuts' | |
| class MumbleProxy | |
| PTT_SHORTCUT = 'xdg-ptt' | |
| attr_reader :bus, :input, :iface | |
| def initialize | |
| @bus = DBus.session_bus | |
| @input = Portal.new(@bus) | |
| @iface = bus['net.sourceforge.mumble.mumble']['/']['net.sourceforge.mumble.Mumble'] | |
| input.on_event { |ev| handle_event(ev) } | |
| input.on_session do | |
| puts "Binding shortcuts..." | |
| bound = input.bind_shortcuts [ | |
| [PTT_SHORTCUT, Portal::Shortcut.new(description: 'Mumble Push-to-talk')] | |
| ] | |
| puts "Bound shortcuts:" | |
| bound['shortcuts'].each do |_, bind| | |
| puts " #{bind['description']} => #{bind['trigger_description']}" | |
| end | |
| end | |
| end | |
| def run! | |
| input.start | |
| loop = DBus::Main.new | |
| loop << bus | |
| loop.run | |
| end | |
| def handle_event(data) | |
| case data | |
| when Portal::ShortcutEvent | |
| if data.id == PTT_SHORTCUT | |
| if data.pressed? | |
| puts "Start talking" | |
| iface.startTalking | |
| else | |
| puts "Stop talking" | |
| iface.stopTalking | |
| end | |
| end | |
| else | |
| puts "Received #{data.inspect}" | |
| end | |
| end | |
| end | |
| app = MumbleProxy.new | |
| app.run! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # frozen_string_literal: true | |
| require 'dbus' | |
| require 'securerandom' | |
| class Portal | |
| attr_reader :bus, :session | |
| class Shortcut | |
| def initialize(description: nil, trigger: nil) | |
| @description = description | |
| @preferred_trigger = trigger | |
| end | |
| def to_hash | |
| { 'description' => @description, 'preferred_trigger' => @preferred_trigger }.compact | |
| end | |
| end | |
| ShortcutEvent = Struct.new(:id, :active) do | |
| def pressed? | |
| active | |
| end | |
| end | |
| def self.test | |
| p = Portal.new(DBus.session_bus) | |
| end | |
| def initialize(bus) | |
| @bus = bus | |
| @on_event = nil | |
| @on_session = nil | |
| global_shortcuts_service.on_signal('Activated') do |handle, id, ts, options| | |
| @on_event&.call(ShortcutEvent.new(id, true)) | |
| end | |
| global_shortcuts_service.on_signal('Deactivated') do |handle, id, ts, options| | |
| @on_event&.call(ShortcutEvent.new(id, false)) | |
| end | |
| end | |
| def start | |
| return if @session | |
| create_session | |
| end | |
| def on_session(&callback) | |
| @on_session = callback | |
| end | |
| def on_event(&callback) | |
| @on_event = callback | |
| end | |
| def bind_shortcuts(shortcuts, window: "") | |
| call_async(:BindShortcuts, session, shortcuts.map { |k,v| [k, v.to_hash] }, window) | |
| end | |
| def list_shortcuts | |
| call_async(:ListShortcuts, session) | |
| end | |
| def configure_shortcuts(window: "") | |
| call_async(:ConfigureShortcuts, session, window) | |
| end | |
| private | |
| def create_session | |
| options = { | |
| handle_token: generate_token, | |
| session_handle_token: generate_token | |
| } | |
| req = global_shortcuts_service.send(:CreateSession, options.transform_keys(&:to_s)) | |
| request_service(req).on_signal('Response') do |resp, results| | |
| raise "Failed to create session with error #{resp}" unless resp == 0 | |
| @session = results['session_handle'] | |
| @on_session&.call | |
| end | |
| end | |
| def call_async(meth, *args, **options, &block) | |
| options ||= { handle_token: generate_token } | |
| req = global_shortcuts_service.send(meth, *args, options.transform_keys(&:to_s)) | |
| ret = nil | |
| request_service(req).on_signal('Response') do |resp, results| | |
| raise "Request failed with error #{resp}" unless resp == 0 | |
| ret = results | |
| end | |
| # Workaround for Ruby's lack of proper async calls | |
| while ret.nil? | |
| if (m = bus.message_queue.pop) | |
| bus.process(m) | |
| end | |
| end | |
| ret | |
| end | |
| def generate_token | |
| SecureRandom.uuid.delete '-' | |
| end | |
| def global_shortcuts_service | |
| @gs ||= bus['org.freedesktop.portal.Desktop']['/org/freedesktop/portal/desktop']['org.freedesktop.portal.GlobalShortcuts'] | |
| end | |
| def request_service(handle) | |
| bus['org.freedesktop.portal.Desktop'][handle]['org.freedesktop.portal.Request'] | |
| end | |
| end |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
An example of how this looks when in use: