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