diff --git a/README.md b/README.md index aba5f0a..1feb352 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ -# vtabs +# Playpen -A proof-of-concept terminal emulator with **vertical tabs**, built on -[libghostty-vt](https://github.com/ghostty-org/ghostty) and GTK4/libadwaita. +A proof-of-concept workspace built on +[libghostty-vt](https://github.com/ghostty-org/ghostty) and GTK4/libadwaita: +**vertical tabs**, each holding a split tree of terminal and web panes, with +**saved layouts** that open a whole arrangement — directories, scripts and all — +in one go. The sidebar holds the window controls, a new-tab button, and one row per tab — -the layout Zen Browser uses for vertical tabs — with the terminal inset to its +the layout Zen Browser uses for vertical tabs — with the content inset to its right. ## Quick start @@ -23,7 +26,7 @@ mise run install # builds release, installs into ~/.local mise run uninstall ``` -That puts the binary in `~/.local/libexec/vtabs`, a launcher on `~/.local/bin`, +That puts the binary in `~/.local/libexec/playpen`, a launcher on `~/.local/bin`, a desktop entry in `~/.local/share/applications`, and the icon in the hicolor theme, then refreshes the desktop and icon caches. `mise tasks` lists the rest (`build`, `run`, `fmt`, `screenshot`). @@ -200,7 +203,7 @@ because shells that set up line editing discard whatever was buffered while they were initializing. The shell's first output is the signal that it is reading, so that is when the script goes in. -Layouts live in `~/.config/vtabs/layouts.json` (or `$XDG_CONFIG_HOME`), and the +Layouts live in `~/.config/playpen/layouts.json` (or `$XDG_CONFIG_HOME`), and the file is meant to be edited by hand as well — *Reload from disk* picks up changes. It is JSON because the app writes it too, and a format that round-trips without a hand-written emitter is worth more here than a prettier diff --git a/build.zig b/build.zig index 0913e8c..e2e3df7 100644 --- a/build.zig +++ b/build.zig @@ -21,7 +21,7 @@ pub fn build(b: *std.Build) void { }); const exe = b.addExecutable(.{ - .name = "vtabs", + .name = "playpen", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, diff --git a/build.zig.zon b/build.zig.zon index fadaa2c..2ae70a9 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,7 +1,9 @@ .{ - .name = .vtabs, + .name = .playpen, .version = "0.1.0", - .fingerprint = 0x833f5d0dd064e02a, + // Half package-name hash, half per-package constant, so renaming the + // package invalidates it. Regenerated when vtabs became playpen. + .fingerprint = 0x5a26ca72bb4d2c53, .minimum_zig_version = "0.16.0", .paths = .{ "build.zig", diff --git a/dist/dev.greyson.vtabs.desktop.in b/dist/dev.greyson.playpen.desktop.in similarity index 77% rename from dist/dev.greyson.vtabs.desktop.in rename to dist/dev.greyson.playpen.desktop.in index 5b43c4a..52acad2 100644 --- a/dist/dev.greyson.vtabs.desktop.in +++ b/dist/dev.greyson.playpen.desktop.in @@ -1,16 +1,16 @@ [Desktop Entry] Type=Application -Name=vtabs +Name=Playpen GenericName=Terminal -Comment=Terminal emulator with vertical tabs +Comment=Terminal with vertical tabs, web panes and saved layouts # Substituted with an absolute path by `mise run install`, because desktop # launchers do not reliably have ~/.local/bin on PATH. Exec=@EXEC@ -Icon=dev.greyson.vtabs +Icon=dev.greyson.playpen Terminal=false Categories=System;TerminalEmulator; Keywords=shell;prompt;command;commandline;cmd;terminal;tabs; StartupNotify=true # Lets the compositor match the running window to this entry, which is what # gives the window its icon. Must equal the GApplication id. -StartupWMClass=dev.greyson.vtabs +StartupWMClass=dev.greyson.playpen diff --git a/dist/dev.greyson.vtabs.svg b/dist/dev.greyson.playpen.svg similarity index 95% rename from dist/dev.greyson.vtabs.svg rename to dist/dev.greyson.playpen.svg index 0c3e30b..baa6a8b 100644 --- a/dist/dev.greyson.vtabs.svg +++ b/dist/dev.greyson.playpen.svg @@ -1,6 +1,6 @@ diff --git a/flake.nix b/flake.nix index 393bf21..b7e14f4 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "Vertical-tab terminal built on libghostty-vt"; + description = "Playpen: vertical-tab terminal with web panes and saved layouts, built on libghostty-vt"; inputs = { # Ghostty tracks nixpkgs-unstable to get GTK 4.20 / GNOME 49. We follow @@ -42,7 +42,7 @@ in { devShells = forAllSystems (pkgs: system: { default = pkgs.mkShell { - name = "vtabs"; + name = "playpen"; nativeBuildInputs = [ zig.packages.${system}."0.16.0" @@ -106,16 +106,16 @@ # an RPATH into the Nix store and runs fine outside this shell, but # it still has to be pointed at this data or GTK aborts on a missing # schema and falls back to broken-image icons. - export VTABS_GSETTINGS_SCHEMA_DIR="${pkgs.gtk4}/share/gsettings-schemas/${pkgs.gtk4.name}/glib-2.0/schemas" - export VTABS_RUNTIME_DATA_DIRS="${pkgs.gtk4}/share/gsettings-schemas/${pkgs.gtk4.name}:${pkgs.adwaita-icon-theme}/share:${pkgs.hicolor-icon-theme}/share" + export PLAYPEN_GSETTINGS_SCHEMA_DIR="${pkgs.gtk4}/share/gsettings-schemas/${pkgs.gtk4.name}/glib-2.0/schemas" + export PLAYPEN_RUNTIME_DATA_DIRS="${pkgs.gtk4}/share/gsettings-schemas/${pkgs.gtk4.name}:${pkgs.adwaita-icon-theme}/share:${pkgs.hicolor-icon-theme}/share" # The GIO module holding the TLS backend. Same story as the schemas: # it is found by path at runtime, so the installer bakes it in too. - export VTABS_GIO_MODULE_DIR="${pkgs.glib-networking}/lib/gio/modules" + export PLAYPEN_GIO_MODULE_DIR="${pkgs.glib-networking}/lib/gio/modules" - export GSETTINGS_SCHEMA_DIR="$VTABS_GSETTINGS_SCHEMA_DIR" - export XDG_DATA_DIRS="$VTABS_RUNTIME_DATA_DIRS:$XDG_DATA_DIRS" - export GIO_EXTRA_MODULES="$VTABS_GIO_MODULE_DIR''${GIO_EXTRA_MODULES:+:$GIO_EXTRA_MODULES}" + export GSETTINGS_SCHEMA_DIR="$PLAYPEN_GSETTINGS_SCHEMA_DIR" + export XDG_DATA_DIRS="$PLAYPEN_RUNTIME_DATA_DIRS:$XDG_DATA_DIRS" + export GIO_EXTRA_MODULES="$PLAYPEN_GIO_MODULE_DIR''${GIO_EXTRA_MODULES:+:$GIO_EXTRA_MODULES}" # WebKit's web process renders through mesa, and every piece of that # path has to come from the *same* mesa. Two things go wrong on a @@ -133,13 +133,13 @@ # Pointing all three at this mesa keeps the stack self-consistent. # It still uses the real GPU through /dev/dri; this is not a # software-rendering fallback. - export VTABS_EGL_VENDOR_DIR="${pkgs.mesa}/share/glvnd/egl_vendor.d" - export VTABS_DRI_DRIVERS_PATH="${pkgs.mesa}/lib/dri" - export VTABS_GBM_BACKENDS_PATH="${pkgs.mesa}/lib/gbm" + export PLAYPEN_EGL_VENDOR_DIR="${pkgs.mesa}/share/glvnd/egl_vendor.d" + export PLAYPEN_DRI_DRIVERS_PATH="${pkgs.mesa}/lib/dri" + export PLAYPEN_GBM_BACKENDS_PATH="${pkgs.mesa}/lib/gbm" - export __EGL_VENDOR_LIBRARY_DIRS="$VTABS_EGL_VENDOR_DIR" - export LIBGL_DRIVERS_PATH="$VTABS_DRI_DRIVERS_PATH" - export GBM_BACKENDS_PATH="$VTABS_GBM_BACKENDS_PATH" + export __EGL_VENDOR_LIBRARY_DIRS="$PLAYPEN_EGL_VENDOR_DIR" + export LIBGL_DRIVERS_PATH="$PLAYPEN_DRI_DRIVERS_PATH" + export GBM_BACKENDS_PATH="$PLAYPEN_GBM_BACKENDS_PATH" ''; }; }); diff --git a/mise.toml b/mise.toml index 9c2a3fc..207d308 100644 --- a/mise.toml +++ b/mise.toml @@ -10,7 +10,7 @@ description = "Build a debug binary (leak checking, safety checks)" run = "nix develop --command zig build" [tasks.run] -description = "Build and run vtabs" +description = "Build and run playpen" run = "nix develop --command zig build run" [tasks.fmt] @@ -18,12 +18,12 @@ description = "Format all Zig source" run = "nix develop --command zig fmt build.zig src" [tasks.screenshot] -description = "Screenshot vtabs in a throwaway headless compositor" +description = "Screenshot playpen in a throwaway headless compositor" depends = ["build-debug"] run = "nix develop --command ./shot.sh" [tasks.install] -description = "Install vtabs into ~/.local with a desktop entry and icon" +description = "Install playpen into ~/.local with a desktop entry and icon" depends = ["build"] run = ''' #!/usr/bin/env bash @@ -31,10 +31,10 @@ set -euo pipefail data="${XDG_DATA_HOME:-$HOME/.local/share}" bindir="$HOME/.local/bin" -libexec="$HOME/.local/libexec/vtabs" +libexec="$HOME/.local/libexec/playpen" apps="$data/applications" icons="$data/icons/hicolor/scalable/apps" -gcroots="$data/vtabs/gcroots" +gcroots="$data/playpen/gcroots" # The binary carries an RPATH into the Nix store, so it runs anywhere, but it # still has to be told where GTK's GSettings schemas and icon themes live or @@ -42,21 +42,21 @@ gcroots="$data/vtabs/gcroots" # https:// page in a web pane fails, and which mesa to render web panes # through or WebKit's web process dies on startup. Read those paths back out # of the dev shell that built it rather than hardcoding store hashes. -schemas="$(nix develop --command printenv VTABS_GSETTINGS_SCHEMA_DIR)" -datadirs="$(nix develop --command printenv VTABS_RUNTIME_DATA_DIRS)" -giomodules="$(nix develop --command printenv VTABS_GIO_MODULE_DIR)" -eglvendor="$(nix develop --command printenv VTABS_EGL_VENDOR_DIR)" -dridrivers="$(nix develop --command printenv VTABS_DRI_DRIVERS_PATH)" -gbmbackends="$(nix develop --command printenv VTABS_GBM_BACKENDS_PATH)" +schemas="$(nix develop --command printenv PLAYPEN_GSETTINGS_SCHEMA_DIR)" +datadirs="$(nix develop --command printenv PLAYPEN_RUNTIME_DATA_DIRS)" +giomodules="$(nix develop --command printenv PLAYPEN_GIO_MODULE_DIR)" +eglvendor="$(nix develop --command printenv PLAYPEN_EGL_VENDOR_DIR)" +dridrivers="$(nix develop --command printenv PLAYPEN_DRI_DRIVERS_PATH)" +gbmbackends="$(nix develop --command printenv PLAYPEN_GBM_BACKENDS_PATH)" mkdir -p "$bindir" "$libexec" "$apps" "$icons" "$gcroots" -install -m755 zig-out/bin/vtabs "$libexec/vtabs" +install -m755 zig-out/bin/playpen "$libexec/playpen" # A launcher script rather than a symlink: a desktop launcher starts the app # with a minimal environment, so the GTK runtime paths must be set here. # Existing values are preserved so this composes with the rest of the session. -cat > "$bindir/vtabs" < "$bindir/playpen" < "$apps/dev.greyson.vtabs.desktop" -install -m644 dist/dev.greyson.vtabs.svg "$icons/dev.greyson.vtabs.svg" +sed "s|@EXEC@|$bindir/playpen|" dist/dev.greyson.playpen.desktop.in \ + > "$apps/dev.greyson.playpen.desktop" +install -m644 dist/dev.greyson.playpen.svg "$icons/dev.greyson.playpen.svg" # Pin the store paths the installed binary links against. Without this a # later `nix-collect-garbage` deletes GTK and the installed app stops @@ -86,7 +86,7 @@ rm -f "$gcroots"/* { # What the dynamic linker needs, read straight out of the binary. if command -v readelf >/dev/null; then - readelf -d "$libexec/vtabs" \ + readelf -d "$libexec/playpen" \ | sed -n 's/.*R\(UN\)\?PATH.*\[\(.*\)\]/\2/p' \ | tr ':' '\n' else @@ -112,7 +112,7 @@ echo "pinned $(find "$gcroots" -maxdepth 1 -type l | wc -l) store paths against command -v update-desktop-database >/dev/null && update-desktop-database -q "$apps" || true command -v gtk-update-icon-cache >/dev/null && gtk-update-icon-cache -qtf "$data/icons/hicolor" 2>/dev/null || true -echo "installed $bindir/vtabs" +echo "installed $bindir/playpen" case ":$PATH:" in *":$bindir:"*) ;; *) echo "note: $bindir is not on your PATH; the desktop entry still works" >&2 ;; @@ -128,15 +128,15 @@ set -euo pipefail data="${XDG_DATA_HOME:-$HOME/.local/share}" apps="$data/applications" -rm -f "$HOME/.local/bin/vtabs" -rm -rf "$HOME/.local/libexec/vtabs" -rm -f "$apps/dev.greyson.vtabs.desktop" -rm -f "$data/icons/hicolor/scalable/apps/dev.greyson.vtabs.svg" +rm -f "$HOME/.local/bin/playpen" +rm -rf "$HOME/.local/libexec/playpen" +rm -f "$apps/dev.greyson.playpen.desktop" +rm -f "$data/icons/hicolor/scalable/apps/dev.greyson.playpen.svg" # Dropping the roots lets Nix reclaim the runtime closure on its next GC. -rm -rf "$data/vtabs" +rm -rf "$data/playpen" command -v update-desktop-database >/dev/null && update-desktop-database -q "$apps" || true command -v gtk-update-icon-cache >/dev/null && gtk-update-icon-cache -qtf "$data/icons/hicolor" 2>/dev/null || true -echo "uninstalled vtabs" +echo "uninstalled playpen" ''' diff --git a/shot.sh b/shot.sh index 1bde936..6d63ec7 100755 --- a/shot.sh +++ b/shot.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Run vtabs inside a throwaway headless Sway and screenshot it. +# Run playpen inside a throwaway headless Sway and screenshot it. # # This keeps UI checks out of the developer's real Wayland session: nothing # pops up on screen, and it works even while the session is locked. @@ -8,7 +8,7 @@ set -u cd "$(dirname "$0")" -OUT="${1:-/tmp/vtabs-shot.png}" +OUT="${1:-/tmp/playpen-shot.png}" KEYS="${2:-}" SETTLE="${3:-2}" @@ -23,14 +23,14 @@ CONF # The nix dev shell exports bash functions into the environment. Any /bin/sh # the terminal's child spawns tries to import them and spews parse errors that -# have nothing to do with vtabs, so drop them just before exec. +# have nothing to do with playpen, so drop them just before exec. cat > "$RUNDIR/launch.sh" <<'LAUNCH' #!/usr/bin/env bash while read -r fn; do unset -f "$fn" 2>/dev/null; done < <(declare -Fx | awk '{print $3}') -exec "$VTABS_BIN" +exec "$PLAYPEN_BIN" LAUNCH chmod +x "$RUNDIR/launch.sh" -export VTABS_BIN="$PWD/zig-out/bin/vtabs" +export PLAYPEN_BIN="$PWD/zig-out/bin/playpen" export WLR_BACKENDS=headless export WLR_LIBINPUT_NO_DEVICES=1 @@ -96,7 +96,7 @@ if [ -n "$KEYS" ]; then sleep 1.5 fi -if [ -n "${VTABS_SHOT_DEBUG:-}" ]; then +if [ -n "${PLAYPEN_SHOT_DEBUG:-}" ]; then echo "--- swaymsg get_tree (apps) ---" >&2 swaymsg -t get_tree 2>&1 | grep -E '"(app_id|name|pid)"' | head -30 >&2 echo "--- sway log ---" >&2 diff --git a/src/Browser.zig b/src/Browser.zig index 8c518e0..2e69d80 100644 --- a/src/Browser.zig +++ b/src/Browser.zig @@ -177,11 +177,11 @@ pub fn navigate(self: *Browser, input: []const u8) void { fn buildNav(self: *Browser) *gtk.Widget { const nav = gtk.Box.new(.horizontal, 2); - nav.as(gtk.Widget).addCssClass("vtabs-nav"); + nav.as(gtk.Widget).addCssClass("playpen-nav"); for ([_]*gtk.Button{ self.back, self.forward, self.reload }) |button| { button.as(gtk.Widget).addCssClass("flat"); - button.as(gtk.Widget).addCssClass("vtabs-nav-button"); + button.as(gtk.Widget).addCssClass("playpen-nav-button"); nav.append(button.as(gtk.Widget)); } @@ -190,7 +190,7 @@ fn buildNav(self: *Browser) *gtk.Widget { _ = gtk.Button.signals.clicked.connect(self.reload, *Browser, &onReload, self, .{}); self.entry.setPlaceholderText("Enter address or search"); - self.entry.as(gtk.Widget).addCssClass("vtabs-nav-entry"); + self.entry.as(gtk.Widget).addCssClass("playpen-nav-entry"); self.entry.as(gtk.Widget).setHexpand(1); _ = gtk.Entry.signals.activate.connect(self.entry, *Browser, &onEntryActivate, self, .{}); nav.append(self.entry.as(gtk.Widget)); diff --git a/src/Layouts.zig b/src/Layouts.zig index 7b1240a..c59ad99 100644 --- a/src/Layouts.zig +++ b/src/Layouts.zig @@ -115,17 +115,17 @@ pub fn find(self: *Layouts, name: []const u8) ?*Layout { // ------------------------------------------------------------------------- // Paths -/// `$XDG_CONFIG_HOME/vtabs/layouts.json`, or `~/.config/vtabs/layouts.json` +/// `$XDG_CONFIG_HOME/playpen/layouts.json`, or `~/.config/playpen/layouts.json` /// when that isn't set — whichever GLib reports as the user's config dir. pub fn configPath(buf: []u8) ?[:0]const u8 { const dir = std.mem.span(glib.getUserConfigDir()); - return std.fmt.bufPrintZ(buf, "{s}/vtabs/layouts.json", .{dir}) catch null; + return std.fmt.bufPrintZ(buf, "{s}/playpen/layouts.json", .{dir}) catch null; } /// The directory `configPath` lives in. fn configDir(buf: []u8) ?[:0]const u8 { const dir = std.mem.span(glib.getUserConfigDir()); - return std.fmt.bufPrintZ(buf, "{s}/vtabs", .{dir}) catch null; + return std.fmt.bufPrintZ(buf, "{s}/playpen", .{dir}) catch null; } // ------------------------------------------------------------------------- diff --git a/src/OpenLayoutDialog.zig b/src/OpenLayoutDialog.zig index 7a56306..3d00dee 100644 --- a/src/OpenLayoutDialog.zig +++ b/src/OpenLayoutDialog.zig @@ -58,10 +58,10 @@ pub fn present( self.window.setTransientFor(parent); self.window.setModal(1); self.window.setDefaultSize(480, -1); - self.window.as(gtk.Widget).addCssClass("vtabs-dialog"); + self.window.as(gtk.Widget).addCssClass("playpen-dialog"); const content = gtk.Box.new(.vertical, 12); - content.as(gtk.Widget).addCssClass("vtabs-dialog-content"); + content.as(gtk.Widget).addCssClass("playpen-dialog-content"); const fields = gtk.Grid.new(); fields.setRowSpacing(8); @@ -75,7 +75,7 @@ pub fn present( const text = if (param.description.len > 0) param.description else param.name; label.setText(std.fmt.bufPrintZ(&label_buf, "{s}", .{text}) catch "parameter"); label.setXalign(0); - label.as(gtk.Widget).addCssClass("vtabs-dialog-label"); + label.as(gtk.Widget).addCssClass("playpen-dialog-label"); fields.attach(label.as(gtk.Widget), 0, @intCast(i), 1, 1); const entry = gtk.Entry.new(); diff --git a/src/Pane.zig b/src/Pane.zig index acdfe01..3965fee 100644 --- a/src/Pane.zig +++ b/src/Pane.zig @@ -166,7 +166,7 @@ pub fn create(alloc: std.mem.Allocator, view: *View, spec: Spec) !*Pane { _ = self.box.as(gobject.Object).refSink(); const box_widget = self.box.as(gtk.Widget); - box_widget.addCssClass("vtabs-pane"); + box_widget.addCssClass("playpen-pane"); box_widget.setHexpand(1); box_widget.setVexpand(1); // Clip the content to the pane's rounded corners. A terminal paints a @@ -224,39 +224,39 @@ pub fn setActive(self: *Pane, active: bool) void { fn buildHeader(self: *Pane) void { const header = self.header; - header.as(gtk.Widget).addCssClass("vtabs-pane-header"); + header.as(gtk.Widget).addCssClass("playpen-pane-header"); // The whole strip is the drag handle, so advertise that with the cursor. header.as(gtk.Widget).setCursorFromName("grab"); // With two kinds of pane in a view, the title alone no longer says what // you're looking at, so the header leads with the kind. const icon = gtk.Image.newFromIconName(self.kind.iconName()); - icon.as(gtk.Widget).addCssClass("vtabs-pane-icon"); + icon.as(gtk.Widget).addCssClass("playpen-pane-icon"); header.append(icon.as(gtk.Widget)); self.label.setXalign(0); self.label.setEllipsize(.end); self.label.as(gtk.Widget).setHexpand(1); - self.label.as(gtk.Widget).addCssClass("vtabs-pane-title"); + self.label.as(gtk.Widget).addCssClass("playpen-pane-title"); header.append(self.label.as(gtk.Widget)); const split = gtk.Button.newFromIconName("list-add-symbolic"); split.as(gtk.Widget).addCssClass("flat"); - split.as(gtk.Widget).addCssClass("vtabs-pane-button"); + split.as(gtk.Widget).addCssClass("playpen-pane-button"); split.as(gtk.Widget).setTooltipText("New terminal in this view (Ctrl+Shift+E)"); _ = gtk.Button.signals.clicked.connect(split, *Pane, &onSplitClicked, self, .{}); header.append(split.as(gtk.Widget)); const web = gtk.Button.newFromIconName("web-browser-symbolic"); web.as(gtk.Widget).addCssClass("flat"); - web.as(gtk.Widget).addCssClass("vtabs-pane-button"); + web.as(gtk.Widget).addCssClass("playpen-pane-button"); web.as(gtk.Widget).setTooltipText("New web view in this view (Ctrl+Shift+B)"); _ = gtk.Button.signals.clicked.connect(web, *Pane, &onWebClicked, self, .{}); header.append(web.as(gtk.Widget)); const close = gtk.Button.newFromIconName("window-close-symbolic"); close.as(gtk.Widget).addCssClass("flat"); - close.as(gtk.Widget).addCssClass("vtabs-pane-button"); + close.as(gtk.Widget).addCssClass("playpen-pane-button"); close.as(gtk.Widget).setTooltipText("Close pane (Ctrl+Shift+W)"); _ = gtk.Button.signals.clicked.connect(close, *Pane, &onCloseClicked, self, .{}); header.append(close.as(gtk.Widget)); @@ -269,7 +269,7 @@ fn buildHeader(self: *Pane) void { // pane being dragged is recorded on the view instead: drags never leave this // process, and passing a raw pointer through a GValue buys nothing. -const drag_payload = "vtabs-pane"; +const drag_payload = "playpen-pane"; fn installDragSource(self: *Pane) void { const source = gtk.DragSource.new(); diff --git a/src/SaveLayoutDialog.zig b/src/SaveLayoutDialog.zig index cf53359..1d34f3b 100644 --- a/src/SaveLayoutDialog.zig +++ b/src/SaveLayoutDialog.zig @@ -118,10 +118,10 @@ pub fn present( self.window.setTransientFor(parent); self.window.setModal(1); self.window.setDefaultSize(560, 620); - self.window.as(gtk.Widget).addCssClass("vtabs-dialog"); + self.window.as(gtk.Widget).addCssClass("playpen-dialog"); const content = gtk.Box.new(.vertical, 12); - content.as(gtk.Widget).addCssClass("vtabs-dialog-content"); + content.as(gtk.Widget).addCssClass("playpen-dialog-content"); // ---- name ---------------------------------------------------------- content.append(heading("Name")); @@ -169,7 +169,7 @@ pub fn present( // ---- footer ---------------------------------------------------------- self.error_label.setXalign(0); - self.error_label.as(gtk.Widget).addCssClass("vtabs-dialog-error"); + self.error_label.as(gtk.Widget).addCssClass("playpen-dialog-error"); self.error_label.as(gtk.Widget).setVisible(0); content.append(self.error_label.as(gtk.Widget)); @@ -180,7 +180,7 @@ pub fn present( const buttons = gtk.Box.new(.horizontal, 8); buttons.as(gtk.Widget).setHalign(.end); - buttons.as(gtk.Widget).addCssClass("vtabs-dialog-actions"); + buttons.as(gtk.Widget).addCssClass("playpen-dialog-actions"); const cancel = gtk.Button.newWithLabel("Cancel"); _ = gtk.Button.signals.clicked.connect(cancel, *SaveLayoutDialog, &onCancel, self, .{}); @@ -214,7 +214,7 @@ fn buildPaneSection(self: *SaveLayoutDialog, index: usize, leaf: *Layouts.Node) const spec = leaf.pane; const box = gtk.Box.new(.vertical, 4); - box.as(gtk.Widget).addCssClass("vtabs-dialog-pane"); + box.as(gtk.Widget).addCssClass("playpen-dialog-pane"); var title_buf: [64]u8 = undefined; const title = std.fmt.bufPrintZ(&title_buf, "Pane {d} — {s}", .{ @@ -223,7 +223,7 @@ fn buildPaneSection(self: *SaveLayoutDialog, index: usize, leaf: *Layouts.Node) }) catch "Pane"; const label = gtk.Label.new(title); label.setXalign(0); - label.as(gtk.Widget).addCssClass("vtabs-dialog-label"); + label.as(gtk.Widget).addCssClass("playpen-dialog-label"); box.append(label.as(gtk.Widget)); const grid = gtk.Grid.new(); @@ -256,7 +256,7 @@ fn field( ) *gtk.Entry { const label = gtk.Label.new(label_text); label.setXalign(0); - label.as(gtk.Widget).addCssClass("vtabs-dialog-sublabel"); + label.as(gtk.Widget).addCssClass("playpen-dialog-sublabel"); grid.attach(label.as(gtk.Widget), 0, row, 1, 1); const entry = gtk.Entry.new(); @@ -462,7 +462,7 @@ fn onDestroy(_: *gtk.Window, self: *SaveLayoutDialog) callconv(.c) void { fn heading(text: [:0]const u8) *gtk.Widget { const label = gtk.Label.new(text); label.setXalign(0); - label.as(gtk.Widget).addCssClass("vtabs-dialog-heading"); + label.as(gtk.Widget).addCssClass("playpen-dialog-heading"); return label.as(gtk.Widget); } @@ -470,7 +470,7 @@ fn hint(text: [:0]const u8) *gtk.Widget { const label = gtk.Label.new(text); label.setXalign(0); label.setWrap(1); - label.as(gtk.Widget).addCssClass("vtabs-dialog-hint"); + label.as(gtk.Widget).addCssClass("playpen-dialog-hint"); return label.as(gtk.Widget); } diff --git a/src/View.zig b/src/View.zig index 76494ed..2c8e470 100644 --- a/src/View.zig +++ b/src/View.zig @@ -93,7 +93,7 @@ pub fn create(alloc: std.mem.Allocator, cbs: Callbacks) !*View { .ctx = cbs.ctx, }; - self.box.as(gtk.Widget).addCssClass("vtabs-view"); + self.box.as(gtk.Widget).addCssClass("playpen-view"); self.box.as(gtk.Widget).setHexpand(1); self.box.as(gtk.Widget).setVexpand(1); diff --git a/src/Window.zig b/src/Window.zig index 835ca4d..bc43047 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -82,7 +82,7 @@ pub fn create(alloc: std.mem.Allocator, app: *adw.Application) !*Window { errdefer alloc.destroy(self); const window = adw.ApplicationWindow.new(app.as(gtk.Application)); - window.as(gtk.Window).setTitle("vtabs"); + window.as(gtk.Window).setTitle("Playpen"); window.as(gtk.Window).setDefaultSize(1100, 720); self.* = .{ @@ -96,11 +96,11 @@ pub fn create(alloc: std.mem.Allocator, app: *adw.Application) !*Window { self.layouts.load(); if (self.layouts.load_error) |message| std.log.warn("{s}", .{message}); - window.as(gtk.Widget).addCssClass("vtabs-window"); + window.as(gtk.Widget).addCssClass("playpen-window"); // ---- sidebar ------------------------------------------------------- const sidebar = gtk.Box.new(.vertical, 0); - sidebar.as(gtk.Widget).addCssClass("vtabs-sidebar"); + sidebar.as(gtk.Widget).addCssClass("playpen-sidebar"); sidebar.as(gtk.Widget).setSizeRequest(sidebar_width, -1); // The header bar lives inside the sidebar rather than spanning the @@ -128,14 +128,14 @@ pub fn create(alloc: std.mem.Allocator, app: *adw.Application) !*Window { layout_button.setIconName("view-grid-symbolic"); layout_button.as(gtk.Widget).setTooltipText("Open a saved layout"); layout_button.setPopover(self.layout_popover); - self.layout_popover.as(gtk.Widget).addCssClass("vtabs-layout-popover"); + self.layout_popover.as(gtk.Widget).addCssClass("playpen-layout-popover"); self.refreshLayoutMenu(); header.packEnd(layout_button.as(gtk.Widget)); sidebar.append(header.as(gtk.Widget)); self.list.setSelectionMode(.single); self.list.as(gtk.Widget).addCssClass("navigation-sidebar"); - self.list.as(gtk.Widget).addCssClass("vtabs-list"); + self.list.as(gtk.Widget).addCssClass("playpen-list"); _ = gtk.ListBox.signals.row_selected.connect( self.list, *Window, @@ -153,7 +153,7 @@ pub fn create(alloc: std.mem.Allocator, app: *adw.Application) !*Window { // ---- content ------------------------------------------------------- self.stack.as(gtk.Widget).setHexpand(1); self.stack.as(gtk.Widget).setVexpand(1); - self.stack.as(gtk.Widget).addCssClass("vtabs-content"); + self.stack.as(gtk.Widget).addCssClass("playpen-content"); const content = gtk.Box.new(.horizontal, 0); content.append(sidebar.as(gtk.Widget)); @@ -247,7 +247,7 @@ fn newTabEmpty(self: *Window) !*Tab { // ---- sidebar row --------------------------------------------------- const row_box = gtk.Box.new(.horizontal, 6); - row_box.as(gtk.Widget).addCssClass("vtabs-row"); + row_box.as(gtk.Widget).addCssClass("playpen-row"); row_box.append(tab.icon.as(gtk.Widget)); @@ -258,7 +258,7 @@ fn newTabEmpty(self: *Window) !*Tab { const close = gtk.Button.newFromIconName("window-close-symbolic"); close.as(gtk.Widget).addCssClass("flat"); - close.as(gtk.Widget).addCssClass("vtabs-close"); + close.as(gtk.Widget).addCssClass("playpen-close"); _ = gtk.Button.signals.clicked.connect(close, *Tab, &onCloseClicked, tab, .{}); row_box.append(close.as(gtk.Widget)); @@ -288,14 +288,14 @@ fn refreshLayoutMenu(self: *Window) void { self.freeLayoutRows(); const box = gtk.Box.new(.vertical, 2); - box.as(gtk.Widget).addCssClass("vtabs-layout-menu"); + box.as(gtk.Widget).addCssClass("playpen-layout-menu"); if (self.layouts.items.items.len == 0) { const empty = gtk.Label.new(if (self.layouts.load_error != null) "Layouts file could not be read" else "No saved layouts yet"); - empty.as(gtk.Widget).addCssClass("vtabs-layout-empty"); + empty.as(gtk.Widget).addCssClass("playpen-layout-empty"); box.append(empty.as(gtk.Widget)); } diff --git a/src/main.zig b/src/main.zig index 30e11df..8944a9d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,4 +1,5 @@ -//! vtabs: a terminal with vertical tabs, built on libghostty-vt. +//! Playpen: a terminal with vertical tabs, web panes and saved layouts, built +//! on libghostty-vt. //! //! libghostty-vt supplies the terminal emulator core (escape sequence //! parsing, screen and scrollback state, key/mouse encoding). Everything @@ -29,7 +30,7 @@ pub fn main() u8 { // behavior hands off to an already-running instance over D-Bus, which for // a terminal means a second launch silently does nothing visible here and // opens a window in whatever session owns the first one. - const app = adw.Application.new("dev.greyson.vtabs", .{ .non_unique = true }); + const app = adw.Application.new("dev.greyson.playpen", .{ .non_unique = true }); defer app.unref(); _ = gio.Application.signals.activate.connect(app, ?*anyopaque, &onActivate, null, .{}); diff --git a/src/style.css b/src/style.css index 31ad33e..c3c8159 100644 --- a/src/style.css +++ b/src/style.css @@ -1,27 +1,27 @@ /* Zen-style vertical tabs: a dark sidebar column with the terminal inset to its right. */ -.vtabs-window { +.playpen-window { background-color: #0f0d14; } -.vtabs-sidebar { +.playpen-sidebar { background-color: #1b1823; border-right: 1px solid #2a2536; } -.vtabs-sidebar headerbar { +.playpen-sidebar headerbar { background: none; box-shadow: none; min-height: 38px; } -.vtabs-list { +.playpen-list { background: none; padding: 4px 6px; } -.vtabs-list > row { +.playpen-list > row { border-radius: 8px; margin: 1px 0; padding: 5px 8px; @@ -29,126 +29,126 @@ transition: background-color 120ms ease; } -.vtabs-list > row:hover { +.playpen-list > row:hover { background-color: #262133; } -.vtabs-list > row:selected { +.playpen-list > row:selected { background-color: #342c4a; color: #f0ecf8; } -.vtabs-list > row:selected image { +.playpen-list > row:selected image { color: #b29df5; } /* Keep the close button unobtrusive until the row is hovered or current. */ -.vtabs-close { +.playpen-close { opacity: 0; min-width: 20px; min-height: 20px; padding: 0; } -.vtabs-list > row:hover .vtabs-close, -.vtabs-list > row:selected .vtabs-close { +.playpen-list > row:hover .playpen-close, +.playpen-list > row:selected .playpen-close { opacity: 0.65; } -.vtabs-close:hover { +.playpen-close:hover { opacity: 1; } -.vtabs-content { +.playpen-content { background-color: #0f0d14; } /* A view is the container for one tab's terminals. */ -.vtabs-view { +.playpen-view { padding: 6px 6px 6px 0; } /* Paned itself draws nothing; the panes inside it carry the styling. */ -.vtabs-view paned { +.playpen-view paned { background: none; } /* Each terminal sits in its own rounded frame so multiple panes in a view read as distinct surfaces. */ -.vtabs-pane { +.playpen-pane { background-color: #16141c; border-radius: 10px; border: 1px solid #2a2536; } -.vtabs-pane.active { +.playpen-pane.active { border-color: #5b4d80; } -.vtabs-pane-header { +.playpen-pane-header { padding: 2px 4px 2px 10px; background-color: #1c1926; border-bottom: 1px solid #262133; } -.vtabs-pane.active .vtabs-pane-header { +.playpen-pane.active .playpen-pane-header { background-color: #241f33; } -.vtabs-pane-title { +.playpen-pane-title { font-size: 0.82em; color: #8f87a3; } -.vtabs-pane.active .vtabs-pane-title { +.playpen-pane.active .playpen-pane-title { color: #ded7ef; } -.vtabs-pane-button { +.playpen-pane-button { min-width: 22px; min-height: 22px; padding: 0; opacity: 0; } -.vtabs-pane:hover .vtabs-pane-button, -.vtabs-pane.active .vtabs-pane-button { +.playpen-pane:hover .playpen-pane-button, +.playpen-pane.active .playpen-pane-button { opacity: 0.55; } -.vtabs-pane-button:hover { +.playpen-pane-button:hover { opacity: 1; } -.vtabs-pane-icon { +.playpen-pane-icon { color: #6f6784; -gtk-icon-size: 14px; } -.vtabs-pane.active .vtabs-pane-icon { +.playpen-pane.active .playpen-pane-icon { color: #b29df5; } /* A web pane's navigation bar, below the pane header. Kept visually quieter than the header so the two rows don't compete. */ -.vtabs-nav { +.playpen-nav { padding: 4px 6px; background-color: #16141c; border-bottom: 1px solid #262133; } -.vtabs-nav-button { +.playpen-nav-button { min-width: 24px; min-height: 24px; padding: 0; color: #b6afc7; } -.vtabs-nav-button:disabled { +.playpen-nav-button:disabled { opacity: 0.3; } -.vtabs-nav-entry { +.playpen-nav-entry { min-height: 24px; margin-left: 4px; padding: 2px 8px; @@ -160,85 +160,85 @@ box-shadow: none; } -.vtabs-nav-entry:focus-within { +.playpen-nav-entry:focus-within { border-color: #5b4d80; } /* The pane being dragged. There is no separate drop indicator: the layout rearranges live during the drag, so the view itself is the preview. */ -.vtabs-pane.dragging { +.playpen-pane.dragging { opacity: 0.65; border-color: #b29df5; } /* Saved-layout menu, hanging off the sidebar header. */ -.vtabs-layout-menu { +.playpen-layout-menu { min-width: 260px; } -.vtabs-layout-menu button { +.playpen-layout-menu button { padding: 4px 8px; border-radius: 6px; } -.vtabs-layout-empty { +.playpen-layout-empty { padding: 8px; color: #8f87a3; font-size: 0.9em; } /* Layout dialogs: opening one, and saving the current tab as one. */ -.vtabs-dialog { +.playpen-dialog { background-color: #16141c; } -.vtabs-dialog-content { +.playpen-dialog-content { padding: 16px; } -.vtabs-dialog-actions { +.playpen-dialog-actions { padding: 12px 16px; border-top: 1px solid #262133; } -.vtabs-dialog-heading { +.playpen-dialog-heading { margin-top: 8px; font-weight: bold; color: #ded7ef; } -.vtabs-dialog-label { +.playpen-dialog-label { color: #b6afc7; } -.vtabs-dialog-sublabel { +.playpen-dialog-sublabel { font-size: 0.88em; color: #8f87a3; } -.vtabs-dialog-hint { +.playpen-dialog-hint { font-size: 0.85em; color: #8f87a3; } -.vtabs-dialog-error { +.playpen-dialog-error { color: #f2a0a0; } /* Each captured pane in the save dialog, so the sections read apart. */ -.vtabs-dialog-pane { +.playpen-dialog-pane { padding: 8px; border: 1px solid #2a2536; border-radius: 8px; } /* Divider between panes. Wide enough to grab without hunting for it. */ -.vtabs-view paned > separator { +.playpen-view paned > separator { background-color: #0f0d14; min-width: 6px; min-height: 6px; } -.vtabs-view paned > separator:hover { +.playpen-view paned > separator:hover { background-color: #4a3f6b; }