From e59b2f7fca460a75d12cb639514d86f25c1d4dfa Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 May 2023 18:54:24 -0700 Subject: [PATCH] terminal: track pwd reported via OSC 7 --- src/terminal/Terminal.zig | 19 ++++++++++++++++++ src/terminal/osc.zig | 13 +++++++++++- src/terminal/stream.zig | 6 ++++++ src/termio/Exec.zig | 42 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 9fa814e3e..420d622c5 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -64,6 +64,9 @@ cols: usize, /// The current scrolling region. scrolling_region: ScrollingRegion, +/// The last reported pwd, if any. +pwd: std.ArrayList(u8), + /// The charset state charset: CharsetState = .{}, @@ -169,6 +172,7 @@ pub fn init(alloc: Allocator, cols: usize, rows: usize) !Terminal { .top = 0, .bottom = rows - 1, }, + .pwd = std.ArrayList(u8).init(alloc), }; } @@ -176,6 +180,7 @@ pub fn deinit(self: *Terminal, alloc: Allocator) void { self.tabstops.deinit(alloc); self.screen.deinit(); self.secondary_screen.deinit(); + self.pwd.deinit(); self.* = undefined; } @@ -1501,6 +1506,19 @@ pub fn cursorIsAtPrompt(self: *Terminal) bool { return false; } +/// Set the pwd for the terminal. +pub fn setPwd(self: *Terminal, pwd: []const u8) !void { + self.pwd.clearRetainingCapacity(); + try self.pwd.appendSlice(pwd); +} + +/// Returns the pwd for the terminal, if any. The memory is owned by the +/// Terminal and is not copied. It is safe until a reset or setPwd. +pub fn getPwd(self: *Terminal) ?[]const u8 { + if (self.pwd.items.len == 0) return null; + return self.pwd.items; +} + /// Full reset pub fn fullReset(self: *Terminal) void { self.primaryScreen(.{ .clear_on_exit = true, .cursor_save = true }); @@ -1514,6 +1532,7 @@ pub fn fullReset(self: *Terminal) void { self.screen.selection = null; self.scrolling_region = .{ .top = 0, .bottom = self.rows - 1 }; self.previous_char = null; + self.pwd.clearRetainingCapacity(); } test "Terminal: input with no control characters" { diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 917de83be..ccbdd044e 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -242,7 +242,7 @@ pub const Parser = struct { .@"7" => switch (c) { ';' => { - self.command = .{ .report_pwd = .{ .value = undefined } }; + self.command = .{ .report_pwd = .{ .value = "" } }; self.state = .string; self.temp_state = .{ .str = &self.command.report_pwd.value }; @@ -593,6 +593,17 @@ test "OSC: report pwd" { try testing.expect(std.mem.eql(u8, "file:///tmp/example", cmd.report_pwd.value)); } +test "OSC: report pwd empty" { + const testing = std.testing; + + var p: Parser = .{}; + + const input = "7;"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end() == null); +} + test "OSC: longer than buffer" { const testing = std.testing; diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 2f8766c56..f36fa6315 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -495,6 +495,12 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, + .report_pwd => |v| { + if (@hasDecl(T, "reportPwd")) { + try self.handler.reportPwd(v.value); + } else log.warn("unimplemented OSC callback: {}", .{cmd}); + }, + else => if (@hasDecl(T, "oscUnimplemented")) try self.handler.oscUnimplemented(cmd) else diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 294019b83..d09963419 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1249,4 +1249,46 @@ const StreamHandler = struct { pub fn endOfInput(self: *StreamHandler) !void { self.terminal.markSemanticPrompt(.command); } + + pub fn reportPwd(self: *StreamHandler, url: []const u8) !void { + const uri = std.Uri.parse(url) catch |e| { + log.warn("invalid url in OSC 7: {}", .{e}); + return; + }; + + if (!std.mem.eql(u8, "file", uri.scheme) and + !std.mem.eql(u8, "kitty-shell-cwd", uri.scheme)) + { + log.warn("OSC 7 scheme must be file, got: {s}", .{uri.scheme}); + return; + } + + // OSC 7 is a little sketchy because anyone can send any value from + // any host (such an SSH session). The best practice terminals follow + // is to valid the hostname to be local. + const host_valid = host_valid: { + const host = uri.host orelse break :host_valid false; + + // Empty or localhost is always good + if (host.len == 0 or std.mem.eql(u8, "localhost", host)) { + break :host_valid true; + } + + // Otherwise, it must match our hostname. + var buf: [std.os.HOST_NAME_MAX]u8 = undefined; + const hostname = std.os.gethostname(&buf) catch |err| { + log.warn("failed to get hostname for OSC 7 validation: {}", .{err}); + break :host_valid false; + }; + + break :host_valid std.mem.eql(u8, host, hostname); + }; + if (!host_valid) { + log.warn("OSC 7 host must be local", .{}); + return; + } + + log.debug("terminal pwd: {s}", .{uri.path}); + try self.terminal.setPwd(uri.path); + } };