Each tab carries a unique TABBED_CASILDA_TAB_ID; tabbed-run echoes it to the host so the new page is inserted right of the creating tab. New tabs are selected and focused, and the initial tab is focused on startup.
142 lines
4.4 KiB
Python
142 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
# /// script
|
|
# requires-python = ">=3.11,<3.14"
|
|
# dependencies = [
|
|
# "PyGObject",
|
|
# ]
|
|
# ///
|
|
import argparse
|
|
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 Gio, GLib, Gtk, Casilda
|
|
|
|
|
|
class TabbedCasilda(Gtk.Application):
|
|
def __init__(self, commands):
|
|
super().__init__(application_id="local.tabbed-casilda")
|
|
self.win = None
|
|
self.commands = commands
|
|
self.notebook = None
|
|
self.tab_ids = {}
|
|
self.next_tab_id = 0
|
|
self.ctrl_path = os.path.join(
|
|
os.environ.get("XDG_RUNTIME_DIR", "/tmp"), "tabbed-casilda.sock"
|
|
)
|
|
|
|
def add_tab(self, command, ctrl_spawn=False, creator_id=None):
|
|
tab_id = self.next_tab_id
|
|
self.next_tab_id += 1
|
|
compositor = Casilda.Compositor()
|
|
compositor.props.maximized = True
|
|
label = Gtk.Label(label=command)
|
|
position = -1
|
|
if creator_id is not None:
|
|
creator = self.tab_ids.get(creator_id)
|
|
if creator is not None:
|
|
page = self.notebook.page_num(creator)
|
|
if page != -1:
|
|
position = page + 1
|
|
self.notebook.insert_page(compositor, label, position)
|
|
self.tab_ids[tab_id] = compositor
|
|
self.focus_tab(tab_id)
|
|
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
|
|
env["TABBED_CASILDA_TAB_ID"] = str(tab_id)
|
|
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 focus_tab(self, tab_id):
|
|
compositor = self.tab_ids.get(tab_id)
|
|
if compositor is None:
|
|
return
|
|
page = self.notebook.page_num(compositor)
|
|
if page != -1:
|
|
self.notebook.set_current_page(page)
|
|
compositor.grab_focus()
|
|
|
|
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())
|
|
creator_line, _ = stream.read_line()
|
|
command_line, _ = stream.read_line()
|
|
if command_line:
|
|
creator_id = None
|
|
if creator_line:
|
|
creator_id = creator_line.decode().strip()
|
|
if not creator_id.isdigit():
|
|
creator_id = None
|
|
else:
|
|
creator_id = int(creator_id)
|
|
self.add_tab(command_line.decode(), ctrl_spawn=True, creator_id=creator_id)
|
|
return False
|
|
|
|
def do_activate(self):
|
|
if self.win:
|
|
self.win.present()
|
|
return
|
|
win = Gtk.ApplicationWindow(
|
|
application=self,
|
|
title="tabbed casilda",
|
|
default_width=1280,
|
|
default_height=720,
|
|
)
|
|
nb = Gtk.Notebook()
|
|
win.set_child(nb)
|
|
self.notebook = nb
|
|
self.start_control_channel()
|
|
for command in self.commands:
|
|
self.add_tab(command)
|
|
win.present()
|
|
self.focus_tab(0)
|
|
self.win = win
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(
|
|
description="GTK4 notebook, one tab per embedded Wayland terminal",
|
|
)
|
|
parser.add_argument(
|
|
"commands",
|
|
nargs="+",
|
|
metavar="COMMAND",
|
|
help='quoted command, e.g. "foot -T htop"',
|
|
)
|
|
args = parser.parse_args(sys.argv[1:])
|
|
app = TabbedCasilda(args.commands)
|
|
sys.exit(app.run([sys.argv[0]]))
|