summaryrefslogtreecommitdiff
path: root/src/builtins.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/builtins.zig')
-rw-r--r--src/builtins.zig99
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 {