summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig268
1 files changed, 245 insertions, 23 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index d6385e6f..0950d13a 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -14,6 +14,7 @@ const tracy = @import("tracy.zig");
pub const panes = @import("panes.zig");
pub const tagline = @import("tagline.zig");
pub const exec = @import("exec.zig");
+const recent_files = @import("recent.zig");
pub const mouse = @import("mouse.zig");
pub const normal = @import("normal.zig");
pub const edit = @import("edit.zig");
@@ -447,11 +448,17 @@ test "a compact tagline anchors at its own pane, and both shells step from the s
try std.testing.expectEqual(@as(u16, 3), gridColAt(p, 3.5 * body_w, body_row, body_w, tag_w));
}
+const recent_path_cap = limits.host_path_cap;
+
pub const Loc = struct {
pane: u16,
+ /// 0 once its pane has closed: a file's entry stays, reopened by a
+ /// jump to it from `path_hash`'s recent entry.
serial: u32,
line: u32,
col: u32,
+ /// The file it is in (Wyhash of the path), 0 for a pane with none.
+ path_hash: u64 = 0,
};
test {
@@ -474,6 +481,7 @@ test {
_ = @import("draw.zig");
_ = @import("tag_layer.zig");
_ = @import("dump.zig"); // its tests ran nowhere before
+ _ = @import("recent.zig");
_ = @import("Presentation.zig");
_ = @import("animation.zig");
}
@@ -2321,6 +2329,9 @@ test "owned cwd Save promotion releases the former directory" {
const pane = p.panes[id].?;
try pane.setOwnedCwd("/old/directory");
exec.saveTo(p, id, "saved.txt");
+ // Named once the write is done (a failed one changes nothing).
+ try std.testing.expect(pane.file.?.output != null);
+ while (p.nextEffect()) |effect| p.perform(effect);
try std.testing.expect(pane.cwd == .none);
try std.testing.expect(pane.file.?.output == null);
try std.testing.expectEqualStrings("/old/directory/saved.txt", pane.file.?.path);
@@ -2348,16 +2359,16 @@ test "Save on a scratch asks for a path in its inherited dir and makes it a file
// typing the filename and submitting converts it into an ordinary file
edit.insertKey(p, &np.input, .{ .cp = 'n', .text = "note.txt" });
exec.submitSave(p, id);
- try std.testing.expect(np.file.?.output == null);
- try std.testing.expectEqualStrings("/tmp/pardes-save-dir/note.txt", np.file.?.path);
+ // The write asked for, the pane is the file once it is done.
var saved = false;
- while (p.nextEffect()) |effect| switch (effect) {
- .save_file => |sf| if (@as(usize, sf.pane) == id) {
- saved = true;
- },
- else => {},
- };
+ while (p.nextEffect()) |effect| {
+ if (effect == .save_text and @as(usize, effect.save_text.pane) == id) saved = true;
+ p.perform(effect);
+ }
try std.testing.expect(saved);
+ try std.testing.expect(np.file.?.output == null);
+ try std.testing.expectEqualStrings("/tmp/pardes-save-dir/note.txt", np.file.?.path);
+ try std.testing.expectEqual(np.file.?.revision, np.file.?.saved_revision);
}
test "Save on a terminal writes its plaintext scrollback and stays a terminal" {
@@ -3640,7 +3651,7 @@ pub const Effect = union(enum) {
/// write this pane's file content to its path; the shell reads both off
/// the core (content is unbounded, effects are fixed-size values)
save_file: struct { pane: u8 },
- save_text: struct { pane: u8, serial: u32, path: Buf(effect_path_cap) },
+ save_text: struct { pane: u8, serial: u32, path: Buf(effect_path_cap), promote: bool = false },
write_dump,
set_clipboard,
read_clipboard,
@@ -4229,6 +4240,10 @@ pub const Options = struct {
ninep_quic: ?[]const u8 = null,
mounts: []const filesystem.Mount = &.{},
config_dir: ?[]const u8 = null,
+ /// Where the list of files opened lately is kept across sessions
+ /// (recent.zig), set by the host that has a state directory; none, it
+ /// lives as long as the session.
+ recent_path: ?[]const u8 = null,
startup_config_path: ?[]const u8 = null,
image_allocator: ?std.mem.Allocator = null,
pdf_allocator: ?std.mem.Allocator = null,
@@ -4295,6 +4310,12 @@ pub const Pardes = struct {
presentation: Presentation = .{},
active: usize = 0,
+ /// The files opened lately, closed ones too (recent.zig): `Recent`,
+ /// /recent, and the jumplist's way back to a closed pane.
+ recent: recent_files.List = .{},
+ /// The kept list was read (or there was none): only then is it written,
+ /// so a session never writes over one it did not read.
+ recent_loaded: bool = false,
jumps: [MAX_JUMPS]Loc = undefined,
njumps: usize = 0,
jcur: usize = 0,
@@ -4347,7 +4368,9 @@ pub const Pardes = struct {
/// scroll, G6): the core then keeps the continuous path running, so the
/// shell draws every loop and a virtual clock moves on, as for a lift.
shell_continuous: bool = false,
- cursor_view: struct { serial: u32 = 0, line: i64 = 0 } = .{},
+ cursor_view: struct { serial: u32 = 0, line: i64 = 0, origin: [2]f32 = .{ 0, 0 }, inside: bool = true } = .{},
+ /// The cursor glides from one pane to another: it may cross the grid.
+ cursor_crossing: bool = false,
input_ns: u64 = 0,
animate_theme_changes: bool = false,
native_images: bool = false,
@@ -4621,6 +4644,7 @@ pub const Pardes = struct {
},
}
p.sync();
+ p.loadRecent();
exec.applyStartupConfig(p);
colors.finishThemeInitialization(p);
p.sync();
@@ -4641,7 +4665,58 @@ pub const Pardes = struct {
p.col_panes[0][0] = 0;
}
+ /// The recent list's own allocator: it is kept best effort (a file not
+ /// remembered for want of memory is no failure of what opened it), so
+ /// on a host it takes from the process heap, not the editor's, whose
+ /// every failure must be answered.
+ // ponytail: a host heap for a best-effort list; the board's gpa there.
+ pub fn recentGpa(p: *const Pardes) std.mem.Allocator {
+ return if (comptime hosted) std.heap.smp_allocator else p.gpa;
+ }
+
+ /// The kept list of files opened lately, read once at start.
+ fn loadRecent(p: *Pardes) void {
+ const path = p.opts.recent_path orelse return;
+ if (comptime !hosted) return;
+ defer p.recent_loaded = true;
+ const bytes = filesystem.readFile(p.gpa, path) catch return;
+ defer p.gpa.free(bytes);
+ p.recent.read(p.recentGpa(), bytes);
+ }
+
+ /// Written out when it changed, at the end of a step and at the end.
+ pub fn saveRecent(p: *Pardes) void {
+ if (!p.recent.dirty) return;
+ p.recent.dirty = false;
+ const path = p.opts.recent_path orelse return;
+ if (comptime !hosted) return;
+ if (!p.recent_loaded) return;
+ var out: std.Io.Writer.Allocating = .init(p.gpa);
+ defer out.deinit();
+ p.recent.write(&out.writer) catch return;
+ turn.yield();
+ defer turn.back();
+ std.Io.Dir.cwd().writeFile(std.Io.Threaded.global_single_threaded.io(), .{ .sub_path = path, .data = out.written() }) catch {};
+ }
+
+ /// A file pane's path and dot, for the recent list (0 when none).
+ fn recentDot(pane: *const Pane) ?struct { path: []const u8, line: u32, col: u32 } {
+ const f = pane.file orelse return null;
+ if (f.output != null) return null;
+ return .{ .path = f.path, .line = @intCast(@max(0, pane.body.cur_row) + 1), .col = @intCast(@max(0, pane.body.cur_col) + 1) };
+ }
+
+ /// The recent entry a closed jump names, by its path's hash.
+ pub fn recentByHash(p: *const Pardes, hash: u64) ?recent_files.Entry {
+ for (p.recent.entries.items) |e| if (std.hash.Wyhash.hash(0, e.path) == hash) return e;
+ return null;
+ }
+
pub fn deinit(p: *Pardes) void {
+ // Every file open now is closed with its dot, and the list kept.
+ for (p.panes) |slot| if (slot) |pane| if (recentDot(pane)) |d| p.recent.closed(p.recentGpa(), d.path, d.line, d.col);
+ p.saveRecent();
+ p.recent.deinit(p.recentGpa());
look.cancelLookHover(p);
if (p.repl_ask) |ask| p.gpa.free(ask.text);
for (0..MAX_PANES) |id| p.dropPendingWrite(id);
@@ -4750,6 +4825,9 @@ pub const Pardes = struct {
}
fn retirePane(p: *Pardes, pane: *Pane) void {
+ // Closed, a file is remembered with its dot: Recent and a jump back
+ // to it reopen it there.
+ if (recentDot(pane)) |d| p.recent.closed(p.recentGpa(), d.path, d.line, d.col);
const watched = (if (pane.file) |f| f.output == null else false) or pane.hasPdf();
for (p.panes, 0..) |slot, id| if (slot == pane) {
if (watched) p.emit(.{ .watch = .{ .pane = @intCast(id), .on = false } });
@@ -4813,6 +4891,10 @@ pub const Pardes = struct {
std.debug.assert(p.panes[id] == null);
if (cwd.len > limits.host_path_cap) return error.PathTooLong;
const pane = try panes.Terminal.create(p.gpa, p.screen_w, p.screen_h);
+ // Named by the directory it is started in from the first, so the
+ // shell saying it is there is no rename (`new N /` then `rename`);
+ // out of memory for it, the shell names it when it says.
+ if (cwd.len > 0) pane.setOwnedCwd(cwd) catch {};
panes.Terminal.armShellSpawn(pane);
p.installPane(id, pane);
p.emitSpawn(id, pane.serial, cwd);
@@ -4822,6 +4904,9 @@ pub const Pardes = struct {
/// A child for pane `id` in `cwd`: a directory up to PATH_MAX, the ones
/// past the effect's inline buffer kept here for perform to give.
pub fn emitSpawn(p: *Pardes, id: usize, serial: u32, cwd: []const u8) void {
+ // A new shell has not said anything yet (setCwd's guard), whatever
+ // the one it replaces said.
+ if (p.panes[id]) |pane| pane.shell_spoke = false;
if (p.spawn_dirs[id]) |old| p.gpa.free(old);
p.spawn_dirs[id] = null;
if (cwd.len > effect_path_cap) p.spawn_dirs[id] = p.gpa.dupe(u8, cwd) catch null;
@@ -4995,6 +5080,11 @@ pub const Pardes = struct {
else => "",
};
if (std.mem.eql(u8, cur, cwd)) return;
+ // A shell started in a directory is asked where it is before it has
+ // said anything: its chdir may not have happened yet, and the answer
+ // is this process's directory, a rename to undo. That answer, then,
+ // is not taken.
+ if (pane.isTerminal() and !pane.shell_spoke and pane.cwd == .owned and std.mem.eql(u8, cwd, p.sessionDir())) return;
pane.setOwnedCwd(cwd) catch |err| return p.reportError(id, "directory", err);
if (look.lookHoverPane(p) == id) look.cancelLookHover(p);
// A terminal is named by its directory, so /log hears it renamed; one
@@ -5010,6 +5100,8 @@ pub const Pardes = struct {
pub fn acknowledgeShell(p: *Pardes, id: usize, executable: []const u8, prompt_marks: bool) void {
if (id < MAX_PANES) if (p.panes[id]) |pane| {
ctlfs.pty.shellGone(p, pane, true); // a respawn replaced whatever ran
+ pane.shell_failed = false;
+ pane.had_shell = true;
pane.fs.unmarked = !prompt_marks;
// The tag's Tty word names the shell the host ran, and a click
// on it (`Tty+fish`) opens another of the same.
@@ -5210,13 +5302,73 @@ pub const Pardes = struct {
/// A write of `path` the host could not do: the pane stays dirty, the
/// message row says `Save <path>: <why>`, and the 9P write that asked
/// for it, waiting on the save, fails with that (`fs.late_failure`).
+ /// A scratch written to `path` is that file from here on: named, an
+ /// ordinary file pane, clean, and watched (exec.saveTo).
+ fn promoteSaved(p: *Pardes, id: u8, path: []const u8) void {
+ const pane = p.panes[id] orelse return;
+ const f = if (pane.file) |*file| file else return;
+ const owned = p.gpa.dupe(u8, path) catch return;
+ p.gpa.free(f.path);
+ f.path = owned;
+ f.output = null;
+ pane.clearCwd();
+ // re-derive the tag as a plain file's
+ if (pane.tag.own) |own| p.gpa.free(own);
+ pane.tag.own = null;
+ f.saved_revision = f.revision;
+ f.disk_gone = false;
+ ctlfs.events.noteLog(p, .save, pane);
+ p.recent.opened(p.recentGpa(), f.path);
+ if (filesystem.localPath(f.path) != null)
+ p.emit(.{ .watch = .{ .pane = id, .on = true } });
+ }
+
+ /// A terminal whose shell could not be started: said, and a run waiting
+ /// on it, or written to it from now on, hears `error shell gone`
+ /// rather than wait for a prompt that never comes.
+ pub fn shellFailed(p: *Pardes, id: u8, err: anyerror) void {
+ const pane = p.panes[id] orelse return;
+ pane.shell_failed = true;
+ ctlfs.pty.shellGone(p, pane, false);
+ if (err == error.FileNotFound or err == error.NotDir)
+ p.reportFailure(id, "shell: directory missing (no such directory)")
+ else
+ p.reportError(id, "shell", err);
+ // A 9P write that asked for the shell (pty/ctl's exec), waiting on
+ // it, fails with what was said, as a failed Save's does.
+ const said = pane.msg[0..pane.msg_len];
+ const kept = @import("Messages.zig").clip(said, p.fs.late_failure.len);
+ @memcpy(p.fs.late_failure[0..kept.len], kept);
+ p.fs.late_failure_len = @intCast(kept.len);
+ // A command that never started ended: `exit 127`, as a shell says of
+ // a command it could not run, so a follower waiting on its exit
+ // hears one, its tag stops saying it runs, and Kill finds nothing.
+ if (pane.command != null and !pane.command_done) {
+ pane.command_pty = false;
+ exec.commandDone(p, id, 127);
+ } else if (pane.command == null and !pane.had_shell) {
+ // A Tty whose shell never ran: no dead pane left behind; the
+ // write that asked for it has failed with why. A terminal whose
+ // shell was started again (pty/ctl exec) keeps its pane.
+ p.unplaced.set(id);
+ }
+ }
+
pub fn saveFailed(p: *Pardes, id: u8, path: []const u8, err: anyerror) void {
if (p.panes[id]) |pane| if (pane.file) |*f| {
// The `-%` spelling fs.zig already uses for "make this dirty".
f.saved_revision = f.revision -% 1;
};
- var what: [limits.host_path_cap + 8]u8 = undefined;
- p.reportError(id, std.fmt.bufPrint(&what, "Save {s}", .{path}) catch "Save", err);
+ var what: [limits.host_path_cap + 32]u8 = undefined;
+ // A write into a directory that is not there: said so, in the words
+ // a mount reads as ENOENT.
+ const parent = std.fs.path.dirname(path) orelse "/";
+ const no_dir = err == error.FileNotFound or err == error.NotDir or
+ (if (comptime hosted) (if (filesystem.localPath(parent)) |local| !exec.isDirectory(local) else false) else false);
+ if (no_dir)
+ p.reportFailure(id, std.fmt.bufPrint(&what, "Save {s}: no such directory", .{path}) catch "Save: no such directory")
+ else
+ p.reportError(id, std.fmt.bufPrint(&what, "Save {s}", .{path}) catch "Save", err);
const said = if (p.panes[id]) |pane| pane.msg[0..pane.msg_len] else "Save failed";
const kept = @import("Messages.zig").clip(said, p.fs.late_failure.len);
@memcpy(p.fs.late_failure[0..kept.len], kept);
@@ -5229,7 +5381,14 @@ pub const Pardes = struct {
pub fn dumpFailed(p: *Pardes, path: []const u8, err: anyerror) void {
var what: [limits.host_path_cap + 8]u8 = undefined;
const operation = std.fmt.bufPrint(&what, "Dump {s}", .{path}) catch "Dump";
- p.reportError(p.active, operation, err);
+ // Into a DumpDir that is not there: said so, ENOENT, as a Save is.
+ const parent = std.fs.path.dirname(path) orelse "/";
+ const no_dir = err == error.FileNotFound or err == error.NotDir or
+ (if (comptime hosted) (if (filesystem.localPath(parent)) |local| !exec.isDirectory(local) else false) else false);
+ if (no_dir) {
+ var said: [limits.host_path_cap + 48]u8 = undefined;
+ p.reportFailure(p.active, std.fmt.bufPrint(&said, "{s}: no such directory", .{operation}) catch "Dump: no such directory");
+ } else p.reportError(p.active, operation, err);
// In the words the message row has, so the write's err is the one
// record of it (its msg goes: 9p_io, dropMessage).
const said = if (p.panes[p.active]) |pane| pane.msg[0..pane.msg_len] else "Dump failed";
@@ -5283,13 +5442,21 @@ pub const Pardes = struct {
const pane = p.panes[sf.pane] orelse return;
const f = if (pane.file) |*file| file else return;
const serial = pane.serial;
+ const was = f.saved_revision;
+ const failures = p.fs.failures;
f.saved_revision = f.revision;
p.hostWriteFile(sf.pane, f.path, f.content);
const saved_pane = p.panes[sf.pane] orelse return;
if (saved_pane.serial != serial) return;
const saved = if (saved_pane.file) |*file| file else return;
// A save the host could not do (saveFailed) is its err
- // record alone, not a `save`.
+ // record alone, not a `save`, and changes nothing: a clean
+ // file stays clean.
+ if (p.fs.failures != failures) {
+ saved.saved_revision = was;
+ return;
+ }
+ // Edited while it was written: what is on disk is not this.
if (saved.saved_revision != saved.revision) return;
saved.disk_gone = false;
ctlfs.events.noteLog(p, .save, saved_pane);
@@ -5301,7 +5468,21 @@ pub const Pardes = struct {
.save_text => |st| {
const pane = p.panes[st.pane] orelse return;
if (pane.serial != st.serial) return; // a recycled slot: not ours
- if (pane.file) |f| return p.hostWriteFile(st.pane, st.path.slice(), f.content);
+ if (pane.file) |*f| {
+ const was = f.saved_revision;
+ const failures = p.fs.failures;
+ p.hostWriteFile(st.pane, st.path.slice(), f.content);
+ const after = p.panes[st.pane] orelse return;
+ if (after.serial != st.serial) return;
+ const g = if (after.file) |*file| file else return;
+ // Failed, it changes nothing: not the name, not dirty.
+ if (p.fs.failures != failures) {
+ g.saved_revision = was;
+ return;
+ }
+ if (st.promote) p.promoteSaved(st.pane, st.path.slice());
+ return;
+ }
if (!pane.isTerminal()) return;
const text = panes.Terminal.screenTextAlloc(pane, p.gpa) catch return;
defer p.gpa.free(text);
@@ -5503,6 +5684,7 @@ pub const Pardes = struct {
},
.output => |o| {
const pane = p.panes[o.pane] orelse return;
+ pane.shell_spoke = true;
ctlfs.events.notePtyOutput(p, o.pane, o.bytes);
panes.Terminal.feedOutput(p, pane, o.bytes);
ctlfs.pty.noteMarks(p, o.pane, pane);
@@ -6010,6 +6192,8 @@ pub const Pardes = struct {
// ctl file: the write fails instead (src/ninep/ctl.zig).
if (p.fs.no_prompt) {
p.fs.refused = true;
+ // A failure like any other: the builtin's announcement goes.
+ p.fs.failures +%= 1;
return;
}
// Asked for over 9P (Save from an exec on a terminal): a question
@@ -6457,9 +6641,15 @@ pub const Pardes = struct {
var cur: usize = 0;
for (p.jumps[0..p.njumps], 0..) |j, i| {
if (i == p.jcur) cur = w; // survives -> lands at w; dies -> the next one does
- const pane = p.panes[j.pane] orelse continue;
- if (pane.serial != j.serial) continue;
- p.jumps[w] = j;
+ var kept = j;
+ const live = if (j.serial == 0) false else if (p.panes[j.pane]) |pane| pane.serial == j.serial else false;
+ // A file's pane closed: its entry stays, closed, while the recent
+ // list knows the file, and a jump to it opens it again.
+ if (!live) {
+ if (j.path_hash == 0 or p.recentByHash(j.path_hash) == null) continue;
+ kept.serial = 0;
+ }
+ p.jumps[w] = kept;
w += 1;
}
p.njumps = w;
@@ -6471,6 +6661,7 @@ pub const Pardes = struct {
.serial = pane.serial,
.line = if (pane.pdfPage()) |page| @intCast(page + 1) else if (pane.body.cur_pinned) @intCast(@max(0, pane.body.cur_row) + 1) else 0,
.col = if (pane.pdfPage() != null) 0 else if (pane.body.cur_pinned) @intCast(@max(0, pane.body.cur_col) + 1) else 0,
+ .path_hash = if (recentDot(pane)) |d| std.hash.Wyhash.hash(0, d.path) else 0,
};
if (p.njumps > 0 and p.samePlace(p.jumps[p.jcur], now)) {
p.jumps[p.jcur] = now; // same visit; the entry IS where you are
@@ -6499,6 +6690,21 @@ pub const Pardes = struct {
if (p.njumps == 0 or next < 0 or next >= p.njumps) return;
const j = p.jumps[@intCast(next)];
if (j.pane >= MAX_PANES) return;
+ // A closed file's entry: opened again at its dot, placed as a look
+ // places it, and the entry is that pane from here on.
+ if (j.serial == 0) {
+ const e = p.recentByHash(j.path_hash) orelse return;
+ var where: [recent_path_cap + 32]u8 = undefined;
+ const loc = std.fmt.bufPrint(&where, "{s}:{d}:{d}", .{ e.path, @max(1, j.line), @max(1, j.col) }) catch return;
+ const from = p.active;
+ look.lookAt(p, from, loc);
+ const opened = p.panes[p.active] orelse return;
+ if (p.active == from) return;
+ p.jumps[@intCast(next)].pane = @intCast(p.active);
+ p.jumps[@intCast(next)].serial = opened.serial;
+ p.jcur = @intCast(next);
+ return;
+ }
const pane = p.panes[j.pane] orelse return;
if (pane.serial != j.serial) return;
if (p.panes[p.active]) |source| look.clearNavigationSelection(p, source);
@@ -6525,10 +6731,18 @@ pub const Pardes = struct {
}
pub fn sync(p: *Pardes) void {
+ var refused = false;
while (p.unplaced.findFirstSet()) |id| {
p.unplaced.unset(id);
if (p.panes[id] != null) p.removePane(id, null) catch {};
+ refused = true;
}
+ // A refused pane gone, no pane it squeezed stays under its minimum.
+ if (refused) for (0..p.ncol) |c| if (!layout.columnAtMinimums(p, c)) {
+ _ = layout.shareColumn(p, c);
+ };
+ // A file opened or closed this step: the kept list follows.
+ p.saveRecent();
tagline.flattenHeaders(p);
if (p.header_column != null and p.columnBarHeight() == 0) {
tagline.exitHeader(p);
@@ -6631,11 +6845,19 @@ pub const Pardes = struct {
if (pane.file) |*file| file.syntax_dirty = true;
}
panes.Terminal.releasePendingCommandIfReady(p, id, pane);
- if (pane.greet and pane.isTerminal() and p.resize_count > 0 and panes.Terminal.promptInputReady(pane)) {
- p.emit(.{ .resize_pty = .{ .pane = @intCast(id), .cols = pane.cols, .rows = pane.rows } });
- p.emit(.{ .write = .{ .pane = @intCast(id), .bytes = .from("ls\r") } });
- pane.greet = false;
- }
+ // The greeting `ls` of a new terminal in a directory is typed at
+ // its shell's FIRST prompt, or never: typed later it would land
+ // in whatever runs then, or in the middle of a line someone
+ // typed (panes.Terminal.greeting).
+ if (pane.greet and pane.isTerminal() and p.resize_count > 0) switch (panes.Terminal.greeting(pane)) {
+ .wait => {},
+ .never => pane.greet = false,
+ .now => {
+ p.emit(.{ .resize_pty = .{ .pane = @intCast(id), .cols = pane.cols, .rows = pane.rows } });
+ p.emit(.{ .write = .{ .pane = @intCast(id), .bytes = .from("ls\r") } });
+ pane.greet = false;
+ },
+ };
}
}