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