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.pull/11377/head
commit
8ad9ec8e88
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue