by roundabout, Thursday, 2 July 2026, 09:04:09 (1782983049), pushed by roundabout, Thursday, 2 July 2026, 09:04:26 (1782983066)
Author identity: vlad <vlad.muntoiu@gmail.com>
550bd2202e10dd36a7e0083433a4d1583db3d6be
main.py
@@ -29,7 +29,7 @@ CDLL("libgtk4-layer-shell.so")
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Gtk4LayerShell", "1.0")
from gi.repository import Gtk, Gdk, GObject, Gtk4LayerShell, GLib, Gio
from gi.repository import Gtk, Gdk, GObject, Gtk4LayerShell, GLib, Gio, GObject
from pywayland.client import Display, EventQueue
from pywayland.protocol.wayland import WlRegistry, WlSeat
@@ -43,6 +43,7 @@ from pywayland.protocol.input_method_unstable_v2 import (
)
from pywayland.protocol.virtual_keyboard_unstable_v1 import ZwpVirtualKeyboardManagerV1
from xkbcommon import xkb
import ctypes
from cffi import FFI
@@ -213,12 +214,32 @@ class Plane(Gtk.Grid):
def make_xkb_keymap_from_unicodes(unicodes):
symbols = [
"key <SP00> { [ BackSpace ] };",
"key <SP01> { [ Return ] };",
"key <LEFT> { [ Left ] };"
"key <RGHT> { [ Right ] };"
"key <UP> { [ Up ] };"
"key <DOWN> { [ Down ] };"
"key <TAB> { [ Tab ] };",
"key <LCTL> { [ Control_L ] };",
"key <LALT> { [ Alt_L ] };",
"key <LSHF> { [ Shift_L ] };",
"key <LSPR> { [ Super_L ] };",
"key <ESCP> { [ Escape ] };",
"key <BKSP> { [ BackSpace ] };",
"key <RTRN> { [ Return ] };",
]
keycodes = [
"<SP00> = 4094;",
"<SP01> = 4095;",
"<LEFT> = 4084;",
"<RGHT> = 4085;",
"<UP> = 4086;",
"<DOWN> = 4087;",
"<TAB> = 4088;",
"<LCTL> = 4089;",
"<LALT> = 4090;",
"<LSHF> = 4091;",
"<LSPR> = 4092;",
"<ESCP> = 4093;",
"<BKSP> = 4094;",
"<RTRN> = 4095;",
]
for i, char in enumerate(unicodes):
keycode = 0x1000 + i
@@ -273,6 +294,23 @@ def register_client(client_id: str):
return proxy
class ModifierButton(Gtk.ToggleButton):
def __init__(self, app, mask, **kwargs):
super().__init__(**kwargs)
self.app = app
self.mask = mask
self.connect("toggled", self.toggle_modifier)
def toggle_modifier(self, button):
if button.get_active():
self.app.depressed_modifiers |= self.mask
else:
self.app.depressed_modifiers &= ~self.mask
self.app.send_modifiers()
class Kineboard(Gtk.Application):
def __init__(self):
Gtk.Application.__init__(
@@ -280,8 +318,10 @@ class Kineboard(Gtk.Application):
application_id="sm.puri.OSK0",
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE
)
self.xkb_context = xkb.Context()
self.revealer = None
self.switcher = None
self.depressed_modifiers = 0
self.character_index = {}
self.keymap_fd = None
self.vk = None
@@ -299,6 +339,10 @@ class Kineboard(Gtk.Application):
self.seat = None
self.vkm = None
self.serial = 0
self.extra_revealer = None
self.extra_viewport = None
self.extra_scroll = None
self.extra_controls = None
self.add_main_option(
"invoke",
ord("i"),
@@ -315,9 +359,9 @@ class Kineboard(Gtk.Application):
"Hide the keyboard",
None
)
def toggle_sidebar(self, button):
self.revealer.set_reveal_child(not self.revealer.get_reveal_child())
def send_modifiers(self):
self.vk.modifiers(self.depressed_modifiers, 0, 0, 0)
def do_startup(self):
Gtk.Application.do_startup(self)
@@ -326,13 +370,15 @@ class Kineboard(Gtk.Application):
self.window = Gtk.Window(application=self, title="Demo", focus_on_click=False, focusable=False, can_focus=False)
Gtk4LayerShell.init_for_window(self.window)
Gtk4LayerShell.set_keyboard_mode(self.window, Gtk4LayerShell.KeyboardMode.NONE)
self.window.set_default_size(16384, -1) # large width so that it spans the bottom
Gtk4LayerShell.set_anchor(self.window, Gtk4LayerShell.Edge.BOTTOM, True)
Gtk4LayerShell.set_anchor(self.window, Gtk4LayerShell.Edge.LEFT, True)
Gtk4LayerShell.set_anchor(self.window, Gtk4LayerShell.Edge.RIGHT, True)
Gtk4LayerShell.auto_exclusive_zone_enable(self.window)
Gtk4LayerShell.set_namespace(self.window, "osk")
self.box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
self.window.set_child(self.box)
self.main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, margin_top=8)
self.window.set_child(self.main_box)
self.stack = Gtk.Stack()
self.box.append(self.stack)
layouts = itertools.chain.from_iterable((f for f in path.iterdir() if f.is_file()) for path in get_layout_directories())
@@ -340,6 +386,13 @@ class Kineboard(Gtk.Application):
self.switcher = Gtk.StackSidebar(stack=self.stack)
self.revealer = Gtk.Revealer(child=self.switcher, transition_type=Gtk.RevealerTransitionType.SLIDE_RIGHT)
self.box.prepend(self.revealer)
self.extra_controls = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8,
margin_bottom=8, margin_start=8, margin_end=8)
self.extra_viewport = Gtk.Viewport(child=self.extra_controls)
self.extra_scroll = Gtk.ScrolledWindow(child=self.extra_viewport, propagate_natural_height=True)
self.extra_revealer = Gtk.Revealer(child=self.extra_scroll)
for layout in layouts:
stack = Gtk.Stack()
@@ -349,11 +402,17 @@ class Kineboard(Gtk.Application):
controls = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8, margin_top=0,
margin_bottom=8, margin_start=8, margin_end=8)
layout_button = Gtk.Button(child=Gtk.Image.new_from_icon_name("edit-symbolic"))
layout_button = Gtk.ToggleButton(child=Gtk.Image.new_from_icon_name("edit-symbolic"))
layout_button.set_size_request(48, -1)
layout_button.update_property([Gtk.AccessibleProperty.LABEL], ["Switch layout"])
layout_button.connect("clicked", self.toggle_sidebar)
controls.append(layout_button)
layout_button.bind_property("active", self.revealer, "reveal-child", GObject.BindingFlags.BIDIRECTIONAL)
control_button = Gtk.ToggleButton(child=Gtk.Image.new_from_icon_name("utilities-terminal-symbolic"))
control_button.set_size_request(48, -1)
control_button.update_property([Gtk.AccessibleProperty.LABEL], ["Terminal controls"])
controls.append(control_button)
control_button.bind_property("active", self.extra_revealer, "reveal-child", GObject.BindingFlags.BIDIRECTIONAL)
if len(data["keys"]) >= 2:
shift_button = Gtk.ToggleButton(
@@ -373,7 +432,7 @@ class Kineboard(Gtk.Application):
space_bar.connect("clicked", lambda x: self.typed(x, " "))
controls.append(space_bar)
backspace_button = Gtk.Button(child=Gtk.Image.new_from_icon_name("go-previous-symbolic"))
backspace_button = Gtk.Button(child=Gtk.Image.new_from_icon_name("edit-clear-symbolic"))
backspace_button.set_size_request(48, -1)
backspace_button.update_property([Gtk.AccessibleProperty.LABEL], ["Backspace"])
backspace_button.connect("clicked", lambda x: self.send_keysym(0xFF6))
@@ -388,7 +447,7 @@ class Kineboard(Gtk.Application):
for i, layer in enumerate(data["keys"]):
grid = Plane(self, layer, row_spacing=8, column_spacing=8, row_homogeneous=True,
column_homogeneous=True, margin_top=8, margin_bottom=8,
column_homogeneous=True, margin_bottom=8,
margin_end=8, margin_start=8)
self.all_characters += grid.all_characters
stack.add_named(grid, f"plane-{i}")
@@ -398,6 +457,9 @@ class Kineboard(Gtk.Application):
outer_box.append(controls)
self.stack.add_titled(outer_box, data["symbol"], data["symbol"])
self.main_box.append(self.extra_revealer)
self.main_box.append(self.box)
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = (ctypes.py_object,)
@@ -419,6 +481,36 @@ class Kineboard(Gtk.Application):
dialog.show()
self.keymap_text = make_xkb_keymap_from_unicodes(self.all_characters)
xkb_keymap = self.xkb_context.keymap_new_from_string(self.keymap_text)
state = xkb_keymap.state_new()
escape_button = Gtk.Button(label="Escape")
escape_button.connect("clicked", lambda x: self.send_keysym(0xFF5))
self.extra_controls.append(escape_button)
self.extra_controls.append(ModifierButton(self, 1 << xkb_keymap.mod_get_index("Control"), label="Control"))
self.extra_controls.append(ModifierButton(self, 1 << xkb_keymap.mod_get_index("Mod1"), label="Alt"))
self.extra_controls.append(ModifierButton(self, 1 << xkb_keymap.mod_get_index("Shift"), label="Shift"))
self.extra_controls.append(ModifierButton(self, 1 << xkb_keymap.mod_get_index("Mod4"), label="Super"))
tab_button = Gtk.Button(label="Tab")
tab_button.connect("clicked", lambda x: self.send_keysym(0xFF0))
self.extra_controls.append(tab_button)
left_button = Gtk.Button(child=Gtk.Image.new_from_icon_name("go-previous-symbolic"))
left_button.update_property([Gtk.AccessibleProperty.LABEL], ["Left"])
left_button.connect("clicked", lambda x: self.send_keysym(0xFEC))
self.extra_controls.append(left_button)
down_button = Gtk.Button(child=Gtk.Image.new_from_icon_name("go-down-symbolic"))
down_button.update_property([Gtk.AccessibleProperty.LABEL], ["Down"])
down_button.connect("clicked", lambda x: self.send_keysym(0xFEF))
self.extra_controls.append(down_button)
up_button = Gtk.Button(child=Gtk.Image.new_from_icon_name("go-up-symbolic"))
up_button.update_property([Gtk.AccessibleProperty.LABEL], ["Up"])
up_button.connect("clicked", lambda x: self.send_keysym(0xFEE))
self.extra_controls.append(up_button)
right_button = Gtk.Button(child=Gtk.Image.new_from_icon_name("go-next-symbolic"))
right_button.update_property([Gtk.AccessibleProperty.LABEL], ["Right"])
right_button.connect("clicked", lambda x: self.send_keysym(0xFED))
self.extra_controls.append(right_button)
self.character_index = {}
for i, character in enumerate(self.all_characters):
self.character_index[character] = i