From b53807297291713919de9408f48e8fe5f19beb4b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 May 2023 17:32:06 -0700 Subject: [PATCH 1/8] terminal: osc 7 parsing --- src/terminal/osc.zig | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 3063bed40..917de83be 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -72,6 +72,16 @@ pub const Command = union(enum) { kind: u8, data: []const u8, }, + + /// OSC 7. Reports the current working directory of the shell. This is + /// a moderately flawed escape sequence but one that many major terminals + /// support so we also support it. To understand the flaws, read through + /// this terminal-wg issue: https://gitlab.freedesktop.org/terminal-wg/specifications/-/issues/20 + report_pwd: struct { + /// The reported pwd value. This is not checked for validity. It should + /// be a file URL but it is up to the caller to utilize this value. + value: []const u8, + }, }; pub const Parser = struct { @@ -121,6 +131,7 @@ pub const Parser = struct { @"2", @"5", @"52", + @"7", // We're in a semantic prompt OSC command but we aren't sure // what the command is yet, i.e. `133;` @@ -173,6 +184,7 @@ pub const Parser = struct { '1' => self.state = .@"1", '2' => self.state = .@"2", '5' => self.state = .@"5", + '7' => self.state = .@"7", else => self.state = .invalid, }, @@ -228,6 +240,17 @@ pub const Parser = struct { else => self.state = .invalid, }, + .@"7" => switch (c) { + ';' => { + self.command = .{ .report_pwd = .{ .value = undefined } }; + + self.state = .string; + self.temp_state = .{ .str = &self.command.report_pwd.value }; + self.buf_start = self.buf_idx; + }, + else => self.state = .invalid, + }, + .@"52" => switch (c) { ';' => { self.command = .{ .clipboard_contents = undefined }; @@ -557,6 +580,19 @@ test "OSC: get/set clipboard" { try testing.expect(std.mem.eql(u8, "?", cmd.clipboard_contents.data)); } +test "OSC: report pwd" { + const testing = std.testing; + + var p: Parser = .{}; + + const input = "7;file:///tmp/example"; + for (input) |ch| p.next(ch); + + const cmd = p.end().?; + try testing.expect(cmd == .report_pwd); + try testing.expect(std.mem.eql(u8, "file:///tmp/example", cmd.report_pwd.value)); +} + test "OSC: longer than buffer" { const testing = std.testing; From e59b2f7fca460a75d12cb639514d86f25c1d4dfa Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 May 2023 18:54:24 -0700 Subject: [PATCH 2/8] 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); + } }; From a158813a3dc5b1dc30b0bade4b0ff7e62eda9149 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 May 2023 18:59:40 -0700 Subject: [PATCH 3/8] app keeps track of last focused surface --- src/App.zig | 17 +++++++++++++++++ src/Surface.zig | 7 +++++++ 2 files changed, 24 insertions(+) diff --git a/src/App.zig b/src/App.zig index b39938392..52c926d2a 100644 --- a/src/App.zig +++ b/src/App.zig @@ -29,6 +29,10 @@ alloc: Allocator, /// The list of surfaces that are currently active. surfaces: SurfaceList, +/// The last focused surface. This surface may not be valid; +/// you must always call hasSurface to validate it. +focused_surface: ?*Surface = null, + /// The mailbox that can be used to send this thread messages. Note /// this is a blocking queue so if it is full you will get errors (or block). mailbox: Mailbox.Queue, @@ -131,6 +135,7 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void { .reload_config => try self.reloadConfig(rt_app), .new_window => |msg| try self.newWindow(rt_app, msg), .close => |surface| try self.closeSurface(rt_app, surface), + .focus => |surface| try self.focusSurface(rt_app, surface), .quit => try self.setQuit(), .surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message), .redraw_surface => |surface| try self.redrawSurface(rt_app, surface), @@ -153,6 +158,13 @@ fn closeSurface(self: *App, rt_app: *apprt.App, surface: *Surface) !void { surface.close(); } +fn focusSurface(self: *App, rt_app: *apprt.App, surface: *Surface) !void { + _ = rt_app; + + if (!self.hasSurface(surface)) return; + self.focused_surface = surface; +} + fn redrawSurface(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) !void { if (!self.hasSurface(&surface.core_surface)) return; rt_app.redrawSurface(surface); @@ -215,6 +227,11 @@ pub const Message = union(enum) { /// should close. close: *Surface, + /// The last focused surface. The app keeps track of this to + /// enable "inheriting" various configurations from the last + /// surface. + focus: *Surface, + /// Quit quit: void, diff --git a/src/Surface.zig b/src/Surface.zig index 0d621f758..e9ea5eafb 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1224,6 +1224,13 @@ pub fn focusCallback(self: *Surface, focused: bool) !void { .focus = focused, }, .{ .forever = {} }); + // Notify our app if we gained focus. + if (focused) { + _ = self.app_mailbox.push(.{ + .focus = self, + }, .{ .forever = {} }); + } + // Schedule render which also drains our mailbox try self.queueRender(); From 553e09eff988d83854e5a81af74f8b8d4f5b8eb4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 May 2023 19:12:01 -0700 Subject: [PATCH 4/8] apprt/embedded: new surfaces inherit last focused --- src/App.zig | 8 ++++++++ src/Surface.zig | 10 ++++++++++ src/apprt/embedded.zig | 20 ++++++++++++++++++-- src/terminal/Terminal.zig | 2 +- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/App.zig b/src/App.zig index 52c926d2a..5b638fdf2 100644 --- a/src/App.zig +++ b/src/App.zig @@ -127,6 +127,14 @@ pub fn deleteSurface(self: *App, rt_surface: *apprt.Surface) void { } } +/// The last focused surface. This is only valid while on the main thread +/// before tick is called. +pub fn focusedSurface(self: *App) ?*Surface { + const surface = self.focused_surface orelse return null; + if (!self.hasSurface(surface)) return null; + return surface; +} + /// Drain the mailbox. fn drainMailbox(self: *App, rt_app: *apprt.App) !void { while (self.mailbox.pop()) |message| { diff --git a/src/Surface.zig b/src/Surface.zig index e9ea5eafb..2a5a9f6a9 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -631,6 +631,16 @@ fn changeConfig(self: *Surface, config: *const configpkg.Config) !void { }; } +/// Returns the pwd of the terminal, if any. This is always copied because +/// the pwd can change at any point from termio. If we are calling from the IO +/// thread you should just check the terminal directly. +pub fn pwd(self: *const Surface, alloc: Allocator) !?[]const u8 { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + const terminal_pwd = self.io.terminal.getPwd() orelse return null; + return try alloc.dupe(u8, terminal_pwd); +} + /// Returns the x/y coordinate of where the IME (Input Method Editor) /// keyboard should be rendered. pub fn imePoint(self: *const Surface) apprt.IMEPos { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index b4c8577f3..29a8c96e7 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -144,6 +144,8 @@ pub const Surface = struct { }; pub fn init(self: *Surface, app: *App, opts: Options) !void { + const alloc = app.core_app.alloc; + self.* = .{ .app = app, .core_surface = undefined, @@ -161,11 +163,25 @@ pub const Surface = struct { try app.core_app.addSurface(self); errdefer app.core_app.deleteSurface(self); + // Our parent pwd will be tracked here + var parent_pwd: ?[]const u8 = null; + defer if (parent_pwd) |v| alloc.free(v); + + // Shallow copy the config so that we can modify it. + var config = app.config.*; + + // Get our previously focused surface + const parent = app.core_app.focusedSurface(); + if (parent) |p| { + parent_pwd = try p.pwd(alloc); + if (parent_pwd) |v| config.@"working-directory" = v; + } + // Initialize our surface right away. We're given a view that is // ready to use. try self.core_surface.init( - app.core_app.alloc, - app.config, + alloc, + &config, .{ .rt_app = app, .mailbox = &app.core_app.mailbox }, self, ); diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 420d622c5..57f95841f 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1514,7 +1514,7 @@ pub fn setPwd(self: *Terminal, pwd: []const u8) !void { /// 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 { +pub fn getPwd(self: *const Terminal) ?[]const u8 { if (self.pwd.items.len == 0) return null; return self.pwd.items; } From 866de88ee62c7e5d8d43770c9ea088081ec9921c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 May 2023 19:17:08 -0700 Subject: [PATCH 5/8] termio/exec: check cwd validity --- src/termio/Exec.zig | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index d09963419..0f513c0ba 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -614,12 +614,24 @@ const Subprocess = struct { return pty.master; } + // If we can't access the cwd, then don't set any cwd and inherit. + // This is important because our cwd can be set by the shell (OSC 7) + // and we don't want to break new windows. + const cwd: ?[]const u8 = if (self.cwd) |proposed| cwd: { + if (std.fs.accessAbsolute(proposed, .{})) { + break :cwd proposed; + } else |err| { + log.warn("cannot access cwd, ignoring: {}", .{err}); + break :cwd null; + } + } else null; + // Build our subcommand var cmd: Command = .{ .path = self.path, .args = self.args, .env = &self.env, - .cwd = self.cwd, + .cwd = cwd, .stdin = .{ .handle = pty.slave }, .stdout = .{ .handle = pty.slave }, .stderr = .{ .handle = pty.slave }, From 0fca74c0897ccd58db369cb030222631c81c8324 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 May 2023 19:18:55 -0700 Subject: [PATCH 6/8] apprt/glfw: inherit working dir --- src/apprt/glfw.zig | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index d8da82640..b65573a03 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -346,10 +346,25 @@ pub const Surface = struct { try app.app.addSurface(self); errdefer app.app.deleteSurface(self); + // Our parent pwd will be tracked here + const alloc = app.app.alloc; + var parent_pwd: ?[]const u8 = null; + defer if (parent_pwd) |v| alloc.free(v); + + // Shallow copy the config so that we can modify it. + var config = app.config; + + // Get our previously focused surface + const parent = app.app.focusedSurface(); + if (parent) |p| { + parent_pwd = try p.pwd(alloc); + if (parent_pwd) |v| config.@"working-directory" = v; + } + // Initialize our surface now that we have the stable pointer. try self.core_surface.init( - app.app.alloc, - &app.config, + alloc, + &config, .{ .rt_app = app, .mailbox = &app.app.mailbox }, self, ); From f31d6fb8fe64ba52d5adde20a17c6709abe7d0c8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 May 2023 21:08:50 -0700 Subject: [PATCH 7/8] apprt: clean up how apprt initializes surfaces --- src/App.zig | 4 ++-- src/apprt/embedded.zig | 18 +++--------------- src/apprt/glfw.zig | 19 ++++--------------- src/apprt/gtk.zig | 6 +++++- src/apprt/surface.zig | 21 +++++++++++++++++++++ src/config.zig | 15 +++++++++++++++ 6 files changed, 50 insertions(+), 33 deletions(-) diff --git a/src/App.zig b/src/App.zig index 5b638fdf2..b2893fdd3 100644 --- a/src/App.zig +++ b/src/App.zig @@ -129,7 +129,7 @@ pub fn deleteSurface(self: *App, rt_surface: *apprt.Surface) void { /// The last focused surface. This is only valid while on the main thread /// before tick is called. -pub fn focusedSurface(self: *App) ?*Surface { +pub fn focusedSurface(self: *const App) ?*Surface { const surface = self.focused_surface orelse return null; if (!self.hasSurface(surface)) return null; return surface; @@ -214,7 +214,7 @@ fn surfaceMessage(self: *App, surface: *Surface, msg: apprt.surface.Message) !vo // Not a problem. } -fn hasSurface(self: *App, surface: *Surface) bool { +fn hasSurface(self: *const App, surface: *const Surface) bool { for (self.surfaces.items) |v| { if (&v.core_surface == surface) return true; } diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 29a8c96e7..36e7d3e77 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -144,8 +144,6 @@ pub const Surface = struct { }; pub fn init(self: *Surface, app: *App, opts: Options) !void { - const alloc = app.core_app.alloc; - self.* = .{ .app = app, .core_surface = undefined, @@ -163,24 +161,14 @@ pub const Surface = struct { try app.core_app.addSurface(self); errdefer app.core_app.deleteSurface(self); - // Our parent pwd will be tracked here - var parent_pwd: ?[]const u8 = null; - defer if (parent_pwd) |v| alloc.free(v); - // Shallow copy the config so that we can modify it. - var config = app.config.*; - - // Get our previously focused surface - const parent = app.core_app.focusedSurface(); - if (parent) |p| { - parent_pwd = try p.pwd(alloc); - if (parent_pwd) |v| config.@"working-directory" = v; - } + var config = try apprt.surface.newConfig(app.core_app, app.config); + defer config.deinit(); // Initialize our surface right away. We're given a view that is // ready to use. try self.core_surface.init( - alloc, + app.core_app.alloc, &config, .{ .rt_app = app, .mailbox = &app.core_app.mailbox }, self, diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index b65573a03..822437b50 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -346,24 +346,13 @@ pub const Surface = struct { try app.app.addSurface(self); errdefer app.app.deleteSurface(self); - // Our parent pwd will be tracked here - const alloc = app.app.alloc; - var parent_pwd: ?[]const u8 = null; - defer if (parent_pwd) |v| alloc.free(v); - - // Shallow copy the config so that we can modify it. - var config = app.config; - - // Get our previously focused surface - const parent = app.app.focusedSurface(); - if (parent) |p| { - parent_pwd = try p.pwd(alloc); - if (parent_pwd) |v| config.@"working-directory" = v; - } + // Get our new surface config + var config = try apprt.surface.newConfig(app.app, &app.config); + defer config.deinit(); // Initialize our surface now that we have the stable pointer. try self.core_surface.init( - alloc, + app.app.alloc, &config, .{ .rt_app = app, .mailbox = &app.app.mailbox }, self, diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 51ca0d910..05e43cee3 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -722,10 +722,14 @@ pub const Surface = struct { try self.app.core_app.addSurface(self); errdefer self.app.core_app.deleteSurface(self); + // Get our new surface config + var config = try apprt.surface.newConfig(self.app.core_app, &self.app.config); + defer config.deinit(); + // Initialize our surface now that we have the stable pointer. try self.core_surface.init( self.app.core_app.alloc, - &self.app.config, + &config, .{ .rt_app = self.app, .mailbox = &self.app.core_app.mailbox }, self, ); diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 859d694d5..67b4247e8 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -61,3 +61,24 @@ pub const Mailbox = struct { }, timeout); } }; + +/// Returns a new config for a surface for the given app that should be +/// used for any new surfaces. The resulting config should be deinitialized +/// after the surface is initialized. +pub fn newConfig(app: *const App, config: *const Config) !Config { + // Create a shallow clone + var copy = config.shallowClone(app.alloc); + + // Our allocator is our config's arena + const alloc = copy._arena.?.allocator(); + + // Get our previously focused surface for some inherited values. + const prev = app.focusedSurface(); + if (prev) |p| { + if (try p.pwd(alloc)) |pwd| { + copy.@"working-directory" = pwd; + } + } + + return copy; +} diff --git a/src/config.zig b/src/config.zig index 6d75e4e11..9a82c59d2 100644 --- a/src/config.zig +++ b/src/config.zig @@ -691,6 +691,21 @@ pub const Config = struct { } } + /// Create a shallow copy of this config. This will share all the memory + /// allocated with the previous config but will have a new arena for + /// any changes or new allocations. The config should have `deinit` + /// called when it is complete. + /// + /// Beware: these shallow clones are not meant for a long lifetime, + /// they are just meant to exist temporarily for the duration of some + /// modifications. It is very important that the original config not + /// be deallocated while shallow clones exist. + pub fn shallowClone(self: *const Config, alloc_gpa: Allocator) Config { + var result = self.*; + result._arena = ArenaAllocator.init(alloc_gpa); + return result; + } + /// Create a copy of this configuration. This is useful as a starting /// point for modifying a configuration since a config can NOT be /// modified once it is in use by an app or surface. From c58e2f7aec36e522288dce940e71809b7d5a3d8b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 May 2023 21:11:40 -0700 Subject: [PATCH 8/8] update README --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ffa734403..44868a6d0 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ April 2022. To configure Ghostty, you must use a configuration file. GUI-based configuration is on the roadmap but not yet supported. The configuration file must be -placed at `$XDG_CONFIG_HOME/ghostty/config`, which defaults to +placed at `$XDG_CONFIG_HOME/ghostty/config`, which defaults to `~/.config/ghostty/config` if the [XDG environment is not set](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html). The file format is documented below as an example: @@ -90,17 +90,19 @@ Eventually, we'll have a better mecanism for showing errors to the user. ### Shell Integration Ghostty supports some features that require shell integration. I am aiming -to support many of the features that +to support many of the features that [Kitty supports for shell integration](https://sw.kovidgoyal.net/kitty/shell-integration/). -Today, the most important quality-of-life feature is that Ghostty will -not confirm window close if it detects that the cursor is sitting at a prompt -input (i.e. no subprocess is running). - To enable this functionality, I recommend sourcing Kitty's shell integration files directly for your shell configuration when running Ghostty. For example, for fish, [source this file](https://github.com/kovidgoyal/kitty/blob/master/shell-integration/fish/vendor_conf.d/kitty-shell-integration.fish). +The currently support shell integration features in Ghostty: + + * We do not confirm close for windows where the cursor is at a prompt. + * New terminals start in the working directory of the previously focused terminal. + * The cursor at the prompt is turned into a bar. + ## Roadmap and Status The high-level ambitious plan for the project, in order: