mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
Merge pull request #149 from mitchellh/osc7
OSC 7 (report pwd) and new Window/Tab/Split uses current cwd
This commit is contained in:
14
README.md
14
README.md
@ -53,7 +53,7 @@ April 2022.
|
|||||||
|
|
||||||
To configure Ghostty, you must use a configuration file. GUI-based configuration
|
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
|
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).
|
`~/.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:
|
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
|
### Shell Integration
|
||||||
|
|
||||||
Ghostty supports some features that require shell integration. I am aiming
|
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/).
|
[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
|
To enable this functionality, I recommend sourcing Kitty's shell integration
|
||||||
files directly for your shell configuration when running Ghostty. For
|
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).
|
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
|
## Roadmap and Status
|
||||||
|
|
||||||
The high-level ambitious plan for the project, in order:
|
The high-level ambitious plan for the project, in order:
|
||||||
|
27
src/App.zig
27
src/App.zig
@ -29,6 +29,10 @@ alloc: Allocator,
|
|||||||
/// The list of surfaces that are currently active.
|
/// The list of surfaces that are currently active.
|
||||||
surfaces: SurfaceList,
|
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
|
/// 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).
|
/// this is a blocking queue so if it is full you will get errors (or block).
|
||||||
mailbox: Mailbox.Queue,
|
mailbox: Mailbox.Queue,
|
||||||
@ -123,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: *const App) ?*Surface {
|
||||||
|
const surface = self.focused_surface orelse return null;
|
||||||
|
if (!self.hasSurface(surface)) return null;
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
|
|
||||||
/// Drain the mailbox.
|
/// Drain the mailbox.
|
||||||
fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
||||||
while (self.mailbox.pop()) |message| {
|
while (self.mailbox.pop()) |message| {
|
||||||
@ -131,6 +143,7 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
|||||||
.reload_config => try self.reloadConfig(rt_app),
|
.reload_config => try self.reloadConfig(rt_app),
|
||||||
.new_window => |msg| try self.newWindow(rt_app, msg),
|
.new_window => |msg| try self.newWindow(rt_app, msg),
|
||||||
.close => |surface| try self.closeSurface(rt_app, surface),
|
.close => |surface| try self.closeSurface(rt_app, surface),
|
||||||
|
.focus => |surface| try self.focusSurface(rt_app, surface),
|
||||||
.quit => try self.setQuit(),
|
.quit => try self.setQuit(),
|
||||||
.surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message),
|
.surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message),
|
||||||
.redraw_surface => |surface| try self.redrawSurface(rt_app, surface),
|
.redraw_surface => |surface| try self.redrawSurface(rt_app, surface),
|
||||||
@ -153,6 +166,13 @@ fn closeSurface(self: *App, rt_app: *apprt.App, surface: *Surface) !void {
|
|||||||
surface.close();
|
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 {
|
fn redrawSurface(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) !void {
|
||||||
if (!self.hasSurface(&surface.core_surface)) return;
|
if (!self.hasSurface(&surface.core_surface)) return;
|
||||||
rt_app.redrawSurface(surface);
|
rt_app.redrawSurface(surface);
|
||||||
@ -194,7 +214,7 @@ fn surfaceMessage(self: *App, surface: *Surface, msg: apprt.surface.Message) !vo
|
|||||||
// Not a problem.
|
// Not a problem.
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hasSurface(self: *App, surface: *Surface) bool {
|
fn hasSurface(self: *const App, surface: *const Surface) bool {
|
||||||
for (self.surfaces.items) |v| {
|
for (self.surfaces.items) |v| {
|
||||||
if (&v.core_surface == surface) return true;
|
if (&v.core_surface == surface) return true;
|
||||||
}
|
}
|
||||||
@ -215,6 +235,11 @@ pub const Message = union(enum) {
|
|||||||
/// should close.
|
/// should close.
|
||||||
close: *Surface,
|
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
|
||||||
quit: void,
|
quit: void,
|
||||||
|
|
||||||
|
@ -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)
|
/// Returns the x/y coordinate of where the IME (Input Method Editor)
|
||||||
/// keyboard should be rendered.
|
/// keyboard should be rendered.
|
||||||
pub fn imePoint(self: *const Surface) apprt.IMEPos {
|
pub fn imePoint(self: *const Surface) apprt.IMEPos {
|
||||||
@ -1224,6 +1234,13 @@ pub fn focusCallback(self: *Surface, focused: bool) !void {
|
|||||||
.focus = focused,
|
.focus = focused,
|
||||||
}, .{ .forever = {} });
|
}, .{ .forever = {} });
|
||||||
|
|
||||||
|
// Notify our app if we gained focus.
|
||||||
|
if (focused) {
|
||||||
|
_ = self.app_mailbox.push(.{
|
||||||
|
.focus = self,
|
||||||
|
}, .{ .forever = {} });
|
||||||
|
}
|
||||||
|
|
||||||
// Schedule render which also drains our mailbox
|
// Schedule render which also drains our mailbox
|
||||||
try self.queueRender();
|
try self.queueRender();
|
||||||
|
|
||||||
|
@ -161,11 +161,15 @@ pub const Surface = struct {
|
|||||||
try app.core_app.addSurface(self);
|
try app.core_app.addSurface(self);
|
||||||
errdefer app.core_app.deleteSurface(self);
|
errdefer app.core_app.deleteSurface(self);
|
||||||
|
|
||||||
|
// Shallow copy the config so that we can modify it.
|
||||||
|
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
|
// Initialize our surface right away. We're given a view that is
|
||||||
// ready to use.
|
// ready to use.
|
||||||
try self.core_surface.init(
|
try self.core_surface.init(
|
||||||
app.core_app.alloc,
|
app.core_app.alloc,
|
||||||
app.config,
|
&config,
|
||||||
.{ .rt_app = app, .mailbox = &app.core_app.mailbox },
|
.{ .rt_app = app, .mailbox = &app.core_app.mailbox },
|
||||||
self,
|
self,
|
||||||
);
|
);
|
||||||
|
@ -346,10 +346,14 @@ pub const Surface = struct {
|
|||||||
try app.app.addSurface(self);
|
try app.app.addSurface(self);
|
||||||
errdefer app.app.deleteSurface(self);
|
errdefer app.app.deleteSurface(self);
|
||||||
|
|
||||||
|
// 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.
|
// Initialize our surface now that we have the stable pointer.
|
||||||
try self.core_surface.init(
|
try self.core_surface.init(
|
||||||
app.app.alloc,
|
app.app.alloc,
|
||||||
&app.config,
|
&config,
|
||||||
.{ .rt_app = app, .mailbox = &app.app.mailbox },
|
.{ .rt_app = app, .mailbox = &app.app.mailbox },
|
||||||
self,
|
self,
|
||||||
);
|
);
|
||||||
|
@ -722,10 +722,14 @@ pub const Surface = struct {
|
|||||||
try self.app.core_app.addSurface(self);
|
try self.app.core_app.addSurface(self);
|
||||||
errdefer self.app.core_app.deleteSurface(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.
|
// Initialize our surface now that we have the stable pointer.
|
||||||
try self.core_surface.init(
|
try self.core_surface.init(
|
||||||
self.app.core_app.alloc,
|
self.app.core_app.alloc,
|
||||||
&self.app.config,
|
&config,
|
||||||
.{ .rt_app = self.app, .mailbox = &self.app.core_app.mailbox },
|
.{ .rt_app = self.app, .mailbox = &self.app.core_app.mailbox },
|
||||||
self,
|
self,
|
||||||
);
|
);
|
||||||
|
@ -61,3 +61,24 @@ pub const Mailbox = struct {
|
|||||||
}, timeout);
|
}, 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;
|
||||||
|
}
|
||||||
|
@ -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
|
/// Create a copy of this configuration. This is useful as a starting
|
||||||
/// point for modifying a configuration since a config can NOT be
|
/// point for modifying a configuration since a config can NOT be
|
||||||
/// modified once it is in use by an app or surface.
|
/// modified once it is in use by an app or surface.
|
||||||
|
@ -64,6 +64,9 @@ cols: usize,
|
|||||||
/// The current scrolling region.
|
/// The current scrolling region.
|
||||||
scrolling_region: ScrollingRegion,
|
scrolling_region: ScrollingRegion,
|
||||||
|
|
||||||
|
/// The last reported pwd, if any.
|
||||||
|
pwd: std.ArrayList(u8),
|
||||||
|
|
||||||
/// The charset state
|
/// The charset state
|
||||||
charset: CharsetState = .{},
|
charset: CharsetState = .{},
|
||||||
|
|
||||||
@ -169,6 +172,7 @@ pub fn init(alloc: Allocator, cols: usize, rows: usize) !Terminal {
|
|||||||
.top = 0,
|
.top = 0,
|
||||||
.bottom = rows - 1,
|
.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.tabstops.deinit(alloc);
|
||||||
self.screen.deinit();
|
self.screen.deinit();
|
||||||
self.secondary_screen.deinit();
|
self.secondary_screen.deinit();
|
||||||
|
self.pwd.deinit();
|
||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1501,6 +1506,19 @@ pub fn cursorIsAtPrompt(self: *Terminal) bool {
|
|||||||
return false;
|
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: *const Terminal) ?[]const u8 {
|
||||||
|
if (self.pwd.items.len == 0) return null;
|
||||||
|
return self.pwd.items;
|
||||||
|
}
|
||||||
|
|
||||||
/// Full reset
|
/// Full reset
|
||||||
pub fn fullReset(self: *Terminal) void {
|
pub fn fullReset(self: *Terminal) void {
|
||||||
self.primaryScreen(.{ .clear_on_exit = true, .cursor_save = true });
|
self.primaryScreen(.{ .clear_on_exit = true, .cursor_save = true });
|
||||||
@ -1514,6 +1532,7 @@ pub fn fullReset(self: *Terminal) void {
|
|||||||
self.screen.selection = null;
|
self.screen.selection = null;
|
||||||
self.scrolling_region = .{ .top = 0, .bottom = self.rows - 1 };
|
self.scrolling_region = .{ .top = 0, .bottom = self.rows - 1 };
|
||||||
self.previous_char = null;
|
self.previous_char = null;
|
||||||
|
self.pwd.clearRetainingCapacity();
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Terminal: input with no control characters" {
|
test "Terminal: input with no control characters" {
|
||||||
|
@ -72,6 +72,16 @@ pub const Command = union(enum) {
|
|||||||
kind: u8,
|
kind: u8,
|
||||||
data: []const 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 {
|
pub const Parser = struct {
|
||||||
@ -121,6 +131,7 @@ pub const Parser = struct {
|
|||||||
@"2",
|
@"2",
|
||||||
@"5",
|
@"5",
|
||||||
@"52",
|
@"52",
|
||||||
|
@"7",
|
||||||
|
|
||||||
// We're in a semantic prompt OSC command but we aren't sure
|
// We're in a semantic prompt OSC command but we aren't sure
|
||||||
// what the command is yet, i.e. `133;`
|
// what the command is yet, i.e. `133;`
|
||||||
@ -173,6 +184,7 @@ pub const Parser = struct {
|
|||||||
'1' => self.state = .@"1",
|
'1' => self.state = .@"1",
|
||||||
'2' => self.state = .@"2",
|
'2' => self.state = .@"2",
|
||||||
'5' => self.state = .@"5",
|
'5' => self.state = .@"5",
|
||||||
|
'7' => self.state = .@"7",
|
||||||
else => self.state = .invalid,
|
else => self.state = .invalid,
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -228,6 +240,17 @@ pub const Parser = struct {
|
|||||||
else => self.state = .invalid,
|
else => self.state = .invalid,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.@"7" => switch (c) {
|
||||||
|
';' => {
|
||||||
|
self.command = .{ .report_pwd = .{ .value = "" } };
|
||||||
|
|
||||||
|
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) {
|
.@"52" => switch (c) {
|
||||||
';' => {
|
';' => {
|
||||||
self.command = .{ .clipboard_contents = undefined };
|
self.command = .{ .clipboard_contents = undefined };
|
||||||
@ -557,6 +580,30 @@ test "OSC: get/set clipboard" {
|
|||||||
try testing.expect(std.mem.eql(u8, "?", cmd.clipboard_contents.data));
|
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: 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" {
|
test "OSC: longer than buffer" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
|
|
||||||
|
@ -495,6 +495,12 @@ pub fn Stream(comptime Handler: type) type {
|
|||||||
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
} 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"))
|
else => if (@hasDecl(T, "oscUnimplemented"))
|
||||||
try self.handler.oscUnimplemented(cmd)
|
try self.handler.oscUnimplemented(cmd)
|
||||||
else
|
else
|
||||||
|
@ -614,12 +614,24 @@ const Subprocess = struct {
|
|||||||
return pty.master;
|
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
|
// Build our subcommand
|
||||||
var cmd: Command = .{
|
var cmd: Command = .{
|
||||||
.path = self.path,
|
.path = self.path,
|
||||||
.args = self.args,
|
.args = self.args,
|
||||||
.env = &self.env,
|
.env = &self.env,
|
||||||
.cwd = self.cwd,
|
.cwd = cwd,
|
||||||
.stdin = .{ .handle = pty.slave },
|
.stdin = .{ .handle = pty.slave },
|
||||||
.stdout = .{ .handle = pty.slave },
|
.stdout = .{ .handle = pty.slave },
|
||||||
.stderr = .{ .handle = pty.slave },
|
.stderr = .{ .handle = pty.slave },
|
||||||
@ -1249,4 +1261,46 @@ const StreamHandler = struct {
|
|||||||
pub fn endOfInput(self: *StreamHandler) !void {
|
pub fn endOfInput(self: *StreamHandler) !void {
|
||||||
self.terminal.markSemanticPrompt(.command);
|
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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user