diff options
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 { |
