Rebrand as Playpen.

This commit is contained in:
Greyson Parrelli
2026-08-11 14:26:12 -04:00
parent 3026bdcd4c
commit 73a0187db5
17 changed files with 152 additions and 146 deletions
+3 -3
View File
@@ -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
View File
@@ -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;
}
// -------------------------------------------------------------------------
+3 -3
View File
@@ -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
View File
@@ -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();
+9 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,5 @@
//! vtabs: a terminal with vertical tabs, built on libghostty-vt.
//! Playpen: a terminal with vertical tabs, web panes and saved layouts, built
//! on libghostty-vt.
//!
//! libghostty-vt supplies the terminal emulator core (escape sequence
//! 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
View File
@@ -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;
}