mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
add direct set_surface_title and set_tab_title actions (#11373)
Fixes #11316 This mirrors the `prompt` actions (hence why there is no window action here) and enables setting titles via keybind actions which importantly lets this work via command palettes, App Intents, AppleScript, etc.
This commit is contained in:
@@ -889,6 +889,7 @@ typedef enum {
|
||||
GHOSTTY_ACTION_RENDER_INSPECTOR,
|
||||
GHOSTTY_ACTION_DESKTOP_NOTIFICATION,
|
||||
GHOSTTY_ACTION_SET_TITLE,
|
||||
GHOSTTY_ACTION_SET_TAB_TITLE,
|
||||
GHOSTTY_ACTION_PROMPT_TITLE,
|
||||
GHOSTTY_ACTION_PWD,
|
||||
GHOSTTY_ACTION_MOUSE_SHAPE,
|
||||
@@ -937,6 +938,7 @@ typedef union {
|
||||
ghostty_action_inspector_e inspector;
|
||||
ghostty_action_desktop_notification_s desktop_notification;
|
||||
ghostty_action_set_title_s set_title;
|
||||
ghostty_action_set_title_s set_tab_title;
|
||||
ghostty_action_prompt_title_e prompt_title;
|
||||
ghostty_action_pwd_s pwd;
|
||||
ghostty_action_mouse_shape_e mouse_shape;
|
||||
|
||||
@@ -539,6 +539,9 @@ extension Ghostty {
|
||||
case GHOSTTY_ACTION_SET_TITLE:
|
||||
setTitle(app, target: target, v: action.action.set_title)
|
||||
|
||||
case GHOSTTY_ACTION_SET_TAB_TITLE:
|
||||
return setTabTitle(app, target: target, v: action.action.set_tab_title)
|
||||
|
||||
case GHOSTTY_ACTION_PROMPT_TITLE:
|
||||
return promptTitle(app, target: target, v: action.action.prompt_title)
|
||||
|
||||
@@ -1602,6 +1605,33 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
private static func setTabTitle(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
v: ghostty_action_set_title_s
|
||||
) -> Bool {
|
||||
switch target.tag {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("set tab title does nothing with an app target")
|
||||
return false
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let title = String(cString: v.title!, encoding: .utf8) else { return false }
|
||||
let titleOverride = title.isEmpty ? nil : title
|
||||
guard let surface = target.target.surface else { return false }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return false }
|
||||
guard let window = surfaceView.window,
|
||||
let controller = window.windowController as? BaseTerminalController
|
||||
else { return false }
|
||||
controller.titleOverride = titleOverride
|
||||
return true
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private static func copyTitleToClipboard(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s) -> Bool {
|
||||
|
||||
@@ -5482,6 +5482,26 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
.tab,
|
||||
),
|
||||
|
||||
.set_surface_title => |v| {
|
||||
const title = try self.alloc.dupeZ(u8, v);
|
||||
defer self.alloc.free(title);
|
||||
return try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.set_title,
|
||||
.{ .title = title },
|
||||
);
|
||||
},
|
||||
|
||||
.set_tab_title => |v| {
|
||||
const title = try self.alloc.dupeZ(u8, v);
|
||||
defer self.alloc.free(title);
|
||||
return try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.set_tab_title,
|
||||
.{ .title = title },
|
||||
);
|
||||
},
|
||||
|
||||
.clear_screen => {
|
||||
// This is a duplicate of some of the logic in termio.clearScreen
|
||||
// but we need to do this here so we can know the answer before
|
||||
|
||||
@@ -201,6 +201,9 @@ pub const Action = union(Key) {
|
||||
/// Set the title of the target to the requested value.
|
||||
set_title: SetTitle,
|
||||
|
||||
/// Set the tab title override for the target's tab.
|
||||
set_tab_title: SetTitle,
|
||||
|
||||
/// Set the title of the target to a prompted value. It is up to
|
||||
/// the apprt to prompt. The value specifies whether to prompt for the
|
||||
/// surface title or the tab title.
|
||||
@@ -375,6 +378,7 @@ pub const Action = union(Key) {
|
||||
render_inspector,
|
||||
desktop_notification,
|
||||
set_title,
|
||||
set_tab_title,
|
||||
prompt_title,
|
||||
pwd,
|
||||
mouse_shape,
|
||||
|
||||
@@ -740,6 +740,7 @@ pub const Application = extern struct {
|
||||
.scrollbar => Action.scrollbar(target, value),
|
||||
|
||||
.set_title => Action.setTitle(target, value),
|
||||
.set_tab_title => return Action.setTabTitle(target, value),
|
||||
|
||||
.show_child_exited => return Action.showChildExited(target, value),
|
||||
|
||||
@@ -2545,6 +2546,30 @@ const Action = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setTabTitle(
|
||||
target: apprt.Target,
|
||||
value: apprt.action.SetTitle,
|
||||
) bool {
|
||||
switch (target) {
|
||||
.app => {
|
||||
log.warn("set_tab_title to app is unexpected", .{});
|
||||
return false;
|
||||
},
|
||||
.surface => |core| {
|
||||
const surface = core.rt_surface.surface;
|
||||
const tab = ext.getAncestor(
|
||||
Tab,
|
||||
surface.as(gtk.Widget),
|
||||
) orelse {
|
||||
log.warn("surface is not in a tab, ignoring set_tab_title", .{});
|
||||
return false;
|
||||
};
|
||||
tab.setTitleOverride(if (value.title.len == 0) null else value.title);
|
||||
return true;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn showChildExited(
|
||||
target: apprt.Target,
|
||||
value: apprt.surface.Message.ChildExited,
|
||||
|
||||
@@ -577,6 +577,16 @@ pub const Action = union(enum) {
|
||||
/// and persists across focus changes within the tab.
|
||||
prompt_tab_title,
|
||||
|
||||
/// Set the title for the current focused surface.
|
||||
///
|
||||
/// If the title is empty, the surface title is reset to an empty title.
|
||||
set_surface_title: []const u8,
|
||||
|
||||
/// Set the title for the current focused tab.
|
||||
///
|
||||
/// If the title is empty, the tab title override is cleared.
|
||||
set_tab_title: []const u8,
|
||||
|
||||
/// Create a new split in the specified direction.
|
||||
///
|
||||
/// Valid arguments:
|
||||
@@ -1324,6 +1334,8 @@ pub const Action = union(enum) {
|
||||
.set_font_size,
|
||||
.prompt_surface_title,
|
||||
.prompt_tab_title,
|
||||
.set_surface_title,
|
||||
.set_tab_title,
|
||||
.clear_screen,
|
||||
.select_all,
|
||||
.scroll_to_top,
|
||||
@@ -3292,6 +3304,16 @@ test "parse: action with string" {
|
||||
try testing.expect(binding.action == .esc);
|
||||
try testing.expectEqualStrings("A", binding.action.esc);
|
||||
}
|
||||
{
|
||||
const binding = try parseSingle("a=set_surface_title:surface");
|
||||
try testing.expect(binding.action == .set_surface_title);
|
||||
try testing.expectEqualStrings("surface", binding.action.set_surface_title);
|
||||
}
|
||||
{
|
||||
const binding = try parseSingle("a=set_tab_title:tab");
|
||||
try testing.expect(binding.action == .set_tab_title);
|
||||
try testing.expectEqualStrings("tab", binding.action.set_tab_title);
|
||||
}
|
||||
}
|
||||
|
||||
test "parse: action with enum" {
|
||||
@@ -4557,6 +4579,18 @@ test "action: format" {
|
||||
try testing.expectEqualStrings("text:\\xf0\\x9f\\x91\\xbb", buf.written());
|
||||
}
|
||||
|
||||
test "action: format set title" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const a: Action = .{ .set_tab_title = "foo bar" };
|
||||
|
||||
var buf: std.Io.Writer.Allocating = .init(alloc);
|
||||
defer buf.deinit();
|
||||
try a.format(&buf.writer);
|
||||
try testing.expectEqualStrings("set_tab_title:foo bar", buf.written());
|
||||
}
|
||||
|
||||
test "set: appendChain with no parent returns error" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@@ -689,6 +689,8 @@ fn actionCommands(action: Action.Key) []const Command {
|
||||
.esc,
|
||||
.cursor_key,
|
||||
.set_font_size,
|
||||
.set_surface_title,
|
||||
.set_tab_title,
|
||||
.search,
|
||||
.scroll_to_row,
|
||||
.scroll_page_fractional,
|
||||
|
||||
Reference in New Issue
Block a user