diff options
Diffstat (limited to 'src/builtins.zig')
| -rw-r--r-- | src/builtins.zig | 114 |
1 files changed, 87 insertions, 27 deletions
diff --git a/src/builtins.zig b/src/builtins.zig index be0956e0..23d853c8 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -44,6 +44,10 @@ pub const Ctx = struct { id: usize, txt: []const u8, arg: ?[]const u8, + /// Made by a gesture on the pane's body -- a middle click, the execute + /// key -- as acme's execute() asks of its Text (exec.c:122,169): an + /// Exec from a body may be a bound REPL's (exec.zig routeToRepl). + from_body: bool = false, }; fn isEnabled(comptime T: type) bool { @@ -166,7 +170,7 @@ pub const registry = struct { if (@intFromEnum(b) == i) return switch (setting.action) { // a switch flips bare, and DumpDir bare is the default .toggle, .transition, .scene, .dump_dir => false, - .shell, .theme, .font, .tagline_size, .window_opacity, .window_blur, .message_ms => true, + .shell, .theme, .font, .tagline_size, .window_opacity, .window_blur, .message_ms, .shader, .shader_animation => true, }; unreachable; } @@ -236,16 +240,41 @@ test "Exit asks once about unsaved text, and quits when asked again" { try std.testing.expect(p.quit); } +/// A dump file that exists, for a Restore to get past reading it. +fn testDump(buf: []u8) ![]const u8 { + var tmp = std.testing.tmpDir(.{}); + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "d.zon", .data = ".{}" }); + var dir: [4096]u8 = undefined; + const at = dir[0..try tmp.dir.realPath(std.testing.io, &dir)]; + return std.fmt.bufPrint(buf, "Restore {s}/d.zon", .{at}); +} + test "Restore asks about unsaved text as Exit does, and restores 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, "Restore /tmp/some.dump.zon")); + var line_buf: [4200]u8 = undefined; + const line = try testDump(&line_buf); + try std.testing.expect(p.executeBuiltinLine(p.active, line)); try std.testing.expect(p.restore_req == null); try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified (Restore again to discard)")); - try std.testing.expect(p.executeBuiltinLine(p.active, "Restore /tmp/some.dump.zon")); - try std.testing.expectEqualStrings("/tmp/some.dump.zon", p.restore_req.?); + try std.testing.expect(p.executeBuiltinLine(p.active, line)); + try std.testing.expectEqualStrings(line["Restore ".len..], p.restore_req.?); +} + +test "a Restore of a dump that cannot be read says so, before the unsaved-text warning" { + 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 + p.fs.no_prompt = true; + defer p.fs.no_prompt = false; + try std.testing.expect(p.executeBuiltinLine(p.active, "Restore /nope/missing.zon")); + try std.testing.expect(p.restore_req == null); + try std.testing.expectEqualStrings("Restore: /nope/missing.zon: no such file", p.fs.failure[0..p.fs.failure_len]); + // No warning spent: the Exit after it still asks. + try std.testing.expect(pane.discard_warned == null); } test "a Restore's warning is not an Exit's: each word is warned on its own" { @@ -253,7 +282,8 @@ test "a Restore's warning is not an Exit's: each word is warned on its own" { 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, "Restore /tmp/some.dump.zon")); + var line_buf: [4200]u8 = undefined; + try std.testing.expect(p.executeBuiltinLine(p.active, try testDump(&line_buf))); try std.testing.expect(p.restore_req == null); // The Restore warned; an Exit after it has not been, and asks. try std.testing.expect(p.executeBuiltinLine(p.active, "Exit")); @@ -289,8 +319,9 @@ test "Kill stops the commands pardes typed, bare all and by name those that matc } }; p.host = .{ .vtable = &killed.vtable }; - // Nothing running: nothing to stop, and never the shell. + // Nothing running: nothing to stop, and never the shell; said. try std.testing.expectEqual(@as(usize, 0), killed.count(p, "Kill")); + try std.testing.expectEqualStrings("Kill: nothing running", p.panes[term].?.msg[0..p.panes[term].?.msg_len]); _ = exec.execute(p, term, "make -j8"); while (p.nextEffect()) |_| {} // Typed but not started yet (no C mark): not running. @@ -371,7 +402,7 @@ pub const Exec = struct { pub fn run(c: Ctx) void { // the destination pane is Look's business (it focuses what answered); // an execute deliberately leaves you where you were - _ = exec.execute(c.p, c.id, c.arg orelse return); + _ = exec.executeFrom(c.p, c.id, c.arg orelse return, c.from_body); } }; @@ -458,7 +489,10 @@ pub const Kill = struct { } if (stopped) return; if (jobless) return c.p.reportFailure(c.id, "Kill: no job to signal (its shell runs it without job control)"); - if (names.len > 0) c.p.reportError(c.id, "Kill", error.NoSuchCommand); + if (names.len > 0) + c.p.reportFailure(c.id, "Kill: no running command has that first word") + else // said, but no failure: a script's Kill has nothing left to do + c.p.setMessage(c.id, "Kill: nothing running"); } }; @@ -473,8 +507,24 @@ pub const Restore = struct { pub const scope: Scope = .session; pub const takes_arg = true; pub fn run(c: Ctx) void { - const path = c.arg orelse (c.p.last_dump orelse return); - if (path.len > c.p.restore_buf.len) return; + const path = c.arg orelse (c.p.last_dump orelse return c.p.reportFailure(c.id, "Restore: no dump yet; name one")); + if (path.len > c.p.restore_buf.len) return c.p.reportFailure(c.id, "Restore: that name is too long"); + // A dump that cannot be read is said now, and fails a ctl write: the + // host reads it only after this step, where nobody hears. First, + // so a Restore that could never happen spends no warning. + if (comptime pardes.hosted) { + const bytes = @import("fs.zig").readRestore(c.p.gpa, path, c.p.settings.dump_dir.get()) catch |err| { + var buf: [limits.host_path_cap + 64]u8 = undefined; + const why = switch (err) { + error.FileNotFound => "no such file", + error.PermissionDenied => "permission denied", + error.IsDirectory => "a directory, not a dump", + else => @errorName(err), + }; + return c.p.reportFailure(c.id, std.fmt.bufPrint(&buf, "Restore: {s}: {s}", .{ path, why }) catch "Restore: cannot read the dump"); + }; + c.p.gpa.free(bytes); + } // acme's Load adds a dump's windows to the ones there; a Restore // replaces them all, so it asks what Exit asks first. if (warnModified(c, .Restore)) return; @@ -744,7 +794,8 @@ pub const Newcol = struct { }; /// Close the pane. `Del k` gives its rows to the open pane above it and -/// `Del j` to the one below, each falling back to the other side. +/// `Del j` to the one below, each falling back to the other side. Focus on +/// it goes with its rows. pub const Del = struct { pub const takes_arg = true; pub fn run(c: Ctx) void { @@ -757,7 +808,7 @@ pub const Del = struct { else if (std.mem.eql(u8, side, "j")) below orelse above else if (side.len > 0) - return c.p.reportError(c.id, "Del k|j", error.InvalidArgument) + return c.p.reportFailure(c.id, "Del: takes k, j or nothing") else null; // Someone at the keyboard with a real choice to make is asked, on the @@ -775,8 +826,6 @@ pub const Del = struct { return; } c.p.removePane(c.id, to) catch |err| return c.p.reportError(c.id, "close", err); - // Del k|j is a direction: focus lands where the rows went. - if (to) |gainer| c.p.active = gainer; } }; @@ -834,11 +883,18 @@ pub const Tty = struct { pub const takes_arg = true; pub const plus_arg = true; pub fn run(c: Ctx) void { - exec.spawnTty(c.p, c.id); - const arg = c.arg orelse return; - const pane = c.p.panes[c.p.active] orelse return; - if (!pane.isTerminal() or pane.shell != null) return; - pane.shell = c.p.gpa.dupe(u8, arg) catch return; + const arg = std.mem.trim(u8, c.arg orelse "", " \t"); + // A shell that is not there is said, where the host would quietly + // start its fallback in its place. + if (comptime pardes.hosted) if (arg.len > 0) { + var buf: [std.fs.max_path_bytes]u8 = undefined; + if (@import("host_io.zig").Shell.find(arg, &buf) == null) { + var said: [320]u8 = undefined; + return c.p.reportFailure(c.id, std.fmt.bufPrint(&said, "Tty: no shell \"{s}\" (a name on the usual paths, or a path to one)", .{arg[0..@min(arg.len, 200)]}) catch "Tty: no such shell"); + } + }; + const pane = exec.spawnTty(c.p, c.id) orelse return; + if (arg.len > 0) pane.shell = c.p.gpa.dupe(u8, arg) catch null; } }; @@ -851,7 +907,6 @@ pub const Tty9p = struct { }; /// Fold the active pane's column into the one on its right, keeping its panes. -/// The horizontal mirror of the vertical stacking `New` does. /// Binds this terminal as a language's REPL, by the language's name as the /// syntax table has it: an exec made on the body of a file in that /// language -- a middle click, the execute key, a single word like `ls` @@ -867,7 +922,10 @@ pub const Repl = struct { pub const takes_arg = true; pub fn run(c: Ctx) void { const pane = c.pane; - if (!pane.isTerminal()) return c.p.reportFailure(c.id, "Repl: only a terminal is a REPL"); + // A terminal with its emulator -- not a document pane -- and not + // a command pane whose command, the REPL it would be, is done. + if (!pane.isTerminal() or pane.terminal == null) return c.p.reportFailure(c.id, "Repl: only a terminal is a REPL"); + if (pane.command != null and pane.command_done) return c.p.reportFailure(c.id, "Repl: its command is done; a REPL is a running program"); const arg = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); var buf: [320]u8 = undefined; var idbuf: [48]u8 = undefined; @@ -879,14 +937,15 @@ pub const Repl = struct { pane.repl = null; return c.p.setMessage(c.id, "Repl: unbound"); } - const grammars = @import("grammar_manifest.zig").all; - const lang: u8 = for (grammars, 0..) |g, i| { - if (g.exts.len > 0 and std.mem.eql(u8, g.name, arg)) break @intCast(i); - } else { + const manifest = @import("grammar_manifest.zig"); + const grammars = manifest.all; + // Named as a code fence names it: `py`, `Python`, `sh`. + const named: ?u8 = if (manifest.byName(arg)) |i| if (grammars[i].exts.len > 0) @intCast(i) else null else null; + const lang: u8 = named orelse { var w = std.Io.Writer.fixed(&buf); - w.print("Repl: no language \"{s}\"; one of", .{arg[0..@min(arg.len, 32)]}) catch {}; + w.print("Repl: no language \"{s}\"; - or one of:", .{@import("Messages.zig").clip(arg, 24)}) catch {}; for (grammars) |g| if (g.exts.len > 0) w.print(" {s}", .{g.name}) catch break; - return c.p.reportFailure(c.id, w.buffered()); + return c.p.reportFailure(c.id, @import("Messages.zig").clip(w.buffered(), 256)); }; if (pane.repl) |r| if (r.lang == lang) return c.p.setMessage(c.id, std.fmt.bufPrint(&buf, "Repl: {s}", .{exec.replId(&idbuf, pane)}) catch "Repl"); @@ -907,6 +966,7 @@ pub const Repl = struct { } }; +/// The horizontal mirror of the vertical stacking `New` does. pub const Joincol = struct { pub const scope: Scope = .session; pub fn run(c: Ctx) void { |
