diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 11:22:53 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 1c3842f136b9f98fae784559ec12d66d629c5909 (patch) | |
| tree | 9a53e64542e191b717052dbe5b5cd79cfd2c0754 /src/builtins.zig | |
| parent | 5cf50930dee214fb641bb95bbf8639b8f76930d9 (diff) | |
| download | pardes-1c3842f136b9f98fae784559ec12d66d629c5909.tar.gz pardes-1c3842f136b9f98fae784559ec12d66d629c5909.zip | |
Exit quits and Kill stops commands, as in acme
Kill quit the editor, which in acme is Exit; acme's Kill stops the
commands it started. Exit now quits as acme's does (exec.c, rowclean):
it refuses once, naming each pane with unsaved text, and quits when
asked again with nothing edited since (a small scratch is not asked
about). Kill, bare or with names, stops the commands pardes typed into a
terminal (an exec, a middle click, a pty/run) while their shell's marks
say they run, by SIGTERM to the terminal's foreground job, never to the
shell (acme posts the kill note, which terminates). Both are session
builtins; the topbar's Kill becomes Exit, same width, and every golden's
topbar row changed by exactly that word (checked line by line); the
builtins script scrolls one more row for the index's new line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/builtins.zig')
| -rw-r--r-- | src/builtins.zig | 99 |
1 files changed, 98 insertions, 1 deletions
diff --git a/src/builtins.zig b/src/builtins.zig index 2dac8a59..0154e707 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -213,6 +213,56 @@ test "a builtin's scope is the pane unless it says session, and every setting is try std.testing.expectEqual(Scope.session, registry.scope(std.meta.stringToEnum(Builtin, setting.word).?)); } +test "Exit asks once about unsaved text, and quits when asked again" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer p.deinit(); + const pane = try p.setTestFile("saved\n"); + pane.file.?.saved_revision = pane.file.?.revision -% 1; // modified + try std.testing.expect(p.executeBuiltinLine(p.active, "Exit")); + try std.testing.expect(!p.quit); + try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified")); + // Told once, and nothing edited since: the second Exit quits. + try std.testing.expect(p.executeBuiltinLine(p.active, "Exit")); + try std.testing.expect(p.quit); +} + +test "Kill stops the commands pardes typed, bare all and by name those that match, and never quits" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + const term = p.active; + try std.testing.expect(p.panes[term].?.isTerminal()); + const prompt = "\x1b]133;A;cl=line;aid=pardes\x07$ \x1b]133;B;aid=pardes\x07"; + p.update(.{ .output = .{ .pane = @intCast(term), .bytes = prompt } }); + while (p.nextEffect()) |_| {} + const killed = struct { + fn count(pp: *Pardes, line: []const u8) usize { + _ = pp.executeBuiltinLine(pp.active, line); + var n: usize = 0; + while (pp.nextEffect()) |e| if (e == .signal_pty and e.signal_pty.sig == .command) { + n += 1; + }; + return n; + } + }; + // Nothing running: nothing to stop, and never the shell. + try std.testing.expectEqual(@as(usize, 0), killed.count(p, "Kill")); + _ = exec.execute(p, term, "make -j8"); + while (p.nextEffect()) |_| {} + // Typed but not started yet (no C mark): not running. + try std.testing.expectEqual(@as(usize, 0), killed.count(p, "Kill")); + p.update(.{ .output = .{ .pane = @intCast(term), .bytes = "\x1b]133;C;aid=pardes\x07" } }); + while (p.nextEffect()) |_| {} + try std.testing.expectEqual(@as(usize, 0), killed.count(p, "Kill ls")); + try std.testing.expectEqual(@as(usize, 1), killed.count(p, "Kill ls make")); + try std.testing.expectEqual(@as(usize, 1), killed.count(p, "Kill")); + try std.testing.expect(!p.quit); + // Ended (a D mark): nothing to stop again. + p.update(.{ .output = .{ .pane = @intCast(term), .bytes = "\x1b]133;D;143;aid=pardes\x07" ++ prompt } }); + while (p.nextEffect()) |_| {} + try std.testing.expectEqual(@as(usize, 0), killed.count(p, "Kill")); +} + test "Look does not announce itself on the message row; Del still does" { const Builtin = registry.Builtin(); try std.testing.expect(!registry.announces(Builtin.Look)); @@ -274,14 +324,61 @@ pub const Exec = struct { // ---- session ---- -pub const Kill = struct { +/// acme's Exit (editors/acme/exec.c, `exit` and rowclean): quit, unless a +/// pane holds unsaved text nobody has been told about. Each such pane says +/// so, `<name>: Modified`, and the Exit goes no further; an Exit after that, +/// with nothing edited since, quits. A scratch of under 100 bytes is no +/// loss worth the question, as acme's winclean does not ask for one. +pub const Exit = struct { pub const scope: Scope = .session; pub fn run(c: Ctx) void { + var modified = false; + for (c.p.panes, 0..) |slot, id| { + const pane = slot orelse continue; + const f = pane.file orelse continue; + if (f.revision == f.saved_revision or !panes.Output.fileTraits(f.output).saves) continue; + if (f.output != null and f.content.len < 100) continue; + if (pane.exit_warned == f.revision) continue; + pane.exit_warned = f.revision; + c.p.reportError(id, f.path, error.Modified); + modified = true; + } + if (modified) return; c.p.quit = true; c.p.emit(.quit); } }; +/// acme's Kill (exec.c, `kill`; acme.c's WKill): stop the commands pardes +/// started -- the lines it typed into a terminal, from an exec, a middle +/// click or a pty/run, while their shell says they run (C mark to D). +/// Bare, every one; `Kill make ls`, those whose line starts with one of +/// the words. acme posts the "kill" note, which ends a process; here that +/// is SIGTERM to the terminal's foreground job, and never to the shell, +/// which is its own job and stays. +pub const Kill = struct { + pub const scope: Scope = .session; + pub const takes_arg = true; + pub fn run(c: Ctx) void { + const names = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); + var stopped = false; + for (c.p.panes, 0..) |slot, id| { + const pane = slot orelse continue; + if (!panes.Terminal.commandRunning(pane)) continue; + const sent = pane.sent_command.?; + if (names.len > 0) { + var words = std.mem.tokenizeAny(u8, names, " \t"); + while (words.next()) |w| { + if (std.mem.eql(u8, w, sent.word[0..sent.len])) break; + } else continue; + } + c.p.emit(.{ .signal_pty = .{ .pane = @intCast(id), .sig = .command } }); + stopped = true; + } + if (!stopped and names.len > 0) c.p.reportError(c.id, "Kill", error.NoSuchCommand); + } +}; + pub const Dump = struct { pub const scope: Scope = .session; pub fn run(c: Ctx) void { |
