Rebrand as Playpen.

This commit is contained in:
Greyson Parrelli
2026-08-11 14:26:12 -04:00
parent 3026bdcd4c
commit 73a0187db5
17 changed files with 152 additions and 146 deletions
+9 -6
View File
@@ -1,10 +1,13 @@
# vtabs # Playpen
A proof-of-concept terminal emulator with **vertical tabs**, built on A proof-of-concept workspace built on
[libghostty-vt](https://github.com/ghostty-org/ghostty) and GTK4/libadwaita. [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 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. right.
## Quick start ## Quick start
@@ -23,7 +26,7 @@ mise run install # builds release, installs into ~/.local
mise run uninstall 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 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 theme, then refreshes the desktop and icon caches. `mise tasks` lists the rest
(`build`, `run`, `fmt`, `screenshot`). (`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 they were initializing. The shell's first output is the signal that it is
reading, so that is when the script goes in. 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 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 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 round-trips without a hand-written emitter is worth more here than a prettier
+1 -1
View File
@@ -21,7 +21,7 @@ pub fn build(b: *std.Build) void {
}); });
const exe = b.addExecutable(.{ const exe = b.addExecutable(.{
.name = "vtabs", .name = "playpen",
.root_module = b.createModule(.{ .root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"), .root_source_file = b.path("src/main.zig"),
.target = target, .target = target,
+4 -2
View File
@@ -1,7 +1,9 @@
.{ .{
.name = .vtabs, .name = .playpen,
.version = "0.1.0", .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", .minimum_zig_version = "0.16.0",
.paths = .{ .paths = .{
"build.zig", "build.zig",
@@ -1,16 +1,16 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=vtabs Name=Playpen
GenericName=Terminal 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 # Substituted with an absolute path by `mise run install`, because desktop
# launchers do not reliably have ~/.local/bin on PATH. # launchers do not reliably have ~/.local/bin on PATH.
Exec=@EXEC@ Exec=@EXEC@
Icon=dev.greyson.vtabs Icon=dev.greyson.playpen
Terminal=false Terminal=false
Categories=System;TerminalEmulator; Categories=System;TerminalEmulator;
Keywords=shell;prompt;command;commandline;cmd;terminal;tabs; Keywords=shell;prompt;command;commandline;cmd;terminal;tabs;
StartupNotify=true StartupNotify=true
# Lets the compositor match the running window to this entry, which is what # Lets the compositor match the running window to this entry, which is what
# gives the window its icon. Must equal the GApplication id. # gives the window its icon. Must equal the GApplication id.
StartupWMClass=dev.greyson.vtabs StartupWMClass=dev.greyson.playpen
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- <!--
vtabs app icon: a terminal pane with the vertical tab strip down its left Playpen app icon: a terminal pane with the vertical tab strip down its left
edge. Shapes are kept large and high-contrast so the sidebar is still edge. Shapes are kept large and high-contrast so the sidebar is still
readable when the icon is scaled down to 32px in a launcher. readable when the icon is scaled down to 32px in a launcher.
--> -->

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

+14 -14
View File
@@ -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 = { inputs = {
# Ghostty tracks nixpkgs-unstable to get GTK 4.20 / GNOME 49. We follow # Ghostty tracks nixpkgs-unstable to get GTK 4.20 / GNOME 49. We follow
@@ -42,7 +42,7 @@
in { in {
devShells = forAllSystems (pkgs: system: { devShells = forAllSystems (pkgs: system: {
default = pkgs.mkShell { default = pkgs.mkShell {
name = "vtabs"; name = "playpen";
nativeBuildInputs = [ nativeBuildInputs = [
zig.packages.${system}."0.16.0" zig.packages.${system}."0.16.0"
@@ -106,16 +106,16 @@
# an RPATH into the Nix store and runs fine outside this shell, but # 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 # it still has to be pointed at this data or GTK aborts on a missing
# schema and falls back to broken-image icons. # 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 PLAYPEN_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_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: # 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. # 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 GSETTINGS_SCHEMA_DIR="$PLAYPEN_GSETTINGS_SCHEMA_DIR"
export XDG_DATA_DIRS="$VTABS_RUNTIME_DATA_DIRS:$XDG_DATA_DIRS" export XDG_DATA_DIRS="$PLAYPEN_RUNTIME_DATA_DIRS:$XDG_DATA_DIRS"
export GIO_EXTRA_MODULES="$VTABS_GIO_MODULE_DIR''${GIO_EXTRA_MODULES:+:$GIO_EXTRA_MODULES}" 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 # 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 # 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. # Pointing all three at this mesa keeps the stack self-consistent.
# It still uses the real GPU through /dev/dri; this is not a # It still uses the real GPU through /dev/dri; this is not a
# software-rendering fallback. # software-rendering fallback.
export VTABS_EGL_VENDOR_DIR="${pkgs.mesa}/share/glvnd/egl_vendor.d" export PLAYPEN_EGL_VENDOR_DIR="${pkgs.mesa}/share/glvnd/egl_vendor.d"
export VTABS_DRI_DRIVERS_PATH="${pkgs.mesa}/lib/dri" export PLAYPEN_DRI_DRIVERS_PATH="${pkgs.mesa}/lib/dri"
export VTABS_GBM_BACKENDS_PATH="${pkgs.mesa}/lib/gbm" export PLAYPEN_GBM_BACKENDS_PATH="${pkgs.mesa}/lib/gbm"
export __EGL_VENDOR_LIBRARY_DIRS="$VTABS_EGL_VENDOR_DIR" export __EGL_VENDOR_LIBRARY_DIRS="$PLAYPEN_EGL_VENDOR_DIR"
export LIBGL_DRIVERS_PATH="$VTABS_DRI_DRIVERS_PATH" export LIBGL_DRIVERS_PATH="$PLAYPEN_DRI_DRIVERS_PATH"
export GBM_BACKENDS_PATH="$VTABS_GBM_BACKENDS_PATH" export GBM_BACKENDS_PATH="$PLAYPEN_GBM_BACKENDS_PATH"
''; '';
}; };
}); });
+26 -26
View File
@@ -10,7 +10,7 @@ description = "Build a debug binary (leak checking, safety checks)"
run = "nix develop --command zig build" run = "nix develop --command zig build"
[tasks.run] [tasks.run]
description = "Build and run vtabs" description = "Build and run playpen"
run = "nix develop --command zig build run" run = "nix develop --command zig build run"
[tasks.fmt] [tasks.fmt]
@@ -18,12 +18,12 @@ description = "Format all Zig source"
run = "nix develop --command zig fmt build.zig src" run = "nix develop --command zig fmt build.zig src"
[tasks.screenshot] [tasks.screenshot]
description = "Screenshot vtabs in a throwaway headless compositor" description = "Screenshot playpen in a throwaway headless compositor"
depends = ["build-debug"] depends = ["build-debug"]
run = "nix develop --command ./shot.sh" run = "nix develop --command ./shot.sh"
[tasks.install] [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"] depends = ["build"]
run = ''' run = '''
#!/usr/bin/env bash #!/usr/bin/env bash
@@ -31,10 +31,10 @@ set -euo pipefail
data="${XDG_DATA_HOME:-$HOME/.local/share}" data="${XDG_DATA_HOME:-$HOME/.local/share}"
bindir="$HOME/.local/bin" bindir="$HOME/.local/bin"
libexec="$HOME/.local/libexec/vtabs" libexec="$HOME/.local/libexec/playpen"
apps="$data/applications" apps="$data/applications"
icons="$data/icons/hicolor/scalable/apps" 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 # 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 # 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 # 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 # 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. # of the dev shell that built it rather than hardcoding store hashes.
schemas="$(nix develop --command printenv VTABS_GSETTINGS_SCHEMA_DIR)" schemas="$(nix develop --command printenv PLAYPEN_GSETTINGS_SCHEMA_DIR)"
datadirs="$(nix develop --command printenv VTABS_RUNTIME_DATA_DIRS)" datadirs="$(nix develop --command printenv PLAYPEN_RUNTIME_DATA_DIRS)"
giomodules="$(nix develop --command printenv VTABS_GIO_MODULE_DIR)" giomodules="$(nix develop --command printenv PLAYPEN_GIO_MODULE_DIR)"
eglvendor="$(nix develop --command printenv VTABS_EGL_VENDOR_DIR)" eglvendor="$(nix develop --command printenv PLAYPEN_EGL_VENDOR_DIR)"
dridrivers="$(nix develop --command printenv VTABS_DRI_DRIVERS_PATH)" dridrivers="$(nix develop --command printenv PLAYPEN_DRI_DRIVERS_PATH)"
gbmbackends="$(nix develop --command printenv VTABS_GBM_BACKENDS_PATH)" gbmbackends="$(nix develop --command printenv PLAYPEN_GBM_BACKENDS_PATH)"
mkdir -p "$bindir" "$libexec" "$apps" "$icons" "$gcroots" 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 # 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. # 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. # Existing values are preserved so this composes with the rest of the session.
cat > "$bindir/vtabs" <<EOF cat > "$bindir/playpen" <<EOF
#!/bin/sh #!/bin/sh
# Generated by \`mise run install\` — re-run that instead of editing. # Generated by \`mise run install\` — re-run that instead of editing.
export GSETTINGS_SCHEMA_DIR="$schemas\${GSETTINGS_SCHEMA_DIR:+:\$GSETTINGS_SCHEMA_DIR}" export GSETTINGS_SCHEMA_DIR="$schemas\${GSETTINGS_SCHEMA_DIR:+:\$GSETTINGS_SCHEMA_DIR}"
@@ -70,13 +70,13 @@ export __EGL_VENDOR_LIBRARY_DIRS="$eglvendor"
export LIBGL_DRIVERS_PATH="$dridrivers" export LIBGL_DRIVERS_PATH="$dridrivers"
export GBM_BACKENDS_PATH="$gbmbackends" export GBM_BACKENDS_PATH="$gbmbackends"
exec "$libexec/vtabs" "\$@" exec "$libexec/playpen" "\$@"
EOF EOF
chmod 755 "$bindir/vtabs" chmod 755 "$bindir/playpen"
sed "s|@EXEC@|$bindir/vtabs|" dist/dev.greyson.vtabs.desktop.in \ sed "s|@EXEC@|$bindir/playpen|" dist/dev.greyson.playpen.desktop.in \
> "$apps/dev.greyson.vtabs.desktop" > "$apps/dev.greyson.playpen.desktop"
install -m644 dist/dev.greyson.vtabs.svg "$icons/dev.greyson.vtabs.svg" install -m644 dist/dev.greyson.playpen.svg "$icons/dev.greyson.playpen.svg"
# Pin the store paths the installed binary links against. Without this a # Pin the store paths the installed binary links against. Without this a
# later `nix-collect-garbage` deletes GTK and the installed app stops # 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. # What the dynamic linker needs, read straight out of the binary.
if command -v readelf >/dev/null; then if command -v readelf >/dev/null; then
readelf -d "$libexec/vtabs" \ readelf -d "$libexec/playpen" \
| sed -n 's/.*R\(UN\)\?PATH.*\[\(.*\)\]/\2/p' \ | sed -n 's/.*R\(UN\)\?PATH.*\[\(.*\)\]/\2/p' \
| tr ':' '\n' | tr ':' '\n'
else 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 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 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 case ":$PATH:" in
*":$bindir:"*) ;; *":$bindir:"*) ;;
*) echo "note: $bindir is not on your PATH; the desktop entry still works" >&2 ;; *) 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}" data="${XDG_DATA_HOME:-$HOME/.local/share}"
apps="$data/applications" apps="$data/applications"
rm -f "$HOME/.local/bin/vtabs" rm -f "$HOME/.local/bin/playpen"
rm -rf "$HOME/.local/libexec/vtabs" rm -rf "$HOME/.local/libexec/playpen"
rm -f "$apps/dev.greyson.vtabs.desktop" rm -f "$apps/dev.greyson.playpen.desktop"
rm -f "$data/icons/hicolor/scalable/apps/dev.greyson.vtabs.svg" rm -f "$data/icons/hicolor/scalable/apps/dev.greyson.playpen.svg"
# Dropping the roots lets Nix reclaim the runtime closure on its next GC. # 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 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 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"
''' '''
+6 -6
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/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 # 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. # pops up on screen, and it works even while the session is locked.
@@ -8,7 +8,7 @@
set -u set -u
cd "$(dirname "$0")" cd "$(dirname "$0")"
OUT="${1:-/tmp/vtabs-shot.png}" OUT="${1:-/tmp/playpen-shot.png}"
KEYS="${2:-}" KEYS="${2:-}"
SETTLE="${3:-2}" SETTLE="${3:-2}"
@@ -23,14 +23,14 @@ CONF
# The nix dev shell exports bash functions into the environment. Any /bin/sh # 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 # 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' cat > "$RUNDIR/launch.sh" <<'LAUNCH'
#!/usr/bin/env bash #!/usr/bin/env bash
while read -r fn; do unset -f "$fn" 2>/dev/null; done < <(declare -Fx | awk '{print $3}') while read -r fn; do unset -f "$fn" 2>/dev/null; done < <(declare -Fx | awk '{print $3}')
exec "$VTABS_BIN" exec "$PLAYPEN_BIN"
LAUNCH LAUNCH
chmod +x "$RUNDIR/launch.sh" 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_BACKENDS=headless
export WLR_LIBINPUT_NO_DEVICES=1 export WLR_LIBINPUT_NO_DEVICES=1
@@ -96,7 +96,7 @@ if [ -n "$KEYS" ]; then
sleep 1.5 sleep 1.5
fi fi
if [ -n "${VTABS_SHOT_DEBUG:-}" ]; then if [ -n "${PLAYPEN_SHOT_DEBUG:-}" ]; then
echo "--- swaymsg get_tree (apps) ---" >&2 echo "--- swaymsg get_tree (apps) ---" >&2
swaymsg -t get_tree 2>&1 | grep -E '"(app_id|name|pid)"' | head -30 >&2 swaymsg -t get_tree 2>&1 | grep -E '"(app_id|name|pid)"' | head -30 >&2
echo "--- sway log ---" >&2 echo "--- sway log ---" >&2
+3 -3
View File
@@ -177,11 +177,11 @@ pub fn navigate(self: *Browser, input: []const u8) void {
fn buildNav(self: *Browser) *gtk.Widget { fn buildNav(self: *Browser) *gtk.Widget {
const nav = gtk.Box.new(.horizontal, 2); 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| { for ([_]*gtk.Button{ self.back, self.forward, self.reload }) |button| {
button.as(gtk.Widget).addCssClass("flat"); 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)); 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, .{}); _ = gtk.Button.signals.clicked.connect(self.reload, *Browser, &onReload, self, .{});
self.entry.setPlaceholderText("Enter address or search"); 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); self.entry.as(gtk.Widget).setHexpand(1);
_ = gtk.Entry.signals.activate.connect(self.entry, *Browser, &onEntryActivate, self, .{}); _ = gtk.Entry.signals.activate.connect(self.entry, *Browser, &onEntryActivate, self, .{});
nav.append(self.entry.as(gtk.Widget)); nav.append(self.entry.as(gtk.Widget));
+3 -3
View File
@@ -115,17 +115,17 @@ pub fn find(self: *Layouts, name: []const u8) ?*Layout {
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Paths // 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. /// when that isn't set — whichever GLib reports as the user's config dir.
pub fn configPath(buf: []u8) ?[:0]const u8 { pub fn configPath(buf: []u8) ?[:0]const u8 {
const dir = std.mem.span(glib.getUserConfigDir()); 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. /// The directory `configPath` lives in.
fn configDir(buf: []u8) ?[:0]const u8 { fn configDir(buf: []u8) ?[:0]const u8 {
const dir = std.mem.span(glib.getUserConfigDir()); 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;
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
+3 -3
View File
@@ -58,10 +58,10 @@ pub fn present(
self.window.setTransientFor(parent); self.window.setTransientFor(parent);
self.window.setModal(1); self.window.setModal(1);
self.window.setDefaultSize(480, -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); 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(); const fields = gtk.Grid.new();
fields.setRowSpacing(8); fields.setRowSpacing(8);
@@ -75,7 +75,7 @@ pub fn present(
const text = if (param.description.len > 0) param.description else param.name; const text = if (param.description.len > 0) param.description else param.name;
label.setText(std.fmt.bufPrintZ(&label_buf, "{s}", .{text}) catch "parameter"); label.setText(std.fmt.bufPrintZ(&label_buf, "{s}", .{text}) catch "parameter");
label.setXalign(0); 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); fields.attach(label.as(gtk.Widget), 0, @intCast(i), 1, 1);
const entry = gtk.Entry.new(); const entry = gtk.Entry.new();
+8 -8
View File
@@ -166,7 +166,7 @@ pub fn create(alloc: std.mem.Allocator, view: *View, spec: Spec) !*Pane {
_ = self.box.as(gobject.Object).refSink(); _ = self.box.as(gobject.Object).refSink();
const box_widget = self.box.as(gtk.Widget); const box_widget = self.box.as(gtk.Widget);
box_widget.addCssClass("vtabs-pane"); box_widget.addCssClass("playpen-pane");
box_widget.setHexpand(1); box_widget.setHexpand(1);
box_widget.setVexpand(1); box_widget.setVexpand(1);
// Clip the content to the pane's rounded corners. A terminal paints a // 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 { fn buildHeader(self: *Pane) void {
const header = self.header; 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. // The whole strip is the drag handle, so advertise that with the cursor.
header.as(gtk.Widget).setCursorFromName("grab"); header.as(gtk.Widget).setCursorFromName("grab");
// With two kinds of pane in a view, the title alone no longer says what // 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. // you're looking at, so the header leads with the kind.
const icon = gtk.Image.newFromIconName(self.kind.iconName()); 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)); header.append(icon.as(gtk.Widget));
self.label.setXalign(0); self.label.setXalign(0);
self.label.setEllipsize(.end); self.label.setEllipsize(.end);
self.label.as(gtk.Widget).setHexpand(1); 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)); header.append(self.label.as(gtk.Widget));
const split = gtk.Button.newFromIconName("list-add-symbolic"); const split = gtk.Button.newFromIconName("list-add-symbolic");
split.as(gtk.Widget).addCssClass("flat"); 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)"); split.as(gtk.Widget).setTooltipText("New terminal in this view (Ctrl+Shift+E)");
_ = gtk.Button.signals.clicked.connect(split, *Pane, &onSplitClicked, self, .{}); _ = gtk.Button.signals.clicked.connect(split, *Pane, &onSplitClicked, self, .{});
header.append(split.as(gtk.Widget)); header.append(split.as(gtk.Widget));
const web = gtk.Button.newFromIconName("web-browser-symbolic"); const web = gtk.Button.newFromIconName("web-browser-symbolic");
web.as(gtk.Widget).addCssClass("flat"); 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)"); web.as(gtk.Widget).setTooltipText("New web view in this view (Ctrl+Shift+B)");
_ = gtk.Button.signals.clicked.connect(web, *Pane, &onWebClicked, self, .{}); _ = gtk.Button.signals.clicked.connect(web, *Pane, &onWebClicked, self, .{});
header.append(web.as(gtk.Widget)); header.append(web.as(gtk.Widget));
const close = gtk.Button.newFromIconName("window-close-symbolic"); const close = gtk.Button.newFromIconName("window-close-symbolic");
close.as(gtk.Widget).addCssClass("flat"); 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)"); close.as(gtk.Widget).setTooltipText("Close pane (Ctrl+Shift+W)");
_ = gtk.Button.signals.clicked.connect(close, *Pane, &onCloseClicked, self, .{}); _ = gtk.Button.signals.clicked.connect(close, *Pane, &onCloseClicked, self, .{});
header.append(close.as(gtk.Widget)); 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 // pane being dragged is recorded on the view instead: drags never leave this
// process, and passing a raw pointer through a GValue buys nothing. // 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 { fn installDragSource(self: *Pane) void {
const source = gtk.DragSource.new(); const source = gtk.DragSource.new();
+9 -9
View File
@@ -118,10 +118,10 @@ pub fn present(
self.window.setTransientFor(parent); self.window.setTransientFor(parent);
self.window.setModal(1); self.window.setModal(1);
self.window.setDefaultSize(560, 620); 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); const content = gtk.Box.new(.vertical, 12);
content.as(gtk.Widget).addCssClass("vtabs-dialog-content"); content.as(gtk.Widget).addCssClass("playpen-dialog-content");
// ---- name ---------------------------------------------------------- // ---- name ----------------------------------------------------------
content.append(heading("Name")); content.append(heading("Name"));
@@ -169,7 +169,7 @@ pub fn present(
// ---- footer ---------------------------------------------------------- // ---- footer ----------------------------------------------------------
self.error_label.setXalign(0); 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); self.error_label.as(gtk.Widget).setVisible(0);
content.append(self.error_label.as(gtk.Widget)); content.append(self.error_label.as(gtk.Widget));
@@ -180,7 +180,7 @@ pub fn present(
const buttons = gtk.Box.new(.horizontal, 8); const buttons = gtk.Box.new(.horizontal, 8);
buttons.as(gtk.Widget).setHalign(.end); 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"); const cancel = gtk.Button.newWithLabel("Cancel");
_ = gtk.Button.signals.clicked.connect(cancel, *SaveLayoutDialog, &onCancel, self, .{}); _ = 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 spec = leaf.pane;
const box = gtk.Box.new(.vertical, 4); 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; var title_buf: [64]u8 = undefined;
const title = std.fmt.bufPrintZ(&title_buf, "Pane {d} — {s}", .{ 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"; }) catch "Pane";
const label = gtk.Label.new(title); const label = gtk.Label.new(title);
label.setXalign(0); 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)); box.append(label.as(gtk.Widget));
const grid = gtk.Grid.new(); const grid = gtk.Grid.new();
@@ -256,7 +256,7 @@ fn field(
) *gtk.Entry { ) *gtk.Entry {
const label = gtk.Label.new(label_text); const label = gtk.Label.new(label_text);
label.setXalign(0); 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); grid.attach(label.as(gtk.Widget), 0, row, 1, 1);
const entry = gtk.Entry.new(); 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 { fn heading(text: [:0]const u8) *gtk.Widget {
const label = gtk.Label.new(text); const label = gtk.Label.new(text);
label.setXalign(0); label.setXalign(0);
label.as(gtk.Widget).addCssClass("vtabs-dialog-heading"); label.as(gtk.Widget).addCssClass("playpen-dialog-heading");
return label.as(gtk.Widget); return label.as(gtk.Widget);
} }
@@ -470,7 +470,7 @@ fn hint(text: [:0]const u8) *gtk.Widget {
const label = gtk.Label.new(text); const label = gtk.Label.new(text);
label.setXalign(0); label.setXalign(0);
label.setWrap(1); label.setWrap(1);
label.as(gtk.Widget).addCssClass("vtabs-dialog-hint"); label.as(gtk.Widget).addCssClass("playpen-dialog-hint");
return label.as(gtk.Widget); return label.as(gtk.Widget);
} }
+1 -1
View File
@@ -93,7 +93,7 @@ pub fn create(alloc: std.mem.Allocator, cbs: Callbacks) !*View {
.ctx = cbs.ctx, .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).setHexpand(1);
self.box.as(gtk.Widget).setVexpand(1); self.box.as(gtk.Widget).setVexpand(1);
+10 -10
View File
@@ -82,7 +82,7 @@ pub fn create(alloc: std.mem.Allocator, app: *adw.Application) !*Window {
errdefer alloc.destroy(self); errdefer alloc.destroy(self);
const window = adw.ApplicationWindow.new(app.as(gtk.Application)); 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); window.as(gtk.Window).setDefaultSize(1100, 720);
self.* = .{ self.* = .{
@@ -96,11 +96,11 @@ pub fn create(alloc: std.mem.Allocator, app: *adw.Application) !*Window {
self.layouts.load(); self.layouts.load();
if (self.layouts.load_error) |message| std.log.warn("{s}", .{message}); 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 ------------------------------------------------------- // ---- sidebar -------------------------------------------------------
const sidebar = gtk.Box.new(.vertical, 0); 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); sidebar.as(gtk.Widget).setSizeRequest(sidebar_width, -1);
// The header bar lives inside the sidebar rather than spanning the // 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.setIconName("view-grid-symbolic");
layout_button.as(gtk.Widget).setTooltipText("Open a saved layout"); layout_button.as(gtk.Widget).setTooltipText("Open a saved layout");
layout_button.setPopover(self.layout_popover); 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(); self.refreshLayoutMenu();
header.packEnd(layout_button.as(gtk.Widget)); header.packEnd(layout_button.as(gtk.Widget));
sidebar.append(header.as(gtk.Widget)); sidebar.append(header.as(gtk.Widget));
self.list.setSelectionMode(.single); self.list.setSelectionMode(.single);
self.list.as(gtk.Widget).addCssClass("navigation-sidebar"); 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( _ = gtk.ListBox.signals.row_selected.connect(
self.list, self.list,
*Window, *Window,
@@ -153,7 +153,7 @@ pub fn create(alloc: std.mem.Allocator, app: *adw.Application) !*Window {
// ---- content ------------------------------------------------------- // ---- content -------------------------------------------------------
self.stack.as(gtk.Widget).setHexpand(1); self.stack.as(gtk.Widget).setHexpand(1);
self.stack.as(gtk.Widget).setVexpand(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); const content = gtk.Box.new(.horizontal, 0);
content.append(sidebar.as(gtk.Widget)); content.append(sidebar.as(gtk.Widget));
@@ -247,7 +247,7 @@ fn newTabEmpty(self: *Window) !*Tab {
// ---- sidebar row --------------------------------------------------- // ---- sidebar row ---------------------------------------------------
const row_box = gtk.Box.new(.horizontal, 6); 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)); 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"); const close = gtk.Button.newFromIconName("window-close-symbolic");
close.as(gtk.Widget).addCssClass("flat"); 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, .{}); _ = gtk.Button.signals.clicked.connect(close, *Tab, &onCloseClicked, tab, .{});
row_box.append(close.as(gtk.Widget)); row_box.append(close.as(gtk.Widget));
@@ -288,14 +288,14 @@ fn refreshLayoutMenu(self: *Window) void {
self.freeLayoutRows(); self.freeLayoutRows();
const box = gtk.Box.new(.vertical, 2); 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) { if (self.layouts.items.items.len == 0) {
const empty = gtk.Label.new(if (self.layouts.load_error != null) const empty = gtk.Label.new(if (self.layouts.load_error != null)
"Layouts file could not be read" "Layouts file could not be read"
else else
"No saved layouts yet"); "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)); box.append(empty.as(gtk.Widget));
} }
+3 -2
View File
@@ -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 //! libghostty-vt supplies the terminal emulator core (escape sequence
//! parsing, screen and scrollback state, key/mouse encoding). Everything //! 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 // 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 // a terminal means a second launch silently does nothing visible here and
// opens a window in whatever session owns the first one. // 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(); defer app.unref();
_ = gio.Application.signals.activate.connect(app, ?*anyopaque, &onActivate, null, .{}); _ = gio.Application.signals.activate.connect(app, ?*anyopaque, &onActivate, null, .{});
+47 -47
View File
@@ -1,27 +1,27 @@
/* Zen-style vertical tabs: a dark sidebar column with the terminal inset /* Zen-style vertical tabs: a dark sidebar column with the terminal inset
to its right. */ to its right. */
.vtabs-window { .playpen-window {
background-color: #0f0d14; background-color: #0f0d14;
} }
.vtabs-sidebar { .playpen-sidebar {
background-color: #1b1823; background-color: #1b1823;
border-right: 1px solid #2a2536; border-right: 1px solid #2a2536;
} }
.vtabs-sidebar headerbar { .playpen-sidebar headerbar {
background: none; background: none;
box-shadow: none; box-shadow: none;
min-height: 38px; min-height: 38px;
} }
.vtabs-list { .playpen-list {
background: none; background: none;
padding: 4px 6px; padding: 4px 6px;
} }
.vtabs-list > row { .playpen-list > row {
border-radius: 8px; border-radius: 8px;
margin: 1px 0; margin: 1px 0;
padding: 5px 8px; padding: 5px 8px;
@@ -29,126 +29,126 @@
transition: background-color 120ms ease; transition: background-color 120ms ease;
} }
.vtabs-list > row:hover { .playpen-list > row:hover {
background-color: #262133; background-color: #262133;
} }
.vtabs-list > row:selected { .playpen-list > row:selected {
background-color: #342c4a; background-color: #342c4a;
color: #f0ecf8; color: #f0ecf8;
} }
.vtabs-list > row:selected image { .playpen-list > row:selected image {
color: #b29df5; color: #b29df5;
} }
/* Keep the close button unobtrusive until the row is hovered or current. */ /* Keep the close button unobtrusive until the row is hovered or current. */
.vtabs-close { .playpen-close {
opacity: 0; opacity: 0;
min-width: 20px; min-width: 20px;
min-height: 20px; min-height: 20px;
padding: 0; padding: 0;
} }
.vtabs-list > row:hover .vtabs-close, .playpen-list > row:hover .playpen-close,
.vtabs-list > row:selected .vtabs-close { .playpen-list > row:selected .playpen-close {
opacity: 0.65; opacity: 0.65;
} }
.vtabs-close:hover { .playpen-close:hover {
opacity: 1; opacity: 1;
} }
.vtabs-content { .playpen-content {
background-color: #0f0d14; background-color: #0f0d14;
} }
/* A view is the container for one tab's terminals. */ /* A view is the container for one tab's terminals. */
.vtabs-view { .playpen-view {
padding: 6px 6px 6px 0; padding: 6px 6px 6px 0;
} }
/* Paned itself draws nothing; the panes inside it carry the styling. */ /* Paned itself draws nothing; the panes inside it carry the styling. */
.vtabs-view paned { .playpen-view paned {
background: none; background: none;
} }
/* Each terminal sits in its own rounded frame so multiple panes in a view /* Each terminal sits in its own rounded frame so multiple panes in a view
read as distinct surfaces. */ read as distinct surfaces. */
.vtabs-pane { .playpen-pane {
background-color: #16141c; background-color: #16141c;
border-radius: 10px; border-radius: 10px;
border: 1px solid #2a2536; border: 1px solid #2a2536;
} }
.vtabs-pane.active { .playpen-pane.active {
border-color: #5b4d80; border-color: #5b4d80;
} }
.vtabs-pane-header { .playpen-pane-header {
padding: 2px 4px 2px 10px; padding: 2px 4px 2px 10px;
background-color: #1c1926; background-color: #1c1926;
border-bottom: 1px solid #262133; border-bottom: 1px solid #262133;
} }
.vtabs-pane.active .vtabs-pane-header { .playpen-pane.active .playpen-pane-header {
background-color: #241f33; background-color: #241f33;
} }
.vtabs-pane-title { .playpen-pane-title {
font-size: 0.82em; font-size: 0.82em;
color: #8f87a3; color: #8f87a3;
} }
.vtabs-pane.active .vtabs-pane-title { .playpen-pane.active .playpen-pane-title {
color: #ded7ef; color: #ded7ef;
} }
.vtabs-pane-button { .playpen-pane-button {
min-width: 22px; min-width: 22px;
min-height: 22px; min-height: 22px;
padding: 0; padding: 0;
opacity: 0; opacity: 0;
} }
.vtabs-pane:hover .vtabs-pane-button, .playpen-pane:hover .playpen-pane-button,
.vtabs-pane.active .vtabs-pane-button { .playpen-pane.active .playpen-pane-button {
opacity: 0.55; opacity: 0.55;
} }
.vtabs-pane-button:hover { .playpen-pane-button:hover {
opacity: 1; opacity: 1;
} }
.vtabs-pane-icon { .playpen-pane-icon {
color: #6f6784; color: #6f6784;
-gtk-icon-size: 14px; -gtk-icon-size: 14px;
} }
.vtabs-pane.active .vtabs-pane-icon { .playpen-pane.active .playpen-pane-icon {
color: #b29df5; color: #b29df5;
} }
/* A web pane's navigation bar, below the pane header. Kept visually quieter /* A web pane's navigation bar, below the pane header. Kept visually quieter
than the header so the two rows don't compete. */ than the header so the two rows don't compete. */
.vtabs-nav { .playpen-nav {
padding: 4px 6px; padding: 4px 6px;
background-color: #16141c; background-color: #16141c;
border-bottom: 1px solid #262133; border-bottom: 1px solid #262133;
} }
.vtabs-nav-button { .playpen-nav-button {
min-width: 24px; min-width: 24px;
min-height: 24px; min-height: 24px;
padding: 0; padding: 0;
color: #b6afc7; color: #b6afc7;
} }
.vtabs-nav-button:disabled { .playpen-nav-button:disabled {
opacity: 0.3; opacity: 0.3;
} }
.vtabs-nav-entry { .playpen-nav-entry {
min-height: 24px; min-height: 24px;
margin-left: 4px; margin-left: 4px;
padding: 2px 8px; padding: 2px 8px;
@@ -160,85 +160,85 @@
box-shadow: none; box-shadow: none;
} }
.vtabs-nav-entry:focus-within { .playpen-nav-entry:focus-within {
border-color: #5b4d80; border-color: #5b4d80;
} }
/* The pane being dragged. There is no separate drop indicator: the layout /* The pane being dragged. There is no separate drop indicator: the layout
rearranges live during the drag, so the view itself is the preview. */ rearranges live during the drag, so the view itself is the preview. */
.vtabs-pane.dragging { .playpen-pane.dragging {
opacity: 0.65; opacity: 0.65;
border-color: #b29df5; border-color: #b29df5;
} }
/* Saved-layout menu, hanging off the sidebar header. */ /* Saved-layout menu, hanging off the sidebar header. */
.vtabs-layout-menu { .playpen-layout-menu {
min-width: 260px; min-width: 260px;
} }
.vtabs-layout-menu button { .playpen-layout-menu button {
padding: 4px 8px; padding: 4px 8px;
border-radius: 6px; border-radius: 6px;
} }
.vtabs-layout-empty { .playpen-layout-empty {
padding: 8px; padding: 8px;
color: #8f87a3; color: #8f87a3;
font-size: 0.9em; font-size: 0.9em;
} }
/* Layout dialogs: opening one, and saving the current tab as one. */ /* Layout dialogs: opening one, and saving the current tab as one. */
.vtabs-dialog { .playpen-dialog {
background-color: #16141c; background-color: #16141c;
} }
.vtabs-dialog-content { .playpen-dialog-content {
padding: 16px; padding: 16px;
} }
.vtabs-dialog-actions { .playpen-dialog-actions {
padding: 12px 16px; padding: 12px 16px;
border-top: 1px solid #262133; border-top: 1px solid #262133;
} }
.vtabs-dialog-heading { .playpen-dialog-heading {
margin-top: 8px; margin-top: 8px;
font-weight: bold; font-weight: bold;
color: #ded7ef; color: #ded7ef;
} }
.vtabs-dialog-label { .playpen-dialog-label {
color: #b6afc7; color: #b6afc7;
} }
.vtabs-dialog-sublabel { .playpen-dialog-sublabel {
font-size: 0.88em; font-size: 0.88em;
color: #8f87a3; color: #8f87a3;
} }
.vtabs-dialog-hint { .playpen-dialog-hint {
font-size: 0.85em; font-size: 0.85em;
color: #8f87a3; color: #8f87a3;
} }
.vtabs-dialog-error { .playpen-dialog-error {
color: #f2a0a0; color: #f2a0a0;
} }
/* Each captured pane in the save dialog, so the sections read apart. */ /* Each captured pane in the save dialog, so the sections read apart. */
.vtabs-dialog-pane { .playpen-dialog-pane {
padding: 8px; padding: 8px;
border: 1px solid #2a2536; border: 1px solid #2a2536;
border-radius: 8px; border-radius: 8px;
} }
/* Divider between panes. Wide enough to grab without hunting for it. */ /* Divider between panes. Wide enough to grab without hunting for it. */
.vtabs-view paned > separator { .playpen-view paned > separator {
background-color: #0f0d14; background-color: #0f0d14;
min-width: 6px; min-width: 6px;
min-height: 6px; min-height: 6px;
} }
.vtabs-view paned > separator:hover { .playpen-view paned > separator:hover {
background-color: #4a3f6b; background-color: #4a3f6b;
} }