Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plugin.zig
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ fn drawExplorer(_: ?*anyopaque) anyerror!dvui.App.Result {

fn beginFrame(_: *anyopaque) void {
AcpClient.frame();
Explorer.frame(sdk.host());
}

fn deinit(_: *anyopaque) void {
Expand Down
10 changes: 5 additions & 5 deletions src/Chat.zig
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ pub fn fromBytes(path: []const u8, bytes: []const u8) !Chat {
};
errdefer self.deinitFields();
if (std.mem.trim(u8, bytes, " \t\r\n").len > 0) {
const dir = try self.imagesDir(gpa);
const dir = try imagesDir(gpa, self.path);
defer gpa.free(dir);
if (self.transcript.load(bytes, dir, dvui.io)) |h| {
defer gpa.free(h.client);
Expand Down Expand Up @@ -122,9 +122,9 @@ pub fn chatIdOf(path: []const u8) chat.ChatId {
return h.final() | 1;
}

/// Where its images are kept: beside the transcript, in a folder of its name.
pub fn imagesDir(self: *const Chat, gpa: std.mem.Allocator) ![]u8 {
const stem = if (std.mem.endsWith(u8, self.path, ".chat")) self.path[0 .. self.path.len - 5] else self.path;
/// Where the images of the transcript at `path` are kept: beside it, in a folder of its name.
pub fn imagesDir(gpa: std.mem.Allocator, path: []const u8) ![]u8 {
const stem = if (std.mem.endsWith(u8, path, ".chat")) path[0 .. path.len - 5] else path;
return std.fmt.allocPrint(gpa, "{s}.images", .{stem});
}

Expand All @@ -142,7 +142,7 @@ pub fn save(self: *Chat) !void {
pub fn write(self: *Chat) !void {
var out: std.Io.Writer.Allocating = .init(self.gpa);
defer out.deinit();
const dir = try self.imagesDir(self.gpa);
const dir = try imagesDir(self.gpa, self.path);
defer self.gpa.free(dir);
try self.transcript.save(&out.writer, .{ .client = self.client_id, .title = self.title.items, .folder = self.folder, .@"resume" = self.resume_state }, dir, dvui.io);
if (std.fs.path.dirname(self.path)) |d| std.Io.Dir.cwd().createDirPath(dvui.io, d) catch {};
Expand Down
6 changes: 6 additions & 0 deletions src/Commands.zig
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ pub fn register(host: *sdk.Host, owner: *sdk.Plugin) !void {
.{ "focus", api.Focus, cmdFocus },
.{ "set_settings", api.SetSettings, cmdSetSettings },
.{ "attach", api.Attach, cmdAttach },
.{ "delete", api.Delete, cmdDelete },
}) |entry| {
try host.registerCommand(.{
.id = "chat." ++ entry[0],
Expand Down Expand Up @@ -333,6 +334,11 @@ fn cmdAttach(a: api.Attach, call: *sdk.Command.Call) !void {
changed(c);
}

fn cmdDelete(a: api.Delete, call: *sdk.Command.Call) !void {
const path = if (a.path.len > 0) a.path else (front() orelse return call.fail("no chat is in front", .{})).path;
if (!Explorer.delete(sdk.host(), path)) return call.fail("no chat {s} is in the list", .{path});
}

fn cmdFocus(a: api.Focus, call: *sdk.Command.Call) !void {
const c = try find(a.chat, call);
_ = try sdk.host().openFile(.{ .path = c.path });
Expand Down
70 changes: 70 additions & 0 deletions src/Explorer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const sdk = @import("fizzy_sdk");
const dvui = @import("dvui");
const core = @import("core");
const Commands = @import("Commands.zig");
const Chat = @import("Chat.zig");
const api = @import("api.zig");

const Row = struct {
Expand All @@ -31,6 +32,10 @@ var rows: std.ArrayListUnmanaged(Row) = .empty;
/// Read the directory again before the next draw.
var stale = true;
var migrated = false;
/// A chat asked to be deleted (its row's menu, `chat.delete`), deleted as the next frame begins
/// (`frame`) rather than in the middle of this one, which may be drawing its tab. Owned by
/// `host.allocator`.
var doomed: ?[]u8 = null;

/// Something on disk changed (a chat saved, made or removed, a client came or went): list again.
pub fn changed() void {
Expand All @@ -46,6 +51,8 @@ pub fn deinit(gpa: std.mem.Allocator) void {
}
pluses.deinit(gpa);
open.deinit(gpa);
if (doomed) |p| gpa.free(p);
doomed = null;
}

fn clear(gpa: std.mem.Allocator) void {
Expand Down Expand Up @@ -192,6 +199,51 @@ fn readHead(gpa: std.mem.Allocator, d: std.Io.Dir, name: []const u8) Head {
return .{ .title = gpa.dupe(u8, title) catch &.{}, .folder = gpa.dupe(u8, folder) catch &.{} };
}

/// Delete the chat at `path` as the next frame begins: only one the list holds, so nothing else on
/// disk can be deleted this way (`chat.delete` takes any path). False when it is not in the list.
pub fn delete(host: *sdk.Host, path: []const u8) bool {
if (stale) {
stale = false;
scan(host);
}
for (rows.items) |r| {
if (!std.mem.eql(u8, r.path, path)) continue;
const p = host.allocator.dupe(u8, path) catch return false;
if (doomed) |old| host.allocator.free(old);
doomed = p;
host.refresh();
return true;
}
return false;
}

/// Delete the chat at `path` as the file tree deletes a file (the workbench's `files.zig`, its
/// row menu's Delete): at once and for good, with no question first. Its tab closes first, which
/// tells its client (an agent's session ends with it) and saves it a last time; a tab that does
/// not close (something is working on it, `sdk.Claim`) keeps its chat. Then its transcript and its
/// images go.
fn deleteChat(host: *sdk.Host, path: []const u8) void {
if (host.docFromPath(path)) |doc| {
host.closeDocById(doc.id) catch |err| return dvui.log.err("chat: could not close {s}: {t}", .{ path, err });
if (host.docFromPath(path) != null) return dvui.log.warn("chat: {s} is still open, so it stays", .{path});
}
const cwd = std.Io.Dir.cwd();
cwd.deleteFile(dvui.io, path) catch |err| return dvui.log.err("chat: could not delete {s}: {t}", .{ path, err });
if (Chat.imagesDir(host.allocator, path)) |images| {
defer host.allocator.free(images);
cwd.deleteTree(dvui.io, images) catch |err| dvui.log.warn("chat: could not delete {s}: {t}", .{ images, err });
} else |_| {}
changed();
}

/// At the start of a frame, before anything draws: a chat asked to be deleted goes now.
pub fn frame(host: *sdk.Host) void {
const path = doomed orelse return;
doomed = null;
defer host.allocator.free(path);
deleteChat(host, path);
}

pub fn draw(host: *sdk.Host) !dvui.App.Result {
if (stale) {
stale = false;
Expand Down Expand Up @@ -451,6 +503,24 @@ fn chatRow(host: *sdk.Host, tree: *core.widgets.TreeWidget, r: Row, selected: bo
const branch = tree.branch(@src(), .{ .can_accept_children = false, .draggable = false, .branch_id = id }, rowOptions(id, selected));
defer branch.deinit();
core.anchor.mark(branch.button.data(), "chat.chat:{s}", .{r.path});
// Its right-click menu, as a file row's is.
{
var context = core.widgets.context(@src(), .{ .rect = branch.button.data().borderRectScale().r }, .{ .id_extra = id });
defer context.deinit();
if (context.activePoint()) |point| {
var menu = core.widgets.contextMenu(@src(), point, .{});
defer menu.deinit();
if (core.widgets.menuRow(@src(), "Open", .{ .icon = dvui.entypo.chat }) != null) {
_ = host.openFile(.{ .path = r.path }) catch {};
menu.close();
}
_ = dvui.separator(@src(), .{ .expand = .horizontal });
if (core.widgets.menuRow(@src(), "Delete", .{ .icon = dvui.entypo.trash }) != null) {
_ = delete(host, r.path);
menu.close();
}
}
}
{
var slot = core.widgets.treeRowGlyph(@src(), .{});
slot.deinit();
Expand Down
3 changes: 3 additions & 0 deletions src/api.zig
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ pub const Attach = struct { chat: ChatId = 0, path: []const u8 };
/// The settings the chat offers by its message box, replacing any it had.
pub const SetSettings = struct { chat: ChatId, settings: []const Setting = &.{} };
pub const Focus = struct { chat: ChatId };
/// A chat in the list to delete, by its transcript's path (empty: the chat in front), as its
/// row's Delete does: its tab closes, and its transcript and images go for good.
pub const Delete = struct { path: []const u8 = "" };

/// To a client.
pub const Send = struct {
Expand Down
Loading