Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions include/ghostty.h
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,12 @@ typedef struct {
uintptr_t len;
} ghostty_action_open_url_s;

// apprt.action.NewTabCommand.C
typedef struct {
const char* command;
uintptr_t len;
} ghostty_action_new_tab_with_command_s;

// apprt.action.CloseTabMode
typedef enum {
GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS,
Expand Down Expand Up @@ -949,6 +955,7 @@ typedef enum {
GHOSTTY_ACTION_SEARCH_SELECTED,
GHOSTTY_ACTION_READONLY,
GHOSTTY_ACTION_COPY_TITLE_TO_CLIPBOARD,
GHOSTTY_ACTION_NEW_TAB_WITH_COMMAND,
} ghostty_action_tag_e;

typedef union {
Expand Down Expand Up @@ -990,6 +997,7 @@ typedef union {
ghostty_action_search_total_s search_total;
ghostty_action_search_selected_s search_selected;
ghostty_action_readonly_e readonly;
ghostty_action_new_tab_with_command_s new_tab_with_command;
} ghostty_action_u;

typedef struct {
Expand Down
51 changes: 51 additions & 0 deletions macos/Sources/Ghostty/Ghostty.App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,9 @@ extension Ghostty {
case GHOSTTY_ACTION_NEW_TAB:
newTab(app, target: target)

case GHOSTTY_ACTION_NEW_TAB_WITH_COMMAND:
newTabWithCommand(app, target: target, v: action.action.new_tab_with_command)

case GHOSTTY_ACTION_NEW_SPLIT:
newSplit(app, target: target, direction: action.action.new_split)

Expand Down Expand Up @@ -851,6 +854,54 @@ extension Ghostty {
}
}

private static func newTabWithCommand(
_ app: ghostty_app_t,
target: ghostty_target_s,
v: ghostty_action_new_tab_with_command_s
) {
let command = String(
bytes: UnsafeRawBufferPointer(start: v.command, count: Int(v.len)),
encoding: .utf8
)

switch target.tag {
case GHOSTTY_TARGET_APP:
// A new tab with a command requires a surface context so
// we can inherit the proper configuration. Without one we
// do nothing.
Ghostty.logger.warning("new_tab_with_command does nothing with an app target")
return

case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return }
guard let appState = self.appState(fromView: surfaceView) else { return }
guard appState.config.windowDecorations else {
let alert = NSAlert()
alert.messageText = "Tabs are disabled"
alert.informativeText = "Enable window decorations to use tabs"
alert.addButton(withTitle: "OK")
alert.alertStyle = .warning
_ = alert.runModal()
return
}

var config = SurfaceConfiguration(from: ghostty_surface_inherited_config(surface, GHOSTTY_SURFACE_CONTEXT_TAB))
config.command = command

NotificationCenter.default.post(
name: Notification.ghosttyNewTab,
object: surfaceView,
userInfo: [
Notification.NewSurfaceConfigKey: config,
]
)

default:
assertionFailure()
}
}

private static func newSplit(
_ app: ghostty_app_t,
target: ghostty_target_s,
Expand Down
6 changes: 6 additions & 0 deletions src/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5268,6 +5268,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
{},
),

.new_tab_with_command => |v| return try self.rt_app.performAction(
.{ .surface = self },
.new_tab_with_command,
.{ .command = v },
),

.close_tab => |v| return try self.rt_app.performAction(
.{ .surface = self },
.close_tab,
Expand Down
24 changes: 24 additions & 0 deletions src/apprt/action.zig
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@ pub const Action = union(Key) {
/// otherwise the terminal-set title.
copy_title_to_clipboard,

/// Open a new tab running the given command instead of the default.
/// Otherwise behaves the same as new_tab.
new_tab_with_command: NewTabCommand,

/// Sync with: ghostty_action_tag_e
pub const Key = enum(c_int) {
quit,
Expand Down Expand Up @@ -415,6 +419,7 @@ pub const Action = union(Key) {
search_selected,
readonly,
copy_title_to_clipboard,
new_tab_with_command,

test "ghostty.h Action.Key" {
try lib.checkGhosttyHEnum(Key, "GHOSTTY_ACTION_");
Expand Down Expand Up @@ -930,6 +935,25 @@ pub const OpenUrl = struct {
}
};

/// The command to run in a new tab for new_tab_with_command.
pub const NewTabCommand = struct {
/// The command to execute.
command: []const u8,

// Sync with: ghostty_action_new_tab_with_command_s
pub const C = extern struct {
command: [*]const u8,
len: usize,
};

pub fn cval(self: NewTabCommand) C {
return .{
.command = self.command.ptr,
.len = self.command.len,
};
}
};

/// sync with ghostty_action_close_tab_mode_e in ghostty.h
pub const CloseTabMode = enum(c_int) {
/// Close the current tab.
Expand Down
4 changes: 4 additions & 0 deletions src/apprt/gtk/class/application.zig
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,10 @@ pub const Application = extern struct {

.new_tab => return Action.newTab(target),

// TODO: GTK does not yet support the custom command and
// falls back to opening a regular new tab.
.new_tab_with_command => return Action.newTab(target),

.new_window => try Action.newWindow(
self,
switch (target) {
Expand Down
35 changes: 35 additions & 0 deletions src/input/Binding.zig
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,22 @@ pub const Action = union(enum) {
/// Open a new tab.
new_tab,

/// Open a new tab running the given command instead of the
/// default configured command, e.g.
/// `keybind = cmd+ctrl+digit_1=new_tab_with_command:ssh myhost`
///
/// The command is always run in a shell (e.g. `/bin/sh -c`), so it
/// may contain arguments. Unlike the `command` configuration, the
/// `direct:` and `shell:` prefixes are not supported.
///
/// The new tab behaves as if `wait-after-command` were enabled:
/// when the command exits, the terminal remains open and must be
/// closed manually.
///
/// This is currently only fully implemented on macOS. On other
/// platforms this opens a new tab with the default command.
new_tab_with_command: []const u8,

/// Go to the previous tab.
previous_tab,

Expand Down Expand Up @@ -1413,6 +1429,7 @@ pub const Action = union(enum) {
// come from. For example `new_window` needs to be sourced to
// a surface so inheritance can be done correctly.
.new_tab,
.new_tab_with_command,
.previous_tab,
.next_tab,
.last_tab,
Expand Down Expand Up @@ -3072,6 +3089,24 @@ test "parse: text action equals sign" {
}
}

test "parse: new_tab_with_command" {
const testing = std.testing;
{
const binding = try parseSingle("ctrl+a=new_tab_with_command:ssh myhost");
try testing.expect(binding.action == .new_tab_with_command);
try testing.expectEqualStrings(
"ssh myhost",
binding.action.new_tab_with_command,
);
}

// A command is required: no colon is an error.
try testing.expectError(
Error.InvalidFormat,
parseSingle("ctrl+a=new_tab_with_command"),
);
}

// For Ghostty 1.2+ we changed our key names to match the W3C and removed
// `physical:`. This tests the backwards compatibility with the old format.
// Note that our backwards compatibility isn't 100% perfect since triggers
Expand Down
1 change: 1 addition & 0 deletions src/input/command.zig
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,7 @@ fn actionCommands(action: Action.Key) []const Command {
.jump_to_prompt,
.write_scrollback_file,
.goto_tab,
.new_tab_with_command,
.resize_split,
.activate_key_table,
.activate_key_table_once,
Expand Down
Loading