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.
This commit is contained in:
parent
88778ecbb4
commit
134de7bb4d
2 changed files with 82 additions and 15 deletions
24
tabbed-run
Executable file
24
tabbed-run
Executable file
|
|
@ -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)
|
||||||
|
|
@ -6,26 +6,17 @@
|
||||||
# ]
|
# ]
|
||||||
# ///
|
# ///
|
||||||
import argparse
|
import argparse
|
||||||
import gi
|
import atexit
|
||||||
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "4.0")
|
gi.require_version("Gtk", "4.0")
|
||||||
gi.require_version("Casilda", "1.0")
|
gi.require_version("Casilda", "1.0")
|
||||||
from gi.repository import GLib, Gtk, Casilda
|
from gi.repository import Gio, 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)
|
|
||||||
|
|
||||||
|
|
||||||
class TabbedCasilda(Gtk.Application):
|
class TabbedCasilda(Gtk.Application):
|
||||||
|
|
@ -33,6 +24,56 @@ class TabbedCasilda(Gtk.Application):
|
||||||
super().__init__(application_id="local.tabbed-casilda")
|
super().__init__(application_id="local.tabbed-casilda")
|
||||||
self.win = None
|
self.win = None
|
||||||
self.commands = commands
|
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):
|
def do_activate(self):
|
||||||
if self.win:
|
if self.win:
|
||||||
|
|
@ -46,8 +87,10 @@ class TabbedCasilda(Gtk.Application):
|
||||||
)
|
)
|
||||||
nb = Gtk.Notebook()
|
nb = Gtk.Notebook()
|
||||||
win.set_child(nb)
|
win.set_child(nb)
|
||||||
|
self.notebook = nb
|
||||||
|
self.start_control_channel()
|
||||||
for command in self.commands:
|
for command in self.commands:
|
||||||
make_tab(nb, command)
|
self.add_tab(command)
|
||||||
win.present()
|
win.present()
|
||||||
self.win = win
|
self.win = win
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue