From 134de7bb4d425006c860764d8cd6198182b7df71 Mon Sep 17 00:00:00 2001 From: Andrew Hamilton Date: Thu, 13 Aug 2026 09:16:03 +1000 Subject: [PATCH] Add tabbed-run control channel to open apps in new tabs Host listens on a unix socket; tabbed-run sends a command to open it as a new maximized casilda tab. Spawned children inherit the full environment plus TABBED_CASILDA_CTRL so tabbed-run works from any tab. --- tabbed-run | 24 ++++++++++++++++ tabbed_casilda.py | 73 +++++++++++++++++++++++++++++++++++++---------- 2 files changed, 82 insertions(+), 15 deletions(-) create mode 100755 tabbed-run diff --git a/tabbed-run b/tabbed-run new file mode 100755 index 0000000..2c222fa --- /dev/null +++ b/tabbed-run @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +import os +import shlex +import socket +import sys + +ctrl = os.environ.get("TABBED_CASILDA_CTRL") +if not ctrl: + print( + "tabbed-run: TABBED_CASILDA_CTRL not set (run inside a tabbed-casilda terminal)", + file=sys.stderr, + ) + sys.exit(1) +if len(sys.argv) < 2: + print("usage: tabbed-run COMMAND [ARGS...]", file=sys.stderr) + sys.exit(1) +payload = (shlex.join(sys.argv[1:]) + "\n").encode() +try: + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s: + s.connect(ctrl) + s.sendall(payload) +except OSError as e: + print(f"tabbed-run: cannot reach host: {e}", file=sys.stderr) + sys.exit(1) diff --git a/tabbed_casilda.py b/tabbed_casilda.py index 3f24c94..d3e3b75 100644 --- a/tabbed_casilda.py +++ b/tabbed_casilda.py @@ -6,26 +6,17 @@ # ] # /// import argparse -import gi +import atexit +import os import shlex import shutil import sys +import gi + gi.require_version("Gtk", "4.0") gi.require_version("Casilda", "1.0") -from gi.repository import GLib, Gtk, Casilda - - -def make_tab(notebook, command): - compositor = Casilda.Compositor() - compositor.props.maximized = True - label = Gtk.Label(label=command) - notebook.append_page(compositor, label) - argv = shlex.split(command) - if not argv: - return - argv[0] = shutil.which(argv[0]) or argv[0] - compositor.spawn_async(None, argv, None, GLib.SpawnFlags.DEFAULT) +from gi.repository import Gio, GLib, Gtk, Casilda class TabbedCasilda(Gtk.Application): @@ -33,6 +24,56 @@ class TabbedCasilda(Gtk.Application): super().__init__(application_id="local.tabbed-casilda") self.win = None self.commands = commands + self.notebook = None + self.ctrl_path = os.path.join( + os.environ.get("XDG_RUNTIME_DIR", "/tmp"), "tabbed-casilda.sock" + ) + + def add_tab(self, command, ctrl_spawn=False): + compositor = Casilda.Compositor() + compositor.props.maximized = True + label = Gtk.Label(label=command) + self.notebook.append_page(compositor, label) + argv = shlex.split(command) + if not argv: + return + argv[0] = shutil.which(argv[0]) or argv[0] + repo_dir = os.path.dirname(os.path.abspath(__file__)) + env = dict(os.environ) + env["PATH"] = f"{repo_dir}:{env.get('PATH', '')}" + env["TABBED_CASILDA_CTRL"] = self.ctrl_path + if ctrl_spawn: + env["G_APPLICATION_NON_UNIQUE"] = "1" + envp = [f"{k}={v}" for k, v in env.items()] + compositor.spawn_async(None, argv, envp, GLib.SpawnFlags.DEFAULT) + + def start_control_channel(self): + try: + os.unlink(self.ctrl_path) + except FileNotFoundError: + pass + service = Gio.SocketService() + address = Gio.UnixSocketAddress.new(self.ctrl_path) + service.add_address( + address, Gio.SocketType.STREAM, Gio.SocketProtocol.DEFAULT, None + ) + service.connect("incoming", self.on_incoming) + service.start() + self.ctrl_service = service + atexit.register(self.cleanup_socket) + + def cleanup_socket(self): + try: + os.unlink(self.ctrl_path) + except FileNotFoundError: + pass + + def on_incoming(self, service, connection, source_object): + stream = Gio.DataInputStream.new(connection.get_input_stream()) + line, _ = stream.read_line() + if line: + self.add_tab(line.decode(), ctrl_spawn=True) + return False def do_activate(self): if self.win: @@ -46,8 +87,10 @@ class TabbedCasilda(Gtk.Application): ) nb = Gtk.Notebook() win.set_child(nb) + self.notebook = nb + self.start_control_channel() for command in self.commands: - make_tab(nb, command) + self.add_tab(command) win.present() self.win = win