summaryrefslogtreecommitdiff
path: root/src/exec.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/exec.zig')
-rw-r--r--src/exec.zig103
1 files changed, 89 insertions, 14 deletions
diff --git a/src/exec.zig b/src/exec.zig
index 2b0c90cb..1fc38b9c 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -37,6 +37,12 @@ pub fn takesCommandLine(p: *const Pardes, id: usize) bool {
const pane = p.panes[id] orelse return false;
// A command pane's child is its command, never a shell to type at.
if (!pane.isTerminal() or pane.command != null) return false;
+ // Someone's line at the shell's prompt, typed and not sent: typing a
+ // command there would run it joined to theirs (`echo TYPEDls`). Only an
+ // idle, EMPTY prompt is typed at; otherwise a new pane runs the line.
+ // (Text in pardes's own edit buffer is not the shell's yet: an exec of
+ // it, or of anything, types only what it runs.)
+ if (panes.Terminal.promptInputReady(pane) and !panes.Terminal.promptInputEmpty(pane)) return false;
// A mounted shell sits behind sudo's process supervisor. Its OSC 133
// prompt marks identify input readiness; the launcher's executable is
// not the shell executable used by the ordinary process heuristic.
@@ -301,17 +307,15 @@ pub fn saveTo(p: *Pardes, id: usize, path: []const u8) void {
if (pane.isTerminal()) askWrite(p, id, pane.serial, full);
return;
};
+ // A scratch takes the name once the write is done (promoteSaved): a
+ // Save that fails changes nothing, the name included.
if (f.output != null and panes.Output.fileTraits(f.output).saves) {
- const owned = p.gpa.dupe(u8, full) catch return;
- p.gpa.free(f.path);
- f.path = owned;
- f.output = null; // an ordinary file pane from here on
- f.watch_after_save = true;
- pane.clearCwd();
- // re-derive the tag as a plain file's
- if (pane.tag.own) |own| p.gpa.free(own);
- pane.tag.own = null;
- p.emit(.{ .save_file = .{ .pane = @intCast(id) } });
+ p.emit(.{ .save_text = .{
+ .pane = @intCast(id),
+ .serial = pane.serial,
+ .path = Pardes.SavePath.from(full),
+ .promote = true,
+ } });
return;
}
// its own path, spelled out: the in-place write, so the pane comes clean
@@ -343,11 +347,20 @@ pub fn spawnV9fsTty(p: *Pardes, from: usize) void {
fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane {
if (p.panes[from] == null) return null;
+ // A directory that is not there makes no terminal: its shell could not
+ // start in it, and a dead pane would answer no run. Asked only of a host
+ // that starts shells (none, as in the core's tests, starts none).
+ const dir = p.execDir(from);
+ if (missingDir(p, dir)) {
+ var said: [pardes.memory.limits.host_path_cap + 48]u8 = undefined;
+ p.reportFailure(from, std.fmt.bufPrint(&said, "Tty: {s}: no such directory", .{dir}) catch "Tty: no such directory");
+ return null;
+ }
const free = p.freeSlot() orelse {
p.reportError(from, "Tty", error.NoPaneSlots);
return null;
};
- const nt = p.newShell(free, p.execDir(from)) catch |err| {
+ const nt = p.newShell(free, dir) catch |err| {
if (err == error.PathTooLong) {
p.reportFailure(from, std.fmt.comptimePrint("Tty: its directory is longer than a path may be ({d} bytes)", .{pardes.memory.limits.host_path_cap}));
return null;
@@ -362,6 +375,26 @@ fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane {
return nt;
}
+/// Whether a shell would be started in `dir` and `dir` is not there: asked
+/// only of a host that starts shells, and only of a local path.
+pub fn missingDir(p: *Pardes, dir: []const u8) bool {
+ if (comptime !hosted) return false;
+ if (p.host.vtable.spawn == null) return false;
+ const local = @import("fs.zig").localPath(dir) orelse return false;
+ return !isDirectory(local);
+}
+
+/// Whether `path` is a directory on this host, the turn out while it asks
+/// (it may be inside a mount this editor serves).
+pub fn isDirectory(path: []const u8) bool {
+ var buf: [4096]u8 = undefined;
+ const z = std.fmt.bufPrintSentinel(&buf, "{s}", .{path}, 0) catch return false;
+ pardes.turn.yield();
+ defer pardes.turn.back();
+ const stat = std.Io.Dir.cwd().statFile(std.Io.Threaded.global_single_threaded.io(), z, .{}) catch return false;
+ return stat.kind == .directory;
+}
+
/// With `BootShell replace`, a doc landing in `col` closes a lone pristine
/// shell there, the boot's placeholder, and absorbVWeight hands its space to
/// the doc. With `keep`, the default, the shell stays like any other pane.
@@ -638,6 +671,13 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize {
}
const src = p.panes[from] orelse return null;
const dir = p.execDir(from);
+ // A directory that is not there runs nothing, as Tty makes nothing
+ // there: no pane, and the write fails ENOENT.
+ if (missingDir(p, dir)) {
+ var said: [pardes.memory.limits.host_path_cap + 48]u8 = undefined;
+ p.reportFailure(from, std.fmt.bufPrint(&said, "exec: {s}: no such directory", .{dir}) catch "exec: no such directory");
+ return null;
+ }
// Not one whose pty a job it left behind still prints to: reusing it
// would hang that job up.
// From a column's tag, only a finished pane in that column is reused,
@@ -771,11 +811,35 @@ pub fn applyStartupConfig(p: *Pardes) void {
if (executeBuiltinLine(p, p.active, line)) continue;
// A setting pardes no longer has is said, once, and changes nothing.
const word = commandText(line);
- if (std.mem.eql(u8, word[0 .. std.mem.indexOfAny(u8, word, " \t") orelse word.len], "ColumnTags"))
+ const head = word[0 .. std.mem.indexOfAny(u8, word, " \t") orelse word.len];
+ if (std.mem.eql(u8, head, "ColumnTags"))
p.setMessage(p.active, "ColumnTags: column tags are always shown now; the init line is ignored");
+ // A word renamed runs as its new name, and says so, once a line.
+ for ([_][2][]const u8{ .{ "ThemeSel", "Themes" }, .{ "FontSel", "Fonts" } }) |renamed| if (std.mem.eql(u8, head, renamed[0])) {
+ _ = executeBuiltinLine(p, p.active, renamed[1]);
+ var said: [64]u8 = undefined;
+ p.setMessage(p.active, std.fmt.bufPrint(&said, "{s}: renamed to {s}", .{ renamed[0], renamed[1] }) catch "renamed");
+ };
}
}
+test "an init file's ThemeSel runs as Themes and says it was renamed" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 });
+ defer p.deinit();
+ p.opts.startup_config = "ThemeSel\n";
+ applyStartupConfig(p);
+ var listed = false;
+ for (p.panes) |slot| if (slot) |pane| if (pane.file) |f| if (f.output) |o| {
+ listed = listed or std.meta.eql(o.from, panes.Output.Origin{ .cmd = .Themes });
+ };
+ try std.testing.expect(listed);
+ var said = false;
+ for (p.panes) |slot| if (slot) |pane| {
+ said = said or std.mem.eql(u8, pane.msg[0..pane.msg_len], "ThemeSel: renamed to Themes");
+ };
+ try std.testing.expect(said);
+}
+
test "an init file's retired ColumnTags line is said and ignored" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 });
defer p.deinit();
@@ -833,7 +897,7 @@ pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[]
if (!p.announce) return;
var near: [96]u8 = undefined;
var text: [200]u8 = undefined;
- return p.reportFailure(p.active, std.fmt.bufPrint(&text, "Theme: no theme \"{s}\"; like it: {s}; ThemeSel lists all", .{ want[0..@min(want.len, 40)], colors.themesNear(&near, want) }) catch "Theme: no such theme");
+ return p.reportFailure(p.active, std.fmt.bufPrint(&text, "Theme: no theme \"{s}\"; like it: {s}; Themes lists all", .{ want[0..@min(want.len, 40)], colors.themesNear(&near, want) }) catch "Theme: no such theme");
},
.font => {
if (comptime !font_picker) return;
@@ -937,9 +1001,12 @@ pub fn placeNew(p: *Pardes, from: usize, origin: ?usize, id: usize, kind: NewPan
layout.snapColWeights(p, f.col);
if (!layout.splitTallest(p, f.col, id) or !layout.placedRoomy(p, id, &before)) {
if (layout.findPane(p, id) == null) layout.insert(p, f.col, p.col_n[f.col], id);
+ // No pane to halve, but the column holds every pane's minimum
+ // with the new one's: the rows are shared out again, so what is
+ // refused is only what the documented arithmetic cannot hold.
// +Errors goes wherever there is room, last column first; with
// none, its maker says the text some other way (appendErrors).
- if (kind == .errors) {
+ if (layout.shareColumn(p, f.col)) {} else if (kind == .errors) {
layout.removePane(p, id);
var c = p.ncol;
while (c > 0) {
@@ -959,12 +1026,20 @@ pub fn placeNew(p: *Pardes, from: usize, origin: ?usize, id: usize, kind: NewPan
return;
}
} else {
+ // Refused, it closes at the step's end holding no weight, so
+ // its column's rows come back as they were, none rounded off.
+ p.panes[id].?.vweight = 0;
p.unplaced.set(id);
p.reportError(from, "placement", error.NoPaneRoom);
return;
}
}
};
+ // Placed: no pane of its column left under its minimum by the rows
+ // rounding or an earlier placement gave it; shared out again if so.
+ if (layout.findPane(p, id)) |f| if (!layout.columnAtMinimums(p, f.col)) {
+ _ = layout.shareColumn(p, f.col);
+ };
if (kind == .command) return;
tagline.exitHeader(p);
p.active = id;