forked from manaflow-ai/cmux
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathworkspace.zig
More file actions
244 lines (209 loc) · 7.98 KB
/
workspace.zig
File metadata and controls
244 lines (209 loc) · 7.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
/// Workspace: the core container for a "tab" worth of state.
///
/// Each workspace owns a split tree of terminal/browser panels,
/// plus metadata (title, color, directory, git branch, status).
/// Maps to macOS Sources/Workspace.swift.
const std = @import("std");
const split_tree = @import("split_tree.zig");
const c = @import("c_api.zig");
const Allocator = std.mem.Allocator;
pub const PanelType = enum {
terminal,
browser,
markdown,
};
pub const Panel = struct {
id: u128,
panel_type: PanelType,
title: ?[]const u8 = null,
custom_title: ?[]const u8 = null,
directory: ?[]const u8 = null,
url: ?[]const u8 = null,
is_pinned: bool = false,
is_manually_unread: bool = false,
git_branch: ?[]const u8 = null,
tty_name: ?[]const u8 = null,
surface: c.ghostty.ghostty_surface_t = null,
widget: ?*c.GtkWidget = null,
flash_count: u32 = 0,
/// Shell integration progress state (OSC 9;4).
progress_state: c.ghostty.ghostty_action_progress_report_state_e = c.ghostty.GHOSTTY_PROGRESS_STATE_REMOVE,
/// Progress percentage (0-100), or -1 if not reported.
progress_value: i8 = -1,
};
pub const StatusEntry = struct {
key: []const u8,
value: []const u8,
icon: ?[]const u8 = null,
color: ?[]const u8 = null,
timestamp: f64 = 0,
};
pub const GitBranch = struct {
branch: []const u8,
is_dirty: bool = false,
};
pub const Workspace = struct {
alloc: Allocator,
/// Stable identifier for this workspace (UUID-like, for socket API).
id: u128 = 0,
/// Split tree root node (owns the pane layout).
root_node: ?*split_tree.Node = null,
/// All panels in this workspace, keyed by panel ID.
panels: std.AutoHashMapUnmanaged(u128, *Panel),
/// Insertion-ordered panel IDs for deterministic surface indexing.
ordered_panels: std.ArrayListUnmanaged(u128) = .{},
/// Display metadata.
title: ?[]const u8 = null,
custom_title: ?[]const u8 = null,
custom_color: ?[]const u8 = null,
current_directory: ?[]const u8 = null,
description: ?[]const u8 = null,
is_pinned: bool = false,
is_manually_unread: bool = false,
/// Status.
git_branch: ?GitBranch = null,
status_entries: std.ArrayList(StatusEntry),
/// The GTK widget for this workspace's content (the split tree root widget).
content_widget: ?*c.GtkWidget = null,
/// Focused panel ID.
focused_panel_id: ?u128 = null,
pub fn init(alloc: Allocator) Workspace {
return .{
.alloc = alloc,
.panels = .{},
.status_entries = .{},
};
}
pub fn deinit(self: *Workspace) void {
var it = self.panels.valueIterator();
while (it.next()) |panel_ptr| {
const panel = panel_ptr.*;
if (panel.surface) |s| c.ghostty.ghostty_surface_free(s);
self.alloc.destroy(panel);
}
self.panels.deinit(self.alloc);
self.ordered_panels.deinit(self.alloc);
self.status_entries.deinit(self.alloc);
if (self.description) |d| self.alloc.free(d);
if (self.root_node) |node| split_tree.destroy(self.alloc, node);
}
/// Create a new terminal panel in this workspace.
pub fn createTerminalPanel(self: *Workspace, ghostty_app: c.ghostty_app_t) !*Panel {
const id = generateId();
const panel = try self.alloc.create(Panel);
panel.* = .{
.id = id,
.panel_type = .terminal,
};
// Create the surface widget
const widget = try @import("surface.zig").Surface.create(ghostty_app);
panel.widget = widget;
try self.panels.put(self.alloc, id, panel);
try self.ordered_panels.append(self.alloc, id);
self.focused_panel_id = id;
return panel;
}
/// Create a new browser panel in this workspace.
/// Returns error.WebKitNotAvailable when built without WebKitGTK (-Dno-webkit).
pub const createBrowserPanel = if (c.has_webkit) createBrowserPanelWebkit else createBrowserPanelStub;
fn createBrowserPanelStub(_: *Workspace, _: ?[]const u8) !*Panel {
return error.WebKitNotAvailable;
}
fn createBrowserPanelWebkit(self: *Workspace, url: ?[]const u8) !*Panel {
const id = generateId();
const panel = try self.alloc.create(Panel);
panel.* = .{
.id = id,
.panel_type = .browser,
.url = if (url) |u| self.alloc.dupe(u8, u) catch null else null,
};
// Create the WebKitGTK browser widget
const widget = try @import("browser.zig").BrowserView.create(url);
panel.widget = widget;
try self.panels.put(self.alloc, id, panel);
try self.ordered_panels.append(self.alloc, id);
self.focused_panel_id = id;
return panel;
}
/// Create a mock panel for test mode (no GL surface, no GTK widget).
/// Used when CMUX_NO_SURFACE is set to avoid GL and GTK thread-safety crashes.
pub fn createMockPanel(self: *Workspace, panel_type: PanelType) !*Panel {
const id = generateId();
const panel = try self.alloc.create(Panel);
panel.* = .{
.id = id,
.panel_type = panel_type,
};
try self.panels.put(self.alloc, id, panel);
try self.ordered_panels.append(self.alloc, id);
self.focused_panel_id = id;
return panel;
}
/// Remove a panel by ID from both maps.
pub fn removePanel(self: *Workspace, panel_id: u128) void {
if (self.panels.get(panel_id)) |panel| {
if (panel.surface) |s| c.ghostty.ghostty_surface_free(s);
self.alloc.destroy(panel);
_ = self.panels.remove(panel_id);
}
// Remove from ordered list
for (self.ordered_panels.items, 0..) |id, i| {
if (id == panel_id) {
_ = self.ordered_panels.orderedRemove(i);
break;
}
}
}
/// Detach a panel without destroying it — for transferring between workspaces.
/// Returns the panel pointer if found, null otherwise.
pub fn detachPanel(self: *Workspace, panel_id: u128) ?*Panel {
const panel = self.panels.get(panel_id) orelse return null;
_ = self.panels.remove(panel_id);
for (self.ordered_panels.items, 0..) |id, i| {
if (id == panel_id) {
_ = self.ordered_panels.orderedRemove(i);
break;
}
}
if (self.focused_panel_id) |fid| {
if (fid == panel_id) {
// Focus the next available panel
self.focused_panel_id = if (self.ordered_panels.items.len > 0)
self.ordered_panels.items[0]
else
null;
}
}
return panel;
}
/// Attach an existing panel to this workspace (for transfers from another workspace).
pub fn attachPanel(self: *Workspace, panel: *Panel) !void {
try self.panels.put(self.alloc, panel.id, panel);
try self.ordered_panels.append(self.alloc, panel.id);
self.focused_panel_id = panel.id;
}
/// Get the number of panels.
pub fn panelCount(self: *const Workspace) usize {
return self.panels.count();
}
/// Get the focused panel.
pub fn focusedPanel(self: *const Workspace) ?*Panel {
const id = self.focused_panel_id orelse return null;
return self.panels.get(id);
}
/// Set the display title (from terminal process or user override).
pub fn setTitle(self: *Workspace, title: []const u8) void {
if (self.title) |old| self.alloc.free(old);
self.title = self.alloc.dupe(u8, title) catch null;
}
/// Get the display title (custom > process > default).
pub fn displayTitle(self: *const Workspace) []const u8 {
return self.custom_title orelse self.title orelse "Terminal";
}
};
/// Generate a random 128-bit ID (UUID-like).
fn generateId() u128 {
var buf: [16]u8 = undefined;
std.crypto.random.bytes(&buf);
return std.mem.readInt(u128, &buf, .little);
}