Rebrand as Playpen.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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 |
@@ -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"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user