Add a web browser tab.

This commit is contained in:
Greyson Parrelli
2026-08-11 12:39:14 -04:00
parent 918ccec6e1
commit a7a9429d09
12 changed files with 868 additions and 93 deletions
+38 -14
View File
@@ -14,6 +14,7 @@ const gobject = @import("gobject");
const gtk = @import("gtk");
const vt = @import("ghostty-vt");
const Terminal = @import("Terminal.zig");
const View = @import("View.zig");
const Window = @This();
@@ -42,13 +43,18 @@ updating: bool = false,
/// doesn't try to close a tab we're already destroying.
closing: bool = false,
/// A single tab: a view of one or more terminals, plus the sidebar row that
/// A single tab: a view of one or more panes, plus the sidebar row that
/// selects it.
const Tab = struct {
window: *Window,
view: *View,
row: *gtk.ListBoxRow,
label: *gtk.Label,
/// Shows what the tab's focused pane is, so a web view is recognisable in
/// the sidebar without reading the title.
icon: *gtk.Image,
name: [16]u8,
name_len: usize,
@@ -185,6 +191,7 @@ pub fn newTab(self: *Window) !void {
.view = view,
.row = gtk.ListBoxRow.new(),
.label = gtk.Label.new("shell"),
.icon = gtk.Image.newFromIconName("utilities-terminal-symbolic"),
.name = undefined,
.name_len = 0,
};
@@ -195,8 +202,7 @@ pub fn newTab(self: *Window) !void {
const row_box = gtk.Box.new(.horizontal, 6);
row_box.as(gtk.Widget).addCssClass("vtabs-row");
const icon = gtk.Image.newFromIconName("utilities-terminal-symbolic");
row_box.append(icon.as(gtk.Widget));
row_box.append(tab.icon.as(gtk.Widget));
tab.label.setXalign(0);
tab.label.setEllipsize(.end);
@@ -217,8 +223,8 @@ pub fn newTab(self: *Window) !void {
try self.tabs.append(self.alloc, tab);
// Only now is `tab` complete enough for the view's callbacks to use, so
// this is the first safe moment to give the view its terminal.
try view.addPane();
// this is the first safe moment to give the view its first pane.
try view.addPane(.terminal);
self.refreshLabel(tab);
self.select(tab);
@@ -301,6 +307,7 @@ fn refreshLabel(self: *Window, tab: *Tab) void {
tab.label.setText(buf[0..text.len :0]);
tab.label.as(gtk.Widget).setTooltipText(buf[0..text.len :0]);
tab.icon.setFromIconName(tab.view.iconName());
}
fn onViewTitle(ctx: ?*anyopaque) void {
@@ -342,6 +349,20 @@ fn activeTab(self: *Window) ?*Tab {
return null;
}
/// The focused terminal of the visible tab, or null when a web pane has focus.
fn focusedTerminal(self: *Window) ?*Terminal {
const tab = self.activeTab() orelse return null;
return tab.view.focusedTerminal();
}
/// Split the visible tab's focused pane, adding a pane of the given kind.
fn addPane(self: *Window, kind: View.Kind) void {
const tab = self.activeTab() orelse return;
tab.view.addPane(kind) catch |err| {
std.log.err("failed to open {s} pane: {s}", .{ @tagName(kind), @errorName(err) });
};
}
fn selectIndex(self: *Window, index: usize) void {
if (index >= self.tabs.items.len) return;
self.select(self.tabs.items[index]);
@@ -376,22 +397,26 @@ fn onShortcut(
return 1;
},
gdk.KEY_W, gdk.KEY_w => {
// Closes the focused terminal. The view raises on_empty when
// its last pane goes, which is what closes the tab.
// Closes the focused pane. The view raises on_empty when its
// last pane goes, which is what closes the tab.
if (self.activeTab()) |tab| {
if (tab.view.focusedPane()) |pane| tab.view.closePane(pane);
}
return 1;
},
gdk.KEY_E, gdk.KEY_e => {
if (self.activeTab()) |tab| {
tab.view.addPane() catch |err| {
std.log.err("failed to open terminal: {s}", .{@errorName(err)});
};
}
self.addPane(.terminal);
return 1;
},
gdk.KEY_B, gdk.KEY_b => {
self.addPane(.web);
return 1;
},
gdk.KEY_V, gdk.KEY_v => {
// Only a terminal needs us to encode a paste for it. A web
// pane has its own clipboard handling, so the key is left
// alone rather than swallowed here.
if (self.focusedTerminal() == null) return 0;
self.paste();
return 1;
},
@@ -478,8 +503,7 @@ fn onPasteReady(
};
defer glib.free(text);
const tab = self.activeTab() orelse return;
const terminal = tab.view.focusedTerminal() orelse return;
const terminal = self.focusedTerminal() orelse return;
const session = terminal.session;
// Coerce to a plain slice: encodePaste dispatches on the exact type.
const span: []const u8 = std.mem.span(text);