diff options
Diffstat (limited to 'src/exec.zig')
| -rw-r--r-- | src/exec.zig | 103 |
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; |
