mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Generalize launching commands and use for clicking links.
This makes launching commands in Ghostty surfaces a little more general and uses that to add the ability to launch commands in new surfaces when clicking on a URI.
This commit is contained in:
132
src/Surface.zig
132
src/Surface.zig
@ -224,6 +224,8 @@ const DerivedConfig = struct {
|
|||||||
window_padding_balance: bool,
|
window_padding_balance: bool,
|
||||||
title: ?[:0]const u8,
|
title: ?[:0]const u8,
|
||||||
links: []Link,
|
links: []Link,
|
||||||
|
editor: ?[]const u8,
|
||||||
|
uri_handlers: configpkg.RepeatableURIHandler,
|
||||||
|
|
||||||
const Link = struct {
|
const Link = struct {
|
||||||
regex: oni.Regex,
|
regex: oni.Regex,
|
||||||
@ -284,6 +286,8 @@ const DerivedConfig = struct {
|
|||||||
.window_padding_balance = config.@"window-padding-balance",
|
.window_padding_balance = config.@"window-padding-balance",
|
||||||
.title = config.title,
|
.title = config.title,
|
||||||
.links = links,
|
.links = links,
|
||||||
|
.editor = if (config.editor) |editor| try alloc.dupe(u8, editor) else null,
|
||||||
|
.uri_handlers = try config.@"uri-handler".clone(alloc),
|
||||||
|
|
||||||
// Assignments happen sequentially so we have to do this last
|
// Assignments happen sequentially so we have to do this last
|
||||||
// so that the memory is captured from allocs above.
|
// so that the memory is captured from allocs above.
|
||||||
@ -2611,7 +2615,7 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
|
|||||||
.trim = false,
|
.trim = false,
|
||||||
});
|
});
|
||||||
defer self.alloc.free(str);
|
defer self.alloc.free(str);
|
||||||
try internal_os.open(self.alloc, str);
|
try self.handleURI(str);
|
||||||
},
|
},
|
||||||
|
|
||||||
._open_osc8 => {
|
._open_osc8 => {
|
||||||
@ -2619,7 +2623,7 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
|
|||||||
log.warn("failed to get URI for OSC8 hyperlink", .{});
|
log.warn("failed to get URI for OSC8 hyperlink", .{});
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
try internal_os.open(self.alloc, uri);
|
try self.handleURI(uri);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2637,6 +2641,93 @@ fn osc8URI(self: *Surface, pin: terminal.Pin) ?[]const u8 {
|
|||||||
return entry.uri.offset.ptr(page.memory)[0..entry.uri.len];
|
return entry.uri.offset.ptr(page.memory)[0..entry.uri.len];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handleURI(self: *Surface, uri: []const u8) !void {
|
||||||
|
const parsed = std.Uri.parse(uri) catch |err| {
|
||||||
|
log.warn("URI \"{s}\" was not able to be parsed: {}", .{ uri, err });
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheme = self.config.uri_handlers.getSupportedScheme(parsed.scheme) orelse {
|
||||||
|
log.warn("ignoring unsupported scheme \"{s}\"", .{parsed.scheme});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const location = self.config.uri_handlers.getLocation(scheme);
|
||||||
|
|
||||||
|
var arena = std.heap.ArenaAllocator.init(self.alloc);
|
||||||
|
const alloc = arena.allocator();
|
||||||
|
defer arena.deinit();
|
||||||
|
|
||||||
|
const command = switch (location) {
|
||||||
|
.disabled => {
|
||||||
|
log.warn("handling {s} uris has beel disabled in the config, ignoring", .{@tagName(scheme)});
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
.open => {
|
||||||
|
try internal_os.open(self.alloc, uri);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
else => switch (scheme) {
|
||||||
|
.http => command: {
|
||||||
|
const cmd = self.config.uri_handlers.getCommand(.http) orelse {
|
||||||
|
log.warn("no command is configured for http, ignoring", .{});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
break :command try std.fmt.allocPrint(alloc, "{s} {}", .{ cmd, parsed });
|
||||||
|
},
|
||||||
|
.ssh => command: {
|
||||||
|
const cmd = self.config.uri_handlers.getCommand(.ssh) orelse {
|
||||||
|
log.warn("no command is configured for ssh, ignoring", .{});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const host = parsed.host orelse {
|
||||||
|
log.warn("ssh url does not have a host!", .{});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const user = if (parsed.user) |user| try std.fmt.allocPrint(alloc, "{s}@", .{try user.toRawMaybeAlloc(alloc)}) else "";
|
||||||
|
const port = if (parsed.port) |port| try std.fmt.allocPrint(alloc, ":{d}", .{port}) else "";
|
||||||
|
break :command try std.fmt.allocPrint(
|
||||||
|
alloc,
|
||||||
|
"{s} ssh://{s}{s}{s}",
|
||||||
|
.{ cmd, user, try host.toRawMaybeAlloc(alloc), port },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
.file => command: {
|
||||||
|
const cmd = self.config.uri_handlers.getCommand(.file) orelse {
|
||||||
|
log.warn("no command is configured for file, ignoring", .{});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const path = try parsed.path.toRawMaybeAlloc(alloc);
|
||||||
|
if (path.len == 0) {
|
||||||
|
log.warn("zero length path", .{});
|
||||||
|
}
|
||||||
|
break :command try std.fmt.allocPrint(
|
||||||
|
alloc,
|
||||||
|
"{s} {s}",
|
||||||
|
.{ cmd, path },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (location == .silent) {
|
||||||
|
try internal_os.run(self.alloc, command);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@hasDecl(apprt.runtime.Surface, "runInternalCommand")) {
|
||||||
|
switch (location) {
|
||||||
|
// these should have been handled above
|
||||||
|
.disabled, .open, .silent => unreachable,
|
||||||
|
.window => try self.rt_surface.runInternalCommand(.window, command),
|
||||||
|
.tab => try self.rt_surface.runInternalCommand(.tab, command),
|
||||||
|
.split_right => try self.rt_surface.runInternalCommand(.split_right, command),
|
||||||
|
.split_down => try self.rt_surface.runInternalCommand(.split_down, command),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("runtime does not support running internal commands", .{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mousePressureCallback(
|
pub fn mousePressureCallback(
|
||||||
self: *Surface,
|
self: *Surface,
|
||||||
stage: input.MousePressureStage,
|
stage: input.MousePressureStage,
|
||||||
@ -3666,21 +3757,28 @@ fn writeScreenFile(
|
|||||||
self.alloc,
|
self.alloc,
|
||||||
path,
|
path,
|
||||||
), .unlocked),
|
), .unlocked),
|
||||||
.edit_window => {
|
else => {
|
||||||
if (@hasDecl(apprt.runtime.Surface, "openEditorWithPath"))
|
const editor = try internal_os.getEditor(self.alloc, self.config.editor);
|
||||||
try self.rt_surface.openEditorWithPath(.window, path);
|
defer self.alloc.free(editor);
|
||||||
},
|
const command = try std.fmt.allocPrint(
|
||||||
.edit_tab => {
|
self.alloc,
|
||||||
if (@hasDecl(apprt.runtime.Surface, "openEditorWithPath"))
|
"{s} {s}",
|
||||||
try self.rt_surface.openEditorWithPath(.tab, path);
|
.{ editor, path },
|
||||||
},
|
);
|
||||||
.edit_split_right => {
|
defer self.alloc.free(command);
|
||||||
if (@hasDecl(apprt.runtime.Surface, "openEditorWithPath"))
|
switch (write_action) {
|
||||||
try self.rt_surface.openEditorWithPath(.split_right, path);
|
.open, .paste => unreachable,
|
||||||
},
|
.silent => try internal_os.run(self.alloc, command),
|
||||||
.edit_split_down => {
|
else => if (@hasDecl(apprt.runtime.Surface, "runInternalCommand")) switch (write_action) {
|
||||||
if (@hasDecl(apprt.runtime.Surface, "openEditorWithPath"))
|
.open, .paste, .silent => unreachable,
|
||||||
try self.rt_surface.openEditorWithPath(.split_down, path);
|
.edit_window => try self.rt_surface.runInternalCommand(.window, command),
|
||||||
|
.edit_tab => try self.rt_surface.runInternalCommand(.tab, command),
|
||||||
|
.edit_split_right => try self.rt_surface.runInternalCommand(.split_right, command),
|
||||||
|
.edit_split_down => try self.rt_surface.runInternalCommand(.split_down, command),
|
||||||
|
} else {
|
||||||
|
log.warn("runtime does not support running internal commands", .{});
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1783,7 +1783,7 @@ pub const CAPI = struct {
|
|||||||
|
|
||||||
/// Request that the surface split in the given direction.
|
/// Request that the surface split in the given direction.
|
||||||
export fn ghostty_surface_split(ptr: *Surface, direction: apprt.SplitDirection) void {
|
export fn ghostty_surface_split(ptr: *Surface, direction: apprt.SplitDirection) void {
|
||||||
ptr.newSplit(direction) catch {};
|
ptr.newSplit(direction, .{}) catch {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Focus on the next split (if any).
|
/// Focus on the next split (if any).
|
||||||
|
@ -2018,7 +2018,8 @@ fn translateMods(state: c.GdkModifierType) input.Mods {
|
|||||||
return mods;
|
return mods;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn openEditorWithPath(
|
/// Run the specified command in a new window, tab or a split.
|
||||||
|
pub fn runInternalCommand(
|
||||||
self: *Surface,
|
self: *Surface,
|
||||||
location: enum {
|
location: enum {
|
||||||
window,
|
window,
|
||||||
@ -2026,23 +2027,23 @@ pub fn openEditorWithPath(
|
|||||||
split_right,
|
split_right,
|
||||||
split_down,
|
split_down,
|
||||||
},
|
},
|
||||||
path: []const u8,
|
command: []const u8,
|
||||||
) !void {
|
) !void {
|
||||||
const alloc = self.app.core_app.alloc;
|
const gpa_alloc = self.app.core_app.alloc;
|
||||||
const config = try alloc.create(configpkg.Config);
|
|
||||||
config.* = try self.app.config.clone(alloc);
|
|
||||||
|
|
||||||
const editor = try internal_os.getEditor(alloc, config);
|
const config = try gpa_alloc.create(configpkg.Config);
|
||||||
defer alloc.free(editor);
|
config.* = try self.app.config.clone(gpa_alloc);
|
||||||
|
errdefer config.deinit();
|
||||||
|
|
||||||
config.command = try std.fmt.allocPrint(
|
const arena_alloc = config._arena.?.allocator();
|
||||||
config._arena.?.allocator(),
|
|
||||||
"{s} {s}",
|
config.command = try arena_alloc.dupe(u8, command);
|
||||||
.{ editor, path },
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (location) {
|
switch (location) {
|
||||||
.window => try self.app.newWindow(.{ .parent = &self.core_surface, .config = config }),
|
.window => try self.app.newWindow(.{
|
||||||
|
.parent = &self.core_surface,
|
||||||
|
.config = config,
|
||||||
|
}),
|
||||||
.tab => try self.newTab(.{ .config = config }),
|
.tab => try self.newTab(.{ .config = config }),
|
||||||
.split_right => try self.newSplit(.right, .{ .config = config }),
|
.split_right => try self.newSplit(.right, .{ .config = config }),
|
||||||
.split_down => try self.newSplit(.down, .{ .config = config }),
|
.split_down => try self.newSplit(.down, .{ .config = config }),
|
||||||
|
@ -12,7 +12,7 @@ pub const fish_completions = comptimeGenerateFishCompletions();
|
|||||||
|
|
||||||
fn comptimeGenerateFishCompletions() []const u8 {
|
fn comptimeGenerateFishCompletions() []const u8 {
|
||||||
comptime {
|
comptime {
|
||||||
@setEvalBranchQuota(16000);
|
@setEvalBranchQuota(17000);
|
||||||
var counter = std.io.countingWriter(std.io.null_writer);
|
var counter = std.io.countingWriter(std.io.null_writer);
|
||||||
try writeFishCompletions(&counter.writer());
|
try writeFishCompletions(&counter.writer());
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ pub const RepeatableFontVariation = Config.RepeatableFontVariation;
|
|||||||
pub const RepeatableString = Config.RepeatableString;
|
pub const RepeatableString = Config.RepeatableString;
|
||||||
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
|
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
|
||||||
pub const WindowPaddingColor = Config.WindowPaddingColor;
|
pub const WindowPaddingColor = Config.WindowPaddingColor;
|
||||||
|
pub const RepeatableURIHandler = Config.RepeatableURIHandler;
|
||||||
|
|
||||||
// Alternate APIs
|
// Alternate APIs
|
||||||
pub const CAPI = @import("config/CAPI.zig");
|
pub const CAPI = @import("config/CAPI.zig");
|
||||||
|
@ -460,24 +460,107 @@ palette: Palette = .{},
|
|||||||
command: ?[]const u8 = null,
|
command: ?[]const u8 = null,
|
||||||
|
|
||||||
/// A command to use to open/edit text files in a terminal window (using
|
/// A command to use to open/edit text files in a terminal window (using
|
||||||
/// something like Helix, Flow, Vim, NeoVim, Emacs, or Nano). If this is not set,
|
/// something like Helix, Flow, Vim, NeoVim, Emacs, or Nano). If this is not
|
||||||
/// Ghostty will check the `EDITOR` environment variable for the command. If
|
/// set, Ghostty will check the `EDITOR` environment variable for the command.
|
||||||
/// the `EDITOR` environment variable is not set, Ghostty will fall back to `vi`
|
/// If the `EDITOR` environment variable is not set, Ghostty will fall back to
|
||||||
/// (similar to how many Linux/Unix systems operate).
|
/// `vi` (similar to how many Linux/Unix systems operate).
|
||||||
///
|
|
||||||
/// This command will be used to open/edit files when Ghostty receives a signal
|
|
||||||
/// from the operating system to open a file. Currently implemented on the GTK
|
|
||||||
/// runtime only.
|
|
||||||
///
|
///
|
||||||
/// The command may contain additional arguments besides the path to the
|
/// The command may contain additional arguments besides the path to the
|
||||||
/// editor's binary. The files that are to be opened will be added to the end of
|
/// editor's binary. The files that are to be opened will be added to the end of
|
||||||
/// the command. For example, if `editor` was set to `emacs -nx` and you tried
|
/// the command. For example, if `editor` was set to `emacs -nw` and you tried
|
||||||
/// to open `README` and `hello.c` in your home directory, the final command
|
/// to open `README` and `hello.c` in your home directory, the final command
|
||||||
/// would look like
|
/// would look like:
|
||||||
///
|
///
|
||||||
/// emacs -nx /home/user/README /home/user/hello.c
|
/// emacs -nw /home/user/README /home/user/hello.c
|
||||||
editor: ?[]const u8 = null,
|
editor: ?[]const u8 = null,
|
||||||
|
|
||||||
|
/// This setting controls how Ghostty handles clicking on a URI. Ghostty
|
||||||
|
/// detects URIs in the terminal in two ways. First by using a regular
|
||||||
|
/// expression to scan the output (see the `link` configuration setting for more
|
||||||
|
/// information). Second, commands can use the OSC 8 protocol to tell Ghostty
|
||||||
|
/// about URIs (much like how <a> is used in HTML).
|
||||||
|
///
|
||||||
|
/// Entries look like:
|
||||||
|
///
|
||||||
|
/// uri-handler = <scheme>:<location>:<command>
|
||||||
|
///
|
||||||
|
/// Entries may be repeated, but later entries will override earlier ones. The handlers
|
||||||
|
/// can be reset to the default with an empty entry.
|
||||||
|
///
|
||||||
|
/// uri-handler =
|
||||||
|
///
|
||||||
|
/// The default is:
|
||||||
|
///
|
||||||
|
/// uri-handler = http:open:
|
||||||
|
/// uri-handler = ssh:open:
|
||||||
|
/// uri-handler = file:open:
|
||||||
|
///
|
||||||
|
/// The following schemes are supported:
|
||||||
|
///
|
||||||
|
/// * `http` - This is also used for `https` URIs.
|
||||||
|
/// * `ssh`
|
||||||
|
/// * `file`
|
||||||
|
///
|
||||||
|
/// The following location are supported:
|
||||||
|
///
|
||||||
|
/// * `open` - Use an OS-provided utility to launch the OS's default handler
|
||||||
|
/// for that URI. On macOS the `open` command is used. On Linux the
|
||||||
|
/// `xdg-open` command is used. On Windows the `url.dll` library is used.
|
||||||
|
/// The `command` part of the configuration entry is ignored.
|
||||||
|
/// * `disabled` - Clicks on URIs with this scheme will be ignored.
|
||||||
|
/// * `silent` - The command will be run "in the background" without opening
|
||||||
|
/// a new window, tab, or split. Ths could be useful for running a command
|
||||||
|
/// that passes the URI to a long running command like an editor that has a
|
||||||
|
/// client-server model of operation.
|
||||||
|
/// * `window` - The command will open in a new window. (GTK only).
|
||||||
|
/// * `tab` - The command will open in a new tab. (GTK only).
|
||||||
|
/// * `split_right` - The focused surface will split horizontally and the
|
||||||
|
/// command will run in the right split. (GTK only).
|
||||||
|
/// * `split_down` - The focused surface will split vertically and the command
|
||||||
|
/// will run in the lower split. (GTK only).
|
||||||
|
///
|
||||||
|
/// Clicking on unparsable (by Zig's URI parser) URIs will be silently ignored.
|
||||||
|
///
|
||||||
|
/// For `ssh` URIs, the command that is actually run is constructed by parsing
|
||||||
|
/// the URI and then building a command that looks like this:
|
||||||
|
///
|
||||||
|
/// <command> ssh://(<user>@)<host>(:<port)
|
||||||
|
///
|
||||||
|
/// The `user` and `port` parts will be left off if they were not in the
|
||||||
|
/// original URI. If the URI does not have a host clicking on the URI will be
|
||||||
|
/// silently ignored.
|
||||||
|
///
|
||||||
|
/// For `file` URIs, the `path` part of the URI is parsed out of the URI and a
|
||||||
|
/// command that looks like this is constructed:
|
||||||
|
///
|
||||||
|
/// <command> <path>
|
||||||
|
///
|
||||||
|
/// Note that `file` URIs cannot be used to access files located on a remote
|
||||||
|
/// system as `file` URIs do not include hostname information.
|
||||||
|
@"uri-handler": RepeatableURIHandler = .{},
|
||||||
|
|
||||||
|
/// Match a regular expression against the terminal text and associate clicking
|
||||||
|
/// it with an action. This can be used to match URLs, file paths, etc. Actions
|
||||||
|
/// can be "opened" using commands specified by the `uri-handlers` or by
|
||||||
|
/// executing any arbitrary binding action.
|
||||||
|
///
|
||||||
|
/// Links that are configured earlier take precedence over links that are
|
||||||
|
/// configured later.
|
||||||
|
///
|
||||||
|
/// A default link that matches a URL and opens it in the system opener always
|
||||||
|
/// exists. This can be disabled using `link-url`.
|
||||||
|
///
|
||||||
|
/// TODO: This can't currently be set!
|
||||||
|
link: RepeatableLink = .{},
|
||||||
|
|
||||||
|
/// Enable URL matching. URLs are matched on hover with control (Linux) or
|
||||||
|
/// super (macOS) pressed and open using the default system application for
|
||||||
|
/// the linked URL.
|
||||||
|
///
|
||||||
|
/// The URL matcher is always lowest priority of any configured links (see
|
||||||
|
/// `link`). If you want to customize URL matching, use `link` and disable this.
|
||||||
|
@"link-url": bool = true,
|
||||||
|
|
||||||
/// If true, keep the terminal open after the command exits. Normally, the
|
/// If true, keep the terminal open after the command exits. Normally, the
|
||||||
/// terminal window closes when the running command (such as a shell) exits.
|
/// terminal window closes when the running command (such as a shell) exits.
|
||||||
/// With this true, the terminal window will stay open until any keypress is
|
/// With this true, the terminal window will stay open until any keypress is
|
||||||
@ -516,28 +599,6 @@ editor: ?[]const u8 = null,
|
|||||||
/// This can be changed at runtime but will only affect new terminal surfaces.
|
/// This can be changed at runtime but will only affect new terminal surfaces.
|
||||||
@"scrollback-limit": u32 = 10_000_000, // 10MB
|
@"scrollback-limit": u32 = 10_000_000, // 10MB
|
||||||
|
|
||||||
/// Match a regular expression against the terminal text and associate clicking
|
|
||||||
/// it with an action. This can be used to match URLs, file paths, etc. Actions
|
|
||||||
/// can be opening using the system opener (i.e. `open` or `xdg-open`) or
|
|
||||||
/// executing any arbitrary binding action.
|
|
||||||
///
|
|
||||||
/// Links that are configured earlier take precedence over links that are
|
|
||||||
/// configured later.
|
|
||||||
///
|
|
||||||
/// A default link that matches a URL and opens it in the system opener always
|
|
||||||
/// exists. This can be disabled using `link-url`.
|
|
||||||
///
|
|
||||||
/// TODO: This can't currently be set!
|
|
||||||
link: RepeatableLink = .{},
|
|
||||||
|
|
||||||
/// Enable URL matching. URLs are matched on hover with control (Linux) or
|
|
||||||
/// super (macOS) pressed and open using the default system application for
|
|
||||||
/// the linked URL.
|
|
||||||
///
|
|
||||||
/// The URL matcher is always lowest priority of any configured links (see
|
|
||||||
/// `link`). If you want to customize URL matching, use `link` and disable this.
|
|
||||||
@"link-url": bool = true,
|
|
||||||
|
|
||||||
/// Start new windows in fullscreen. This setting applies to new windows and
|
/// Start new windows in fullscreen. This setting applies to new windows and
|
||||||
/// does not apply to tabs, splits, etc. However, this setting will apply to all
|
/// does not apply to tabs, splits, etc. However, this setting will apply to all
|
||||||
/// new windows, not just the first one.
|
/// new windows, not just the first one.
|
||||||
@ -4346,3 +4407,188 @@ test "test entryFormatter" {
|
|||||||
try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||||
try std.testing.expectEqualStrings("a = 584y 49w 23h 34m 33s 709ms 551µs 615ns\n", buf.items);
|
try std.testing.expectEqualStrings("a = 584y 49w 23h 34m 33s 709ms 551µs 615ns\n", buf.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// See `uri-handler` documentation
|
||||||
|
pub const RepeatableURIHandler = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
const Scheme = enum {
|
||||||
|
// `http` handles both `http` and `https`
|
||||||
|
http,
|
||||||
|
ssh,
|
||||||
|
file,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Location = enum {
|
||||||
|
/// Use OS utilities to launch default handler for a URL.
|
||||||
|
open,
|
||||||
|
/// Clicks on URLs with this scheme will be ignored.
|
||||||
|
disabled,
|
||||||
|
/// Commands will be run silently in the background.
|
||||||
|
silent,
|
||||||
|
/// A new window will be opened to run the command.
|
||||||
|
window,
|
||||||
|
/// A new tab will be opened to run the command.
|
||||||
|
tab,
|
||||||
|
/// The focused surface will be split right to run the command.
|
||||||
|
split_right,
|
||||||
|
/// The focused surface will be split down to run the command.
|
||||||
|
split_down,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Entry = struct {
|
||||||
|
location: Location = .open,
|
||||||
|
command: ?[]const u8 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const count = @typeInfo(Scheme).Enum.fields.len;
|
||||||
|
|
||||||
|
entries: [count]Entry = [_]Entry{.{}} ** count,
|
||||||
|
|
||||||
|
/// Return an enum if this is a scheme that we support, otherwise null.
|
||||||
|
/// `https` is converted to `http`
|
||||||
|
pub fn getSupportedScheme(_: *Self, scheme: []const u8) ?Scheme {
|
||||||
|
if (std.mem.eql(u8, "https", scheme)) return .http;
|
||||||
|
return std.meta.stringToEnum(Scheme, scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getLocation(self: *Self, scheme: Scheme) Location {
|
||||||
|
const index = @intFromEnum(scheme);
|
||||||
|
return self.entries[index].location;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getCommand(self: *Self, scheme: Scheme) ?[]const u8 {
|
||||||
|
const index = @intFromEnum(scheme);
|
||||||
|
return self.entries[index].command;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void {
|
||||||
|
const value = input orelse return error.ValueRequired;
|
||||||
|
|
||||||
|
// Empty value resets the list
|
||||||
|
if (value.len == 0) {
|
||||||
|
self.entries = [_]Entry{.{}} ** count;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var it = std.mem.splitScalar(u8, value, ':');
|
||||||
|
|
||||||
|
const scheme = std.meta.stringToEnum(Scheme, it.next() orelse return error.ValueRequired) orelse return error.ValueRequired;
|
||||||
|
const location = std.meta.stringToEnum(Location, it.next() orelse return error.ValueRequired) orelse return error.ValueRequired;
|
||||||
|
const command = command: {
|
||||||
|
const command = it.rest();
|
||||||
|
if (command.len == 0) break :command null;
|
||||||
|
break :command try alloc.dupe(u8, command);
|
||||||
|
};
|
||||||
|
|
||||||
|
const index = @intFromEnum(scheme);
|
||||||
|
self.entries[index] = .{
|
||||||
|
.location = if (command == null) .open else location,
|
||||||
|
.command = command,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deep copy of the struct. Required by Config.
|
||||||
|
pub fn clone(self: *const Self, alloc: Allocator) !Self {
|
||||||
|
var new: Self = .{};
|
||||||
|
|
||||||
|
for (self.entries, 0..) |entry, index| {
|
||||||
|
if (entry.command) |command| {
|
||||||
|
new.entries[index] = .{
|
||||||
|
.location = entry.location,
|
||||||
|
.command = try alloc.dupe(u8, command),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare if two of our value are requal. Required by Config.
|
||||||
|
pub fn equal(self: Self, other: Self) bool {
|
||||||
|
for (self.entries, other.entries) |a, b| {
|
||||||
|
if (a.location != b.location) return false;
|
||||||
|
if (a.command == null and b.command == null) continue;
|
||||||
|
if (a.command == null) return false;
|
||||||
|
if (b.command == null) return false;
|
||||||
|
if (!std.mem.eql(u8, a.command.?, b.command.?)) return false;
|
||||||
|
} else return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used by Formatter
|
||||||
|
pub fn formatEntry(self: Self, formatter: anytype) !void {
|
||||||
|
for (self.entries, 0..) |value, index| {
|
||||||
|
const scheme: Scheme = @enumFromInt(index);
|
||||||
|
// this "should" be enough as most operating systems have limits on
|
||||||
|
// the length of commands that they will execute anyway
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
var fbs = std.io.fixedBufferStream(&buf);
|
||||||
|
const writer = fbs.writer();
|
||||||
|
writer.print("{s}:{s}:{s}", .{
|
||||||
|
@tagName(scheme),
|
||||||
|
@tagName(value.location),
|
||||||
|
if (value.command) |command| command else "",
|
||||||
|
}) catch return error.OutOfMemory;
|
||||||
|
try formatter.formatEntry([]const u8, fbs.getWritten());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseCLI" {
|
||||||
|
const testing = std.testing;
|
||||||
|
var arena = ArenaAllocator.init(testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const alloc = arena.allocator();
|
||||||
|
|
||||||
|
var list: Self = .{} ** count;
|
||||||
|
|
||||||
|
try list.parseCLI(alloc, "ssh:window:ssh");
|
||||||
|
try list.parseCLI(alloc, "file:split_right:hx");
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(Location, .open), list.entries[@intFromEnum(@as(Scheme, .http))].location);
|
||||||
|
try testing.expect(list.entries[@intFromEnum(@as(Scheme, .http))].command == null);
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(Location, .window), list.entries[@intFromEnum(@as(Scheme, .ssh))].location);
|
||||||
|
try testing.expect(list.entries[@intFromEnum(@as(Scheme, .ssh))].command != null);
|
||||||
|
try testing.expectEqualStrings("ssh", list.entries[@intFromEnum(@as(Scheme, .ssh))].command.?);
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(Location, .split_right), list.entries[@intFromEnum(@as(Scheme, .file))].location);
|
||||||
|
try testing.expect(list.entries[@intFromEnum(@as(Scheme, .file))].command != null);
|
||||||
|
try testing.expectEqualStrings("hx", list.entries[@intFromEnum(@as(Scheme, .file))].command.?);
|
||||||
|
|
||||||
|
try list.parseCLI(alloc, "");
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(Location, .open), list.entries[@intFromEnum(@as(Scheme, .http))].location);
|
||||||
|
try testing.expect(list.entries[@intFromEnum(@as(Scheme, .http))].command == null);
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(Location, .open), list.entries[@intFromEnum(@as(Scheme, .ssh))].location);
|
||||||
|
try testing.expect(list.entries[@intFromEnum(@as(Scheme, .ssh))].command == null);
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(Location, .open), list.entries[@intFromEnum(@as(Scheme, .file))].location);
|
||||||
|
try testing.expect(list.entries[@intFromEnum(@as(Scheme, .file))].command == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatEntry 1" {
|
||||||
|
const testing = std.testing;
|
||||||
|
var buf = std.ArrayList(u8).init(testing.allocator);
|
||||||
|
defer buf.deinit();
|
||||||
|
|
||||||
|
var list: Self = .{};
|
||||||
|
try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||||
|
try std.testing.expectEqualSlices(u8, "a = http:open:\na = ssh:open:\na = file:open:\n", buf.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatEntry 2" {
|
||||||
|
const testing = std.testing;
|
||||||
|
var buf = std.ArrayList(u8).init(testing.allocator);
|
||||||
|
defer buf.deinit();
|
||||||
|
|
||||||
|
var arena = ArenaAllocator.init(testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const alloc = arena.allocator();
|
||||||
|
|
||||||
|
var list: Self = .{};
|
||||||
|
try list.parseCLI(alloc, "ssh:window:ssh");
|
||||||
|
try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||||
|
try std.testing.expectEqualSlices(u8, "a = http:open:\na = ssh:window:ssh\na = file:open:\n", buf.items);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -232,13 +232,23 @@ pub const Action = union(enum) {
|
|||||||
///
|
///
|
||||||
/// - `paste`: Paste the file path into the terminal.
|
/// - `paste`: Paste the file path into the terminal.
|
||||||
/// - `open`: Open the file in the default OS editor for text files.
|
/// - `open`: Open the file in the default OS editor for text files.
|
||||||
/// - `edit_window`: Create a new window, and open the file in your editor.
|
/// - `silent`: Run a command "in the background" to process the file. The
|
||||||
/// - `edit_tab`: Create a new tab, and open the file in your editor.
|
/// command is constructed using the `editor` configuration setting or
|
||||||
/// - `edit_split_right`: Create a new split right, and open the file in your editor.
|
/// your `EDITOR` environment variable.
|
||||||
/// - `edit_split_down`: Create a new split down, and open the file in your editor.
|
/// - `edit_window`: Create a new window, and open the file in your
|
||||||
///
|
/// editor. The command is constructed using the `editor` configuration
|
||||||
/// `edit_window`, `edit_tab`, `edit_split_right`, and `edit_split_down` are
|
/// setting or your `EDITOR` environment variable. (GTK only.)
|
||||||
/// supported on GTK only.
|
/// - `edit_tab`: Create a new tab, and open the file in your editor. The
|
||||||
|
/// command is constructed using the `editor` configuration setting or
|
||||||
|
/// your `EDITOR` environment variable. (GTK only.)
|
||||||
|
/// - `edit_split_right`: Create a new split right, and open the file
|
||||||
|
/// in your editor. The command is constructed using the `editor`
|
||||||
|
/// configuration setting or your `EDITOR` environment variable. (GTK
|
||||||
|
/// only.)
|
||||||
|
/// - `edit_split_down`: Create a new split down, and open the file
|
||||||
|
/// in your editor. The command is constructed using the `editor`
|
||||||
|
/// configuration setting or your `EDITOR` environment variable. (GTK
|
||||||
|
/// only.)
|
||||||
///
|
///
|
||||||
/// See the configuration setting `editor` for more information on how
|
/// See the configuration setting `editor` for more information on how
|
||||||
/// Ghostty determines your editor.
|
/// Ghostty determines your editor.
|
||||||
@ -249,13 +259,23 @@ pub const Action = union(enum) {
|
|||||||
///
|
///
|
||||||
/// - `paste`: Paste the file path into the terminal.
|
/// - `paste`: Paste the file path into the terminal.
|
||||||
/// - `open`: Open the file in the default OS editor for text files.
|
/// - `open`: Open the file in the default OS editor for text files.
|
||||||
/// - `edit_window`: Create a new window, and open the file in your editor.
|
/// - `silent`: Run a command "in the background" to process the file. The
|
||||||
/// - `edit_tab`: Create a new tab, and open the file in your editor.
|
/// command is constructed using the `editor` configuration setting or
|
||||||
/// - `edit_split_right`: Create a new split right, and open the file in your editor.
|
/// your `EDITOR` environment variable.
|
||||||
/// - `edit_split_down`: Create a new split down, and open the file in your editor.
|
/// - `edit_window`: Create a new window, and open the file in your
|
||||||
///
|
/// editor. The command is constructed using the `editor` configuration
|
||||||
/// `edit_window`, `edit_tab`, `edit_split_right`, and `edit_split_down` are
|
/// setting or your `EDITOR` environment variable. (GTK only.)
|
||||||
/// supported on GTK only.
|
/// - `edit_tab`: Create a new tab, and open the file in your editor. The
|
||||||
|
/// command is constructed using the `editor` configuration setting or
|
||||||
|
/// your `EDITOR` environment variable. (GTK only.)
|
||||||
|
/// - `edit_split_right`: Create a new split right, and open the file
|
||||||
|
/// in your editor. The command is constructed using the `editor`
|
||||||
|
/// configuration setting or your `EDITOR` environment variable. (GTK
|
||||||
|
/// only.)
|
||||||
|
/// - `edit_split_down`: Create a new split down, and open the file
|
||||||
|
/// in your editor. The command is constructed using the `editor`
|
||||||
|
/// configuration setting or your `EDITOR` environment variable. (GTK
|
||||||
|
/// only.)
|
||||||
///
|
///
|
||||||
/// See the configuration setting `editor` for more information on how
|
/// See the configuration setting `editor` for more information on how
|
||||||
/// Ghostty determines your editor.
|
/// Ghostty determines your editor.
|
||||||
@ -267,13 +287,23 @@ pub const Action = union(enum) {
|
|||||||
///
|
///
|
||||||
/// - `paste`: Paste the file path into the terminal.
|
/// - `paste`: Paste the file path into the terminal.
|
||||||
/// - `open`: Open the file in the default OS editor for text files.
|
/// - `open`: Open the file in the default OS editor for text files.
|
||||||
/// - `edit_window`: Create a new window, and open the file in your editor.
|
/// - `silent`: Run a command "in the background" to process the file. The
|
||||||
/// - `edit_tab`: Create a new tab, and open the file in your editor.
|
/// command is constructed using the `editor` configuration setting or
|
||||||
/// - `edit_split_right`: Create a new split right, and open the file in your editor.
|
/// your `EDITOR` environment variable.
|
||||||
/// - `edit_split_down`: Create a new split down, and open the file in your editor.
|
/// - `edit_window`: Create a new window, and open the file in your
|
||||||
///
|
/// editor. The command is constructed using the `editor` configuration
|
||||||
/// `edit_window`, `edit_tab`, `edit_split_right`, and `edit_split_down` are
|
/// setting or your `EDITOR` environment variable. (GTK only.)
|
||||||
/// supported on GTK only.
|
/// - `edit_tab`: Create a new tab, and open the file in your editor. The
|
||||||
|
/// command is constructed using the `editor` configuration setting or
|
||||||
|
/// your `EDITOR` environment variable. (GTK only.)
|
||||||
|
/// - `edit_split_right`: Create a new split right, and open the file
|
||||||
|
/// in your editor. The command is constructed using the `editor`
|
||||||
|
/// configuration setting or your `EDITOR` environment variable. (GTK
|
||||||
|
/// only.)
|
||||||
|
/// - `edit_split_down`: Create a new split down, and open the file
|
||||||
|
/// in your editor. The command is constructed using the `editor`
|
||||||
|
/// configuration setting or your `EDITOR` environment variable. (GTK
|
||||||
|
/// only.)
|
||||||
///
|
///
|
||||||
/// See the configuration setting `editor` for more information on how
|
/// See the configuration setting `editor` for more information on how
|
||||||
/// Ghostty determines your editor.
|
/// Ghostty determines your editor.
|
||||||
@ -401,6 +431,7 @@ pub const Action = union(enum) {
|
|||||||
pub const WriteScreenAction = enum {
|
pub const WriteScreenAction = enum {
|
||||||
paste,
|
paste,
|
||||||
open,
|
open,
|
||||||
|
silent,
|
||||||
edit_window,
|
edit_window,
|
||||||
edit_tab,
|
edit_tab,
|
||||||
edit_split_right,
|
edit_split_right,
|
||||||
|
@ -20,8 +20,8 @@ action: Action,
|
|||||||
highlight: Highlight,
|
highlight: Highlight,
|
||||||
|
|
||||||
pub const Action = union(enum) {
|
pub const Action = union(enum) {
|
||||||
/// Open the full matched value using the default open program.
|
/// Use the `uri-handler` configuation setting to handle clicking on the
|
||||||
/// For example, on macOS this is "open" and on Linux this is "xdg-open".
|
/// link.
|
||||||
open: void,
|
open: void,
|
||||||
|
|
||||||
/// Open the OSC8 hyperlink under the mouse position. _-prefixed means
|
/// Open the OSC8 hyperlink under the mouse position. _-prefixed means
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const Config = @import("../config.zig").Config;
|
|
||||||
|
|
||||||
pub fn getEditor(alloc: std.mem.Allocator, config: *const Config) ![]const u8 {
|
pub fn getEditor(alloc: std.mem.Allocator, editor: ?[]const u8) ![]const u8 {
|
||||||
// figure out what our editor is
|
// figure out what our editor is
|
||||||
if (config.editor) |editor| return try alloc.dupe(u8, editor);
|
if (editor) |e| return try alloc.dupe(u8, e);
|
||||||
switch (builtin.os.tag) {
|
switch (builtin.os.tag) {
|
||||||
.windows => {
|
.windows => {
|
||||||
if (std.process.getenvW(std.unicode.utf8ToUtf16LeStringLiteral("EDITOR"))) |win_editor| {
|
if (std.process.getenvW(std.unicode.utf8ToUtf16LeStringLiteral("EDITOR"))) |win_editor| {
|
||||||
return try std.unicode.utf16leToUtf8Alloc(alloc, win_editor);
|
return try std.unicode.utf16leToUtf8Alloc(alloc, win_editor);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
else => if (std.posix.getenv("EDITOR")) |editor| return alloc.dupe(u8, editor),
|
else => if (std.posix.getenv("EDITOR")) |e| return alloc.dupe(u8, e),
|
||||||
}
|
}
|
||||||
return alloc.dupe(u8, "vi");
|
return alloc.dupe(u8, "vi");
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ pub const ensureLocale = locale.ensureLocale;
|
|||||||
pub const macosVersionAtLeast = macos_version.macosVersionAtLeast;
|
pub const macosVersionAtLeast = macos_version.macosVersionAtLeast;
|
||||||
pub const clickInterval = mouse.clickInterval;
|
pub const clickInterval = mouse.clickInterval;
|
||||||
pub const open = openpkg.open;
|
pub const open = openpkg.open;
|
||||||
|
pub const run = openpkg.run;
|
||||||
pub const pipe = pipepkg.pipe;
|
pub const pipe = pipepkg.pipe;
|
||||||
pub const resourcesDir = resourcesdir.resourcesDir;
|
pub const resourcesDir = resourcesdir.resourcesDir;
|
||||||
pub const getEditor = editor.getEditor;
|
pub const getEditor = editor.getEditor;
|
||||||
|
@ -1,23 +1,9 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
const isFlatpak = @import("flatpak.zig").isFlatpak;
|
||||||
|
|
||||||
/// Open a URL in the default handling application.
|
fn execute(alloc: Allocator, argv: []const []const u8, comptime wait: bool) !void {
|
||||||
///
|
|
||||||
/// Any output on stderr is logged as a warning in the application logs.
|
|
||||||
/// Output on stdout is ignored.
|
|
||||||
pub fn open(alloc: Allocator, url: []const u8) !void {
|
|
||||||
// Some opener commands terminate after opening (macOS open) and some do not
|
|
||||||
// (xdg-open). For those which do not terminate, we do not want to wait for
|
|
||||||
// the process to exit to collect stderr.
|
|
||||||
const argv, const wait = switch (builtin.os.tag) {
|
|
||||||
.linux => .{ &.{ "xdg-open", url }, false },
|
|
||||||
.macos => .{ &.{ "open", url }, true },
|
|
||||||
.windows => .{ &.{ "rundll32", "url.dll,FileProtocolHandler", url }, false },
|
|
||||||
.ios => return error.Unimplemented,
|
|
||||||
else => @compileError("unsupported OS"),
|
|
||||||
};
|
|
||||||
|
|
||||||
var exe = std.process.Child.init(argv, alloc);
|
var exe = std.process.Child.init(argv, alloc);
|
||||||
|
|
||||||
if (comptime wait) {
|
if (comptime wait) {
|
||||||
@ -47,3 +33,67 @@ pub fn open(alloc: Allocator, url: []const u8) !void {
|
|||||||
if (stderr.items.len > 0) std.log.err("open stderr={s}", .{stderr.items});
|
if (stderr.items.len > 0) std.log.err("open stderr={s}", .{stderr.items});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run a command using the system shell to simplify parsing the command line
|
||||||
|
pub fn run(gpa_alloc: Allocator, cmd: []const u8) !void {
|
||||||
|
var arena = std.heap.ArenaAllocator.init(gpa_alloc);
|
||||||
|
const arena_alloc = arena.allocator();
|
||||||
|
defer arena.deinit();
|
||||||
|
|
||||||
|
const argv, const wait = switch (builtin.os.tag) {
|
||||||
|
.ios => return error.Unimplemented,
|
||||||
|
.windows => result: {
|
||||||
|
// We run our shell wrapped in `cmd.exe` so that we don't have
|
||||||
|
// to parse the command line ourselves if it has arguments.
|
||||||
|
|
||||||
|
// Note we don't free any of the memory below since it is
|
||||||
|
// allocated in the arena.
|
||||||
|
var list = std.ArrayList([]const u8).init(arena_alloc);
|
||||||
|
const windir = try std.process.getEnvVarOwned(arena_alloc, "WINDIR");
|
||||||
|
const shell = try std.fs.path.join(arena_alloc, &[_][]const u8{
|
||||||
|
windir,
|
||||||
|
"System32",
|
||||||
|
"cmd.exe",
|
||||||
|
});
|
||||||
|
|
||||||
|
try list.append(shell);
|
||||||
|
try list.append("/C");
|
||||||
|
try list.append(cmd);
|
||||||
|
break :result .{ try list.toOwnedSlice(), false };
|
||||||
|
},
|
||||||
|
else => result: {
|
||||||
|
// We run our shell wrapped in `/bin/sh` so that we don't have
|
||||||
|
// to parse the command line ourselves if it has arguments.
|
||||||
|
// Additionally, some environments (NixOS, I found) use /bin/sh
|
||||||
|
// to setup some environment variables that are important to
|
||||||
|
// have set.
|
||||||
|
var list = std.ArrayList([]const u8).init(arena_alloc);
|
||||||
|
try list.append("/bin/sh");
|
||||||
|
if (isFlatpak()) try list.append("-l");
|
||||||
|
try list.append("-c");
|
||||||
|
try list.append(cmd);
|
||||||
|
break :result .{ try list.toOwnedSlice(), true };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try execute(gpa_alloc, argv, wait);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open a URL in the default handling application.
|
||||||
|
///
|
||||||
|
/// Any output on stderr is logged as a warning in the application logs.
|
||||||
|
/// Output on stdout is ignored.
|
||||||
|
pub fn open(gpa_alloc: Allocator, url: []const u8) !void {
|
||||||
|
// Some opener commands terminate after opening (macOS open) and some do not
|
||||||
|
// (xdg-open). For those which do not terminate, we do not want to wait for
|
||||||
|
// the process to exit to collect stderr.
|
||||||
|
const argv, const wait = switch (builtin.os.tag) {
|
||||||
|
.linux => .{ &.{ "xdg-open", url }, false },
|
||||||
|
.macos => .{ &.{ "open", url }, true },
|
||||||
|
.windows => .{ &.{ "rundll32", "url.dll,FileProtocolHandler", url }, false },
|
||||||
|
.ios => return error.Unimplemented,
|
||||||
|
else => @compileError("unsupported OS"),
|
||||||
|
};
|
||||||
|
|
||||||
|
try execute(gpa_alloc, argv, wait);
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user