Unify input method and keyboard layout switching

This commit is contained in:
David Heinemeier Hansson committed 2026-10-09 15:22:47 +02:00
1 parent e8b1a1f874
commit 73c0660fde
8 files changed
+99 -34

No files matched your search

+45 -6
View File
@@ -1,7 +1,10 @@
"""Live Fcitx state for the existing keyboard layout widget."""
from collections import Counter
import json
import os
import re
import subprocess
import sys
from gi.repository import Gio, GLib
@@ -25,14 +28,51 @@ def snapshot(bus):
return {"methods": methods, "current": info[0], "name": info[1], "label": info[4], "language": info[5]}
def cycle(bus):
state = snapshot(bus)
def cycle(bus, state=None):
state = state if state is not None else snapshot(bus)
methods = state["methods"]
if len(methods) > 1 and state["current"] in methods:
following = methods[(methods.index(state["current"]) + 1) % len(methods)]
call(bus, "SetCurrentIM", GLib.Variant("(s)", (following,)))
def layout_switches(keyboards):
typed = [keyboard for keyboard in keyboards if not re.match(
r"^(hl-virtual-keyboard|power-button|sleep-button|lid-switch|video-bus)", keyboard.get("name", ""))]
typed = [item for item in typed if len(item.get("layout", "").split(",")) > 1]
if not typed:
return []
# Consumer controls also carry the seat layout. Its frequency keeps an
# individually configured keyboard from taking over the desktop shortcut.
counts = Counter(item.get("layout", "") for item in keyboards)
layout = max((item["layout"] for item in typed), key=lambda layout: counts[layout])
keyboard = max((item for item in typed if item["layout"] == layout),
key=lambda item: item.get("active_layout_index", 0))
layout = keyboard.get("layout", "")
count = len(layout.split(","))
if count < 2:
return []
following = (keyboard.get("active_layout_index", 0) + 1) % count
return [["hyprctl", "switchxkblayout", item["name"], str(following)]
for item in keyboards if item.get("layout") == layout and item.get("name")]
def cycle_input():
# Fcitx keyboard entries count as input methods too. With just one method,
# switch the compositor layouts even if the bar widget has been removed.
try:
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
state = snapshot(bus)
except GLib.Error:
state = {}
if len(state.get("methods", [])) > 1:
cycle(bus, state)
else:
devices = subprocess.run(["hyprctl", "-j", "devices"], check=True, text=True, capture_output=True)
for command in layout_switches(json.loads(devices.stdout).get("keyboards", [])):
subprocess.run(command, check=True, capture_output=True)
class Indicator:
def __init__(self, bus):
self.bus = bus
@@ -130,10 +170,9 @@ def watch(bus):
if __name__ == "__main__":
try:
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
if sys.argv[1:] == ["cycle"]:
cycle(bus)
cycle_input()
else:
watch(bus)
except GLib.Error as error:
watch(Gio.bus_get_sync(Gio.BusType.SESSION, None))
except (GLib.Error, OSError, ValueError, subprocess.CalledProcessError) as error:
raise SystemExit(str(error))