Skip to content

Instantly share code, notes, and snippets.

@ananace
Created June 26, 2026 10:22
Show Gist options
  • Select an option

  • Save ananace/d76f2f2bfbdfd0e13444373040d12fee to your computer and use it in GitHub Desktop.

Select an option

Save ananace/d76f2f2bfbdfd0e13444373040d12fee to your computer and use it in GitHub Desktop.
XDG Global Shortcuts example - Mumble push-to-talk
#!/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!
# 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
@ananace

ananace commented Jun 26, 2026

Copy link
Copy Markdown
Author

An example of how this looks when in use:

Screenshot_20260626_121816 Screenshot_20260626_121840 Screenshot_20260626_121852

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment