mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-29 11:57:57 +00:00
Merge remote-tracking branch 'upstream/main' into jacob/uucode
This commit is contained in:
@@ -272,6 +272,7 @@ const DerivedConfig = struct {
|
||||
title_report: bool,
|
||||
links: []Link,
|
||||
link_previews: configpkg.LinkPreviews,
|
||||
scroll_to_bottom: configpkg.Config.ScrollToBottom,
|
||||
|
||||
const Link = struct {
|
||||
regex: oni.Regex,
|
||||
@@ -340,6 +341,7 @@ const DerivedConfig = struct {
|
||||
.title_report = config.@"title-report",
|
||||
.links = links,
|
||||
.link_previews = config.@"link-previews",
|
||||
.scroll_to_bottom = config.@"scroll-to-bottom",
|
||||
|
||||
// Assignments happen sequentially so we have to do this last
|
||||
// so that the memory is captured from allocs above.
|
||||
@@ -2280,7 +2282,8 @@ pub fn keyCallback(
|
||||
try self.setSelection(null);
|
||||
}
|
||||
|
||||
try self.io.terminal.scrollViewport(.{ .bottom = {} });
|
||||
if (self.config.scroll_to_bottom.keystroke) try self.io.terminal.scrollViewport(.bottom);
|
||||
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
@@ -2766,8 +2769,21 @@ pub fn scrollCallback(
|
||||
// that a wheel tick of 1 results in single scroll event.
|
||||
const yoff_adjusted: f64 = if (scroll_mods.precision)
|
||||
yoff
|
||||
else
|
||||
yoff * cell_size * self.config.mouse_scroll_multiplier;
|
||||
else yoff_adjusted: {
|
||||
// Round out the yoff to an absolute minimum of 1. macos tries to
|
||||
// simulate precision scrolling with non precision events by
|
||||
// ramping up the magnitude of the offsets as it detects faster
|
||||
// scrolling. Single click (very slow) scrolls are reported with a
|
||||
// magnitude of 0.1 which would normally require a few clicks
|
||||
// before we register an actual scroll event (depending on cell
|
||||
// height and the mouse_scroll_multiplier setting).
|
||||
const yoff_max: f64 = if (yoff > 0)
|
||||
@max(yoff, 1)
|
||||
else
|
||||
@min(yoff, -1);
|
||||
|
||||
break :yoff_adjusted yoff_max * cell_size * self.config.mouse_scroll_multiplier;
|
||||
};
|
||||
|
||||
// Add our previously saved pending amount to the offset to get the
|
||||
// new offset value. The signs of the pending and yoff should match
|
||||
@@ -4701,10 +4717,13 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
{},
|
||||
),
|
||||
|
||||
.close_tab => return try self.rt_app.performAction(
|
||||
.close_tab => |v| return try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.close_tab,
|
||||
{},
|
||||
switch (v) {
|
||||
.this => .this,
|
||||
.other => .other,
|
||||
},
|
||||
),
|
||||
|
||||
inline .previous_tab,
|
||||
|
||||
@@ -70,13 +70,15 @@ pub const Runtime = enum {
|
||||
gtk,
|
||||
|
||||
pub fn default(target: std.Target) Runtime {
|
||||
// The Linux default is GTK because it is a full featured application.
|
||||
if (target.os.tag == .linux) return .@"gtk-ng";
|
||||
|
||||
// Otherwise, we do NONE so we don't create an exe and we
|
||||
// create libghostty. On macOS, Xcode is used to build the app
|
||||
// that links to libghostty.
|
||||
return .none;
|
||||
return switch (target.os.tag) {
|
||||
// The Linux and FreeBSD default is GTK because it is a full
|
||||
// featured application.
|
||||
.linux, .freebsd => .@"gtk-ng",
|
||||
// Otherwise, we do NONE so we don't create an exe and we create
|
||||
// libghostty. On macOS, Xcode is used to build the app that links
|
||||
// to libghostty.
|
||||
else => .none,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -83,8 +83,9 @@ pub const Action = union(Key) {
|
||||
/// the tab should be opened in a new window.
|
||||
new_tab,
|
||||
|
||||
/// Closes the tab belonging to the currently focused split.
|
||||
close_tab,
|
||||
/// Closes the tab belonging to the currently focused split, or all other
|
||||
/// tabs, depending on the mode.
|
||||
close_tab: CloseTabMode,
|
||||
|
||||
/// Create a new split. The value determines the location of the split
|
||||
/// relative to the target.
|
||||
@@ -701,3 +702,11 @@ pub const OpenUrl = struct {
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// sync with ghostty_action_close_tab_mode_e in ghostty.h
|
||||
pub const CloseTabMode = enum(c_int) {
|
||||
/// Close the current tab.
|
||||
this,
|
||||
/// Close all other tabs.
|
||||
other,
|
||||
};
|
||||
|
||||
@@ -447,6 +447,9 @@ pub const Surface = struct {
|
||||
|
||||
/// Input to send to the command after it is started.
|
||||
initial_input: ?[*:0]const u8 = null,
|
||||
|
||||
/// Wait after the command exits
|
||||
wait_after_command: bool = false,
|
||||
};
|
||||
|
||||
pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||
@@ -540,6 +543,11 @@ pub const Surface = struct {
|
||||
);
|
||||
}
|
||||
|
||||
// Wait after command
|
||||
if (opts.wait_after_command) {
|
||||
config.@"wait-after-command" = true;
|
||||
}
|
||||
|
||||
// Initialize our surface right away. We're given a view that is
|
||||
// ready to use.
|
||||
try self.core_surface.init(
|
||||
|
||||
@@ -116,6 +116,11 @@ pub const Application = extern struct {
|
||||
/// and initialization was successful.
|
||||
transient_cgroup_base: ?[]const u8 = null,
|
||||
|
||||
/// This is set to true so long as we request a window exactly
|
||||
/// once. This prevents quitting the app before we've shown one
|
||||
/// window.
|
||||
requested_window: bool = false,
|
||||
|
||||
/// This is set to false internally when the event loop
|
||||
/// should exit and the application should quit. This must
|
||||
/// only be set by the main loop thread.
|
||||
@@ -461,7 +466,13 @@ pub const Application = extern struct {
|
||||
// If the quit timer has expired, quit.
|
||||
if (priv.quit_timer == .expired) break :q true;
|
||||
|
||||
// There's no quit timer running, or it hasn't expired, don't quit.
|
||||
// If we have no windows attached to our app, also quit.
|
||||
if (priv.requested_window and @as(
|
||||
?*glib.List,
|
||||
self.as(gtk.Application).getWindows(),
|
||||
) == null) break :q true;
|
||||
|
||||
// No quit conditions met
|
||||
break :q false;
|
||||
};
|
||||
|
||||
@@ -488,7 +499,15 @@ pub const Application = extern struct {
|
||||
const parent: ?*gtk.Widget = parent: {
|
||||
const list = gtk.Window.listToplevels();
|
||||
defer list.free();
|
||||
const focused = list.findCustom(null, findActiveWindow);
|
||||
const focused = @as(?*glib.List, list.findCustom(
|
||||
null,
|
||||
findActiveWindow,
|
||||
)) orelse {
|
||||
// If we have an active surface then we should have
|
||||
// a window available but in the rare case we don't we
|
||||
// should exit so we don't crash.
|
||||
break :parent null;
|
||||
};
|
||||
break :parent @ptrCast(@alignCast(focused.f_data));
|
||||
};
|
||||
|
||||
@@ -542,7 +561,7 @@ pub const Application = extern struct {
|
||||
value: apprt.Action.Value(action),
|
||||
) !bool {
|
||||
switch (action) {
|
||||
.close_tab => return Action.closeTab(target),
|
||||
.close_tab => return Action.closeTab(target, value),
|
||||
.close_window => return Action.closeWindow(target),
|
||||
|
||||
.config_change => try Action.configChange(
|
||||
@@ -713,27 +732,24 @@ pub const Application = extern struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn loadRuntimeCss(
|
||||
self: *Self,
|
||||
) Allocator.Error!void {
|
||||
fn loadRuntimeCss(self: *Self) Allocator.Error!void {
|
||||
const alloc = self.allocator();
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
const config = self.private().config.get();
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = try .initCapacity(alloc, 2048);
|
||||
defer buf.deinit(alloc);
|
||||
|
||||
const writer = buf.writer(alloc);
|
||||
|
||||
const config = self.private().config.get();
|
||||
const window_theme = config.@"window-theme";
|
||||
const unfocused_fill: CoreConfig.Color = config.@"unfocused-split-fill" orelse config.background;
|
||||
const headerbar_background = config.@"window-titlebar-background" orelse config.background;
|
||||
const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground;
|
||||
|
||||
try writer.print(
|
||||
\\widget.unfocused-split {{
|
||||
\\ opacity: {d:.2};
|
||||
\\ background-color: rgb({d},{d},{d});
|
||||
\\}}
|
||||
\\
|
||||
, .{
|
||||
1.0 - config.@"unfocused-split-opacity",
|
||||
unfocused_fill.r,
|
||||
@@ -747,6 +763,7 @@ pub const Application = extern struct {
|
||||
\\ color: rgb({[r]d},{[g]d},{[b]d});
|
||||
\\ background: rgb({[r]d},{[g]d},{[b]d});
|
||||
\\}}
|
||||
\\
|
||||
, .{
|
||||
.r = color.r,
|
||||
.g = color.g,
|
||||
@@ -759,9 +776,129 @@ pub const Application = extern struct {
|
||||
\\.window headerbar {{
|
||||
\\ font-family: "{[font_family]s}";
|
||||
\\}}
|
||||
\\
|
||||
, .{ .font_family = font_family });
|
||||
}
|
||||
|
||||
try loadRuntimeCss414(config, &writer);
|
||||
try loadRuntimeCss416(config, &writer);
|
||||
|
||||
// ensure that we have a sentinel
|
||||
try writer.writeByte(0);
|
||||
|
||||
const data = buf.items[0 .. buf.items.len - 1 :0];
|
||||
|
||||
log.debug("runtime CSS is {d} bytes", .{data.len + 1});
|
||||
|
||||
// Clears any previously loaded CSS from this provider
|
||||
loadCssProviderFromData(
|
||||
self.private().css_provider,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/// Load runtime CSS for older than GTK 4.16
|
||||
fn loadRuntimeCss414(
|
||||
config: *const CoreConfig,
|
||||
writer: *const std.ArrayListUnmanaged(u8).Writer,
|
||||
) Allocator.Error!void {
|
||||
if (gtk_version.runtimeAtLeast(4, 16, 0)) return;
|
||||
|
||||
const window_theme = config.@"window-theme";
|
||||
const headerbar_background = config.@"window-titlebar-background" orelse config.background;
|
||||
const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground;
|
||||
|
||||
switch (window_theme) {
|
||||
.ghostty => try writer.print(
|
||||
\\windowhandle {{
|
||||
\\ background-color: rgb({d},{d},{d});
|
||||
\\ color: rgb({d},{d},{d});
|
||||
\\}}
|
||||
\\windowhandle:backdrop {{
|
||||
\\ background-color: oklab(from rgb({d},{d},{d}) calc(l * 0.9) a b / alpha);
|
||||
\\}}
|
||||
\\
|
||||
, .{
|
||||
headerbar_background.r,
|
||||
headerbar_background.g,
|
||||
headerbar_background.b,
|
||||
headerbar_foreground.r,
|
||||
headerbar_foreground.g,
|
||||
headerbar_foreground.b,
|
||||
headerbar_background.r,
|
||||
headerbar_background.g,
|
||||
headerbar_background.b,
|
||||
}),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// Load runtime for GTK 4.16 and newer
|
||||
fn loadRuntimeCss416(
|
||||
config: *const CoreConfig,
|
||||
writer: *const std.ArrayListUnmanaged(u8).Writer,
|
||||
) Allocator.Error!void {
|
||||
if (gtk_version.runtimeUntil(4, 16, 0)) return;
|
||||
|
||||
const window_theme = config.@"window-theme";
|
||||
const headerbar_background = config.@"window-titlebar-background" orelse config.background;
|
||||
const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground;
|
||||
|
||||
try writer.writeAll(
|
||||
\\/*
|
||||
\\ * Child Exited Overlay
|
||||
\\ */
|
||||
\\
|
||||
\\.child-exited.normal revealer widget {
|
||||
\\ background-color: color-mix(
|
||||
\\ in srgb,
|
||||
\\ var(--success-bg-color),
|
||||
\\ transparent 50%
|
||||
\\ );
|
||||
\\}
|
||||
\\
|
||||
\\.child-exited.abnormal revealer widget {
|
||||
\\ background-color: color-mix(
|
||||
\\ in srgb,
|
||||
\\ var(--error-bg-color),
|
||||
\\ transparent 50%
|
||||
\\ );
|
||||
\\}
|
||||
\\
|
||||
\\/*
|
||||
\\ * Surface
|
||||
\\ */
|
||||
\\
|
||||
\\.surface progressbar.error trough progress {
|
||||
\\ background-color: color-mix(
|
||||
\\ in srgb,
|
||||
\\ var(--error-bg-color),
|
||||
\\ transparent 50%
|
||||
\\ );
|
||||
\\}
|
||||
\\
|
||||
\\.surface .bell-overlay {
|
||||
\\ border-color: color-mix(
|
||||
\\ in srgb,
|
||||
\\ var(--accent-color),
|
||||
\\ transparent 50%
|
||||
\\ );
|
||||
\\}
|
||||
\\
|
||||
\\/*
|
||||
\\ * Splits
|
||||
\\ */
|
||||
\\
|
||||
\\.window .split paned > separator {
|
||||
\\ background-color: color-mix(
|
||||
\\ in srgb,
|
||||
\\ var(--window-bg-color),
|
||||
\\ transparent 0%
|
||||
\\ );
|
||||
\\}
|
||||
\\
|
||||
);
|
||||
|
||||
switch (window_theme) {
|
||||
.ghostty => try writer.print(
|
||||
\\:root {{
|
||||
@@ -794,15 +931,6 @@ pub const Application = extern struct {
|
||||
}),
|
||||
else => {},
|
||||
}
|
||||
|
||||
const data = try alloc.dupeZ(u8, buf.items);
|
||||
defer alloc.free(data);
|
||||
|
||||
// Clears any previously loaded CSS from this provider
|
||||
loadCssProviderFromData(
|
||||
self.private().css_provider,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
fn loadCustomCss(self: *Self) !void {
|
||||
@@ -872,7 +1000,8 @@ pub const Application = extern struct {
|
||||
self.syncActionAccelerator("win.close", .{ .close_window = {} });
|
||||
self.syncActionAccelerator("win.new-window", .{ .new_window = {} });
|
||||
self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} });
|
||||
self.syncActionAccelerator("win.close-tab", .{ .close_tab = {} });
|
||||
self.syncActionAccelerator("win.close-tab::this", .{ .close_tab = .this });
|
||||
self.syncActionAccelerator("tab.close::this", .{ .close_tab = .this });
|
||||
self.syncActionAccelerator("win.split-right", .{ .new_split = .right });
|
||||
self.syncActionAccelerator("win.split-down", .{ .new_split = .down });
|
||||
self.syncActionAccelerator("win.split-left", .{ .new_split = .left });
|
||||
@@ -1576,12 +1705,16 @@ pub const Application = extern struct {
|
||||
|
||||
/// All apprt action handlers
|
||||
const Action = struct {
|
||||
pub fn closeTab(target: apprt.Target) bool {
|
||||
pub fn closeTab(target: apprt.Target, value: apprt.Action.Value(.close_tab)) bool {
|
||||
switch (target) {
|
||||
.app => return false,
|
||||
.surface => |core| {
|
||||
const surface = core.rt_surface.surface;
|
||||
return surface.as(gtk.Widget).activateAction("tab.close", null) != 0;
|
||||
return surface.as(gtk.Widget).activateAction(
|
||||
"tab.close",
|
||||
glib.ext.VariantType.stringFor([:0]const u8),
|
||||
@as([*:0]const u8, @tagName(value)),
|
||||
) != 0;
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1853,6 +1986,13 @@ const Action = struct {
|
||||
self: *Application,
|
||||
parent: ?*CoreSurface,
|
||||
) !void {
|
||||
// Note that we've requested a window at least once. This is used
|
||||
// to trigger quit on no windows. Note I'm not sure if this is REALLY
|
||||
// necessary, but I don't want to risk a bug where on a slow machine
|
||||
// or something we quit immediately after starting up because there
|
||||
// was a delay in the event loop before we created a Window.
|
||||
self.private().requested_window = true;
|
||||
|
||||
const win = Window.new(self);
|
||||
initAndShowWindow(self, win, parent);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const Common = @import("../class.zig").Common;
|
||||
const Config = @import("config.zig").Config;
|
||||
const Dialog = @import("dialog.zig").Dialog;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_config_errors_dialog);
|
||||
const log = std.log.scoped(.gtk_ghostty_close_confirmation_dialog);
|
||||
|
||||
pub const CloseConfirmationDialog = extern struct {
|
||||
const Self = @This();
|
||||
|
||||
@@ -105,6 +105,24 @@ pub const Surface = extern struct {
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"error" = struct {
|
||||
pub const name = "error";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.default = false,
|
||||
.accessor = gobject.ext.privateFieldAccessor(
|
||||
Self,
|
||||
Private,
|
||||
&Private.offset,
|
||||
"error",
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"font-size-request" = struct {
|
||||
pub const name = "font-size-request";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
@@ -472,6 +490,12 @@ pub const Surface = extern struct {
|
||||
// false by a parent widget.
|
||||
bell_ringing: bool = false,
|
||||
|
||||
/// True if this surface is in an error state. This is currently
|
||||
/// a simple boolean with no additional information on WHAT the
|
||||
/// error state is, because we don't yet need it or use it. For now,
|
||||
/// if this is true, then it means the terminal is non-functional.
|
||||
@"error": bool = false,
|
||||
|
||||
/// A weak reference to an inspector window.
|
||||
inspector: ?*InspectorWindow = null,
|
||||
|
||||
@@ -571,6 +595,17 @@ pub const Surface = extern struct {
|
||||
return @intFromBool(config.@"bell-features".border);
|
||||
}
|
||||
|
||||
fn closureStackChildName(
|
||||
_: *Self,
|
||||
error_: c_int,
|
||||
) callconv(.c) ?[*:0]const u8 {
|
||||
const err = error_ != 0;
|
||||
return if (err)
|
||||
glib.ext.dupeZ(u8, "error")
|
||||
else
|
||||
glib.ext.dupeZ(u8, "terminal");
|
||||
}
|
||||
|
||||
pub fn toggleFullscreen(self: *Self) void {
|
||||
signals.@"toggle-fullscreen".impl.emit(
|
||||
self,
|
||||
@@ -1540,6 +1575,12 @@ pub const Surface = extern struct {
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"bell-ringing".impl.param_spec);
|
||||
}
|
||||
|
||||
pub fn setError(self: *Self, v: bool) void {
|
||||
const priv = self.private();
|
||||
priv.@"error" = v;
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"error".impl.param_spec);
|
||||
}
|
||||
|
||||
fn propConfig(
|
||||
self: *Self,
|
||||
_: *gobject.ParamSpec,
|
||||
@@ -1592,6 +1633,28 @@ pub const Surface = extern struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn propError(
|
||||
self: *Self,
|
||||
_: *gobject.ParamSpec,
|
||||
_: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
if (priv.@"error") {
|
||||
// Ensure we have an opaque background. The window will NOT set
|
||||
// this if we have transparency set and we need an opaque
|
||||
// background for the error message to be readable.
|
||||
self.as(gtk.Widget).addCssClass("background");
|
||||
} else {
|
||||
// Regardless of transparency setting, we remove the background
|
||||
// CSS class from this widget. Parent widgets will set it
|
||||
// appropriately (see window.zig for example).
|
||||
self.as(gtk.Widget).removeCssClass("background");
|
||||
}
|
||||
|
||||
// Note above: in both cases setting our error view is handled by
|
||||
// a Gtk.Stack visible-child-name binding.
|
||||
}
|
||||
|
||||
fn propMouseHoverUrl(
|
||||
self: *Self,
|
||||
_: *gobject.ParamSpec,
|
||||
@@ -1942,8 +2005,11 @@ pub const Surface = extern struct {
|
||||
// Bell stops ringing if any mouse button is pressed.
|
||||
self.setBellRinging(false);
|
||||
|
||||
// If we don't have focus, grab it.
|
||||
// Get our surface. If we don't have one, ignore this.
|
||||
const priv = self.private();
|
||||
const core_surface = priv.core_surface orelse return;
|
||||
|
||||
// If we don't have focus, grab it.
|
||||
const gl_area_widget = priv.gl_area.as(gtk.Widget);
|
||||
if (gl_area_widget.hasFocus() == 0) {
|
||||
_ = gl_area_widget.grabFocus();
|
||||
@@ -1951,10 +2017,10 @@ pub const Surface = extern struct {
|
||||
|
||||
// Report the event
|
||||
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
|
||||
const consumed = if (priv.core_surface) |surface| consumed: {
|
||||
const consumed = consumed: {
|
||||
const gtk_mods = event.getModifierState();
|
||||
const mods = gtk_key.translateMods(gtk_mods);
|
||||
break :consumed surface.mouseButtonCallback(
|
||||
break :consumed core_surface.mouseButtonCallback(
|
||||
.press,
|
||||
button,
|
||||
mods,
|
||||
@@ -1962,7 +2028,7 @@ pub const Surface = extern struct {
|
||||
log.warn("error in key callback err={}", .{err});
|
||||
break :err false;
|
||||
};
|
||||
} else false;
|
||||
};
|
||||
|
||||
// If a right click isn't consumed, mouseButtonCallback selects the hovered
|
||||
// word and returns false. We can use this to handle the context menu
|
||||
@@ -2303,21 +2369,23 @@ pub const Surface = extern struct {
|
||||
) callconv(.c) void {
|
||||
log.debug("realize", .{});
|
||||
|
||||
// Make the GL area current so we can detect any OpenGL errors. If
|
||||
// we have errors here we can't render and we switch to the error
|
||||
// state.
|
||||
const priv = self.private();
|
||||
priv.gl_area.makeCurrent();
|
||||
if (priv.gl_area.getError()) |err| {
|
||||
log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"});
|
||||
log.warn("this error is almost always due to a library, driver, or GTK issue", .{});
|
||||
log.warn("this is a common cause of this issue: https://ghostty.org/docs/help/gtk-opengl-context", .{});
|
||||
self.setError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we already have an initialized surface then we notify it.
|
||||
// If we don't, we'll initialize it on the first resize so we have
|
||||
// our proper initial dimensions.
|
||||
const priv = self.private();
|
||||
if (priv.core_surface) |v| realize: {
|
||||
// We need to make the context current so we can call GL functions.
|
||||
// This is required for all surface operations.
|
||||
priv.gl_area.makeCurrent();
|
||||
if (priv.gl_area.getError()) |err| {
|
||||
log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"});
|
||||
log.warn("this error is usually due to a driver or gtk bug", .{});
|
||||
log.warn("this is a common cause of this issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/4950", .{});
|
||||
break :realize;
|
||||
}
|
||||
|
||||
v.renderer.displayRealized() catch |err| {
|
||||
log.warn("core displayRealized failed err={}", .{err});
|
||||
break :realize;
|
||||
@@ -2662,11 +2730,13 @@ pub const Surface = extern struct {
|
||||
class.bindTemplateCallback("child_exited_close", &childExitedClose);
|
||||
class.bindTemplateCallback("context_menu_closed", &contextMenuClosed);
|
||||
class.bindTemplateCallback("notify_config", &propConfig);
|
||||
class.bindTemplateCallback("notify_error", &propError);
|
||||
class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl);
|
||||
class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden);
|
||||
class.bindTemplateCallback("notify_mouse_shape", &propMouseShape);
|
||||
class.bindTemplateCallback("notify_bell_ringing", &propBellRinging);
|
||||
class.bindTemplateCallback("should_border_be_shown", &closureShouldBorderBeShown);
|
||||
class.bindTemplateCallback("stack_child_name", &closureStackChildName);
|
||||
|
||||
// Properties
|
||||
gobject.ext.registerProperties(class, &.{
|
||||
@@ -2674,6 +2744,7 @@ pub const Surface = extern struct {
|
||||
properties.config.impl,
|
||||
properties.@"child-exited".impl,
|
||||
properties.@"default-size".impl,
|
||||
properties.@"error".impl,
|
||||
properties.@"font-size-request".impl,
|
||||
properties.focused.impl,
|
||||
properties.@"min-size".impl,
|
||||
|
||||
@@ -18,7 +18,6 @@ const gresource = @import("../build/gresource.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Config = @import("config.zig").Config;
|
||||
const Application = @import("application.zig").Application;
|
||||
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
|
||||
const SplitTree = @import("split_tree.zig").SplitTree;
|
||||
const Surface = @import("surface.zig").Surface;
|
||||
|
||||
@@ -199,8 +198,11 @@ pub const Tab = extern struct {
|
||||
}
|
||||
|
||||
fn initActionMap(self: *Self) void {
|
||||
const s_param_type = glib.ext.VariantType.newFor([:0]const u8);
|
||||
defer s_param_type.free();
|
||||
|
||||
const actions = [_]ext.actions.Action(Self){
|
||||
.init("close", actionClose, null),
|
||||
.init("close", actionClose, s_param_type),
|
||||
.init("ring-bell", actionRingBell, null),
|
||||
};
|
||||
|
||||
@@ -314,18 +316,44 @@ pub const Tab = extern struct {
|
||||
|
||||
fn actionClose(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
param_: ?*glib.Variant,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
const param = param_ orelse {
|
||||
log.warn("tab.close-tab called without a parameter", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
var str: ?[*:0]const u8 = null;
|
||||
param.get("&s", &str);
|
||||
|
||||
const tab_view = ext.getAncestor(
|
||||
adw.TabView,
|
||||
self.as(gtk.Widget),
|
||||
) orelse return;
|
||||
|
||||
const page = tab_view.getPage(self.as(gtk.Widget));
|
||||
|
||||
const mode = std.meta.stringToEnum(
|
||||
apprt.action.CloseTabMode,
|
||||
std.mem.span(
|
||||
str orelse {
|
||||
log.warn("invalid mode provided to tab.close-tab", .{});
|
||||
return;
|
||||
},
|
||||
),
|
||||
) orelse {
|
||||
// Need to be defensive here since actions can be triggered externally.
|
||||
log.warn("invalid mode provided to tab.close-tab: {s}", .{str.?});
|
||||
return;
|
||||
};
|
||||
|
||||
// Delegate to our parent to handle this, since this will emit
|
||||
// a close-page signal that the parent can intercept.
|
||||
tab_view.closePage(page);
|
||||
switch (mode) {
|
||||
.this => tab_view.closePage(page),
|
||||
.other => tab_view.closeOtherPages(page),
|
||||
}
|
||||
}
|
||||
|
||||
fn actionRingBell(
|
||||
|
||||
@@ -320,10 +320,13 @@ pub const Window = extern struct {
|
||||
|
||||
/// Setup our action map.
|
||||
fn initActionMap(self: *Self) void {
|
||||
const s_variant_type = glib.ext.VariantType.newFor([:0]const u8);
|
||||
defer s_variant_type.free();
|
||||
|
||||
const actions = [_]ext.actions.Action(Self){
|
||||
.init("about", actionAbout, null),
|
||||
.init("close", actionClose, null),
|
||||
.init("close-tab", actionCloseTab, null),
|
||||
.init("close-tab", actionCloseTab, s_variant_type),
|
||||
.init("new-tab", actionNewTab, null),
|
||||
.init("new-window", actionNewWindow, null),
|
||||
.init("ring-bell", actionRingBell, null),
|
||||
@@ -961,7 +964,14 @@ pub const Window = extern struct {
|
||||
_: *gobject.ParamSpec,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
self.addToast(i18n._("Reloaded the configuration"));
|
||||
const priv = self.private();
|
||||
if (priv.config) |config_obj| {
|
||||
const config = config_obj.get();
|
||||
if (config.@"app-notifications".@"config-reload") {
|
||||
self.addToast(i18n._("Reloaded the configuration"));
|
||||
}
|
||||
}
|
||||
|
||||
self.syncAppearance();
|
||||
}
|
||||
|
||||
@@ -980,6 +990,22 @@ pub const Window = extern struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn propIsActive(
|
||||
_: *gtk.Window,
|
||||
_: *gobject.ParamSpec,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// Don't change urgency if we're not the active window.
|
||||
if (self.as(gtk.Window).isActive() == 0) return;
|
||||
|
||||
self.winproto().setUrgent(false) catch |err| {
|
||||
log.warn(
|
||||
"winproto failed to reset urgency={}",
|
||||
.{err},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
fn propGdkSurfaceWidth(
|
||||
_: *gdk.Surface,
|
||||
_: *gobject.ParamSpec,
|
||||
@@ -1656,10 +1682,31 @@ pub const Window = extern struct {
|
||||
|
||||
fn actionCloseTab(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
param_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.close_tab);
|
||||
const param = param_ orelse {
|
||||
log.warn("win.close-tab called without a parameter", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
var str: ?[*:0]const u8 = null;
|
||||
param.get("&s", &str);
|
||||
|
||||
const mode = std.meta.stringToEnum(
|
||||
input.Binding.Action.CloseTabMode,
|
||||
std.mem.span(
|
||||
str orelse {
|
||||
log.warn("invalid mode provided to win.close-tab", .{});
|
||||
return;
|
||||
},
|
||||
),
|
||||
) orelse {
|
||||
log.warn("invalid mode provided to win.close-tab: {s}", .{str.?});
|
||||
return;
|
||||
};
|
||||
|
||||
self.performBindingAction(.{ .close_tab = mode });
|
||||
}
|
||||
|
||||
fn actionNewWindow(
|
||||
@@ -1758,10 +1805,13 @@ pub const Window = extern struct {
|
||||
native.beep();
|
||||
}
|
||||
|
||||
if (config.@"bell-features".attention) {
|
||||
if (config.@"bell-features".attention) attention: {
|
||||
// Dont set urgency if the window is already active.
|
||||
if (self.as(gtk.Window).isActive() != 0) break :attention;
|
||||
|
||||
// Request user attention
|
||||
self.winproto().setUrgent(true) catch |err| {
|
||||
log.warn("failed to request user attention={}", .{err});
|
||||
log.warn("winproto failed to set urgency={}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1905,6 +1955,7 @@ pub const Window = extern struct {
|
||||
class.bindTemplateCallback("notify_selected_page", &tabViewSelectedPage);
|
||||
class.bindTemplateCallback("notify_config", &propConfig);
|
||||
class.bindTemplateCallback("notify_fullscreened", &propFullscreened);
|
||||
class.bindTemplateCallback("notify_is_active", &propIsActive);
|
||||
class.bindTemplateCallback("notify_maximized", &propMaximized);
|
||||
class.bindTemplateCallback("notify_menu_active", &propMenuActive);
|
||||
class.bindTemplateCallback("notify_quick_terminal", &propQuickTerminal);
|
||||
|
||||
@@ -12,7 +12,7 @@ window.ssd.no-border-radius {
|
||||
border-radius: 0 0;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* GhosttySurface URL overlay
|
||||
*/
|
||||
label.url-overlay {
|
||||
@@ -83,13 +83,13 @@ label.resize-overlay {
|
||||
*/
|
||||
.child-exited.normal revealer widget {
|
||||
background-color: rgba(38, 162, 105, 0.5);
|
||||
/* after GTK 4.16 is a requirement, switch to the following:
|
||||
/* after GTK 4.16 is a requirement, switch to the following: */
|
||||
/* background-color: color-mix(in srgb, var(--success-bg-color), transparent 50%); */
|
||||
}
|
||||
|
||||
.child-exited.abnormal revealer widget {
|
||||
background-color: rgba(192, 28, 40, 0.5);
|
||||
/* after GTK 4.16 is a requirement, switch to the following:
|
||||
/* after GTK 4.16 is a requirement, switch to the following: */
|
||||
/* background-color: color-mix(in srgb, var(--error-bg-color), transparent 50%); */
|
||||
}
|
||||
|
||||
@@ -97,13 +97,15 @@ label.resize-overlay {
|
||||
* Surface
|
||||
*/
|
||||
.surface progressbar.error trough progress {
|
||||
background-color: rgb(192, 28, 40);
|
||||
background-color: rgba(192, 28, 40, 0.5);
|
||||
/* after GTK 4.16 is a requirement, switch to the following: */
|
||||
/* background-color: color-mix(in srgb, var(--error-bg-color), transparent); */
|
||||
/* background-color: color-mix(in srgb, var(--error-bg-color), transparent 50%); */
|
||||
}
|
||||
|
||||
.surface .bell-overlay {
|
||||
border-color: color-mix(in srgb, var(--accent-color), transparent 50%);
|
||||
border-color: rgba(58, 148, 74, 0.5);
|
||||
/* after GTK 4.16 is a requirement, switch to the following: */
|
||||
/* background-color: color-mix(in srgb, var(--accent-color), transparent 50%); */
|
||||
border-width: 3px;
|
||||
border-style: solid;
|
||||
}
|
||||
@@ -127,6 +129,8 @@ label.resize-overlay {
|
||||
|
||||
.window .split paned > separator {
|
||||
background-color: rgba(250, 250, 250, 1);
|
||||
/* after GTK 4.16 is a requirement, switch to the following: */
|
||||
/* background-color: color-mix(in srgb, var(--window-bg-color), transparent 0%); */
|
||||
background-clip: content-box;
|
||||
|
||||
/* This works around the oversized drag area for the right side of GtkPaned.
|
||||
|
||||
@@ -7,4 +7,6 @@ template $GhosttyCloseConfirmationDialog: $GhosttyDialog {
|
||||
cancel: _("Cancel"),
|
||||
close: _("Close") destructive,
|
||||
]
|
||||
|
||||
close-response: "cancel";
|
||||
}
|
||||
|
||||
@@ -8,146 +8,172 @@ template $GhosttySurface: Adw.Bin {
|
||||
|
||||
notify::bell-ringing => $notify_bell_ringing();
|
||||
notify::config => $notify_config();
|
||||
notify::error => $notify_error();
|
||||
notify::mouse-hover-url => $notify_mouse_hover_url();
|
||||
notify::mouse-hidden => $notify_mouse_hidden();
|
||||
notify::mouse-shape => $notify_mouse_shape();
|
||||
|
||||
Overlay {
|
||||
focusable: false;
|
||||
focus-on-click: false;
|
||||
Stack {
|
||||
StackPage {
|
||||
name: "terminal";
|
||||
|
||||
child: Box {
|
||||
hexpand: true;
|
||||
vexpand: true;
|
||||
child: Overlay {
|
||||
focusable: false;
|
||||
focus-on-click: false;
|
||||
|
||||
GLArea gl_area {
|
||||
realize => $gl_realize();
|
||||
unrealize => $gl_unrealize();
|
||||
render => $gl_render();
|
||||
resize => $gl_resize();
|
||||
hexpand: true;
|
||||
vexpand: true;
|
||||
focusable: true;
|
||||
focus-on-click: true;
|
||||
has-stencil-buffer: false;
|
||||
has-depth-buffer: false;
|
||||
allowed-apis: gl;
|
||||
}
|
||||
child: Box {
|
||||
hexpand: true;
|
||||
vexpand: true;
|
||||
|
||||
PopoverMenu context_menu {
|
||||
closed => $context_menu_closed();
|
||||
menu-model: context_menu_model;
|
||||
flags: nested;
|
||||
halign: start;
|
||||
has-arrow: false;
|
||||
}
|
||||
};
|
||||
GLArea gl_area {
|
||||
realize => $gl_realize();
|
||||
unrealize => $gl_unrealize();
|
||||
render => $gl_render();
|
||||
resize => $gl_resize();
|
||||
hexpand: true;
|
||||
vexpand: true;
|
||||
focusable: true;
|
||||
focus-on-click: true;
|
||||
has-stencil-buffer: false;
|
||||
has-depth-buffer: false;
|
||||
allowed-apis: gl;
|
||||
}
|
||||
|
||||
[overlay]
|
||||
ProgressBar progress_bar_overlay {
|
||||
styles [
|
||||
"osd",
|
||||
]
|
||||
PopoverMenu context_menu {
|
||||
closed => $context_menu_closed();
|
||||
menu-model: context_menu_model;
|
||||
flags: nested;
|
||||
halign: start;
|
||||
has-arrow: false;
|
||||
}
|
||||
};
|
||||
|
||||
visible: false;
|
||||
halign: fill;
|
||||
valign: start;
|
||||
[overlay]
|
||||
ProgressBar progress_bar_overlay {
|
||||
styles [
|
||||
"osd",
|
||||
]
|
||||
|
||||
visible: false;
|
||||
halign: fill;
|
||||
valign: start;
|
||||
}
|
||||
|
||||
[overlay]
|
||||
// The "border" bell feature is implemented here as an overlay rather than
|
||||
// just adding a border to the GLArea or other widget for two reasons.
|
||||
// First, adding a border to an existing widget causes a resize of the
|
||||
// widget which undesirable side effects. Second, we can make it reactive
|
||||
// here in the blueprint with relatively little code.
|
||||
Revealer {
|
||||
reveal-child: bind $should_border_be_shown(template.config, template.bell-ringing) as <bool>;
|
||||
transition-type: crossfade;
|
||||
transition-duration: 500;
|
||||
|
||||
Box bell_overlay {
|
||||
styles [
|
||||
"bell-overlay",
|
||||
]
|
||||
|
||||
halign: fill;
|
||||
valign: fill;
|
||||
}
|
||||
}
|
||||
|
||||
[overlay]
|
||||
$GhosttySurfaceChildExited child_exited_overlay {
|
||||
visible: bind template.child-exited;
|
||||
close-request => $child_exited_close();
|
||||
}
|
||||
|
||||
[overlay]
|
||||
$GhosttyResizeOverlay resize_overlay {}
|
||||
|
||||
[overlay]
|
||||
Label url_left {
|
||||
styles [
|
||||
"background",
|
||||
"url-overlay",
|
||||
]
|
||||
|
||||
visible: false;
|
||||
halign: start;
|
||||
valign: end;
|
||||
label: bind template.mouse-hover-url;
|
||||
|
||||
EventControllerMotion url_ec_motion {
|
||||
enter => $url_mouse_enter();
|
||||
leave => $url_mouse_leave();
|
||||
}
|
||||
}
|
||||
|
||||
[overlay]
|
||||
Label url_right {
|
||||
styles [
|
||||
"background",
|
||||
"url-overlay",
|
||||
]
|
||||
|
||||
visible: false;
|
||||
halign: end;
|
||||
valign: end;
|
||||
label: bind template.mouse-hover-url;
|
||||
}
|
||||
|
||||
// Event controllers for interactivity
|
||||
EventControllerFocus {
|
||||
enter => $focus_enter();
|
||||
leave => $focus_leave();
|
||||
}
|
||||
|
||||
EventControllerKey {
|
||||
key-pressed => $key_pressed();
|
||||
key-released => $key_released();
|
||||
}
|
||||
|
||||
EventControllerMotion {
|
||||
motion => $mouse_motion();
|
||||
leave => $mouse_leave();
|
||||
}
|
||||
|
||||
EventControllerScroll {
|
||||
scroll => $scroll();
|
||||
scroll-begin => $scroll_begin();
|
||||
scroll-end => $scroll_end();
|
||||
flags: both_axes;
|
||||
}
|
||||
|
||||
GestureClick {
|
||||
pressed => $mouse_down();
|
||||
released => $mouse_up();
|
||||
button: 0;
|
||||
}
|
||||
|
||||
DropTarget drop_target {
|
||||
drop => $drop();
|
||||
actions: copy;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[overlay]
|
||||
// The "border" bell feature is implemented here as an overlay rather than
|
||||
// just adding a border to the GLArea or other widget for two reasons.
|
||||
// First, adding a border to an existing widget causes a resize of the
|
||||
// widget which undesirable side effects. Second, we can make it reactive
|
||||
// here in the blueprint with relatively little code.
|
||||
Revealer {
|
||||
reveal-child: bind $should_border_be_shown(template.config, template.bell-ringing) as <bool>;
|
||||
transition-type: crossfade;
|
||||
transition-duration: 500;
|
||||
StackPage {
|
||||
name: "error";
|
||||
|
||||
Box bell_overlay {
|
||||
styles [
|
||||
"bell-overlay",
|
||||
]
|
||||
child: Adw.StatusPage {
|
||||
icon-name: "computer-fail-symbolic";
|
||||
title: _("Oh, no.");
|
||||
description: _("Unable to acquire an OpenGL context for rendering.");
|
||||
|
||||
halign: fill;
|
||||
valign: fill;
|
||||
}
|
||||
child: LinkButton {
|
||||
label: "https://ghostty.org/docs/help/gtk-opengl-context";
|
||||
uri: "https://ghostty.org/docs/help/gtk-opengl-context";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
[overlay]
|
||||
$GhosttySurfaceChildExited child_exited_overlay {
|
||||
visible: bind template.child-exited;
|
||||
close-request => $child_exited_close();
|
||||
}
|
||||
|
||||
[overlay]
|
||||
$GhosttyResizeOverlay resize_overlay {}
|
||||
|
||||
[overlay]
|
||||
Label url_left {
|
||||
styles [
|
||||
"background",
|
||||
"url-overlay",
|
||||
]
|
||||
|
||||
visible: false;
|
||||
halign: start;
|
||||
valign: end;
|
||||
label: bind template.mouse-hover-url;
|
||||
|
||||
EventControllerMotion url_ec_motion {
|
||||
enter => $url_mouse_enter();
|
||||
leave => $url_mouse_leave();
|
||||
}
|
||||
}
|
||||
|
||||
[overlay]
|
||||
Label url_right {
|
||||
styles [
|
||||
"background",
|
||||
"url-overlay",
|
||||
]
|
||||
|
||||
visible: false;
|
||||
halign: end;
|
||||
valign: end;
|
||||
label: bind template.mouse-hover-url;
|
||||
}
|
||||
}
|
||||
|
||||
// Event controllers for interactivity
|
||||
EventControllerFocus {
|
||||
enter => $focus_enter();
|
||||
leave => $focus_leave();
|
||||
}
|
||||
|
||||
EventControllerKey {
|
||||
key-pressed => $key_pressed();
|
||||
key-released => $key_released();
|
||||
}
|
||||
|
||||
EventControllerMotion {
|
||||
motion => $mouse_motion();
|
||||
leave => $mouse_leave();
|
||||
}
|
||||
|
||||
EventControllerScroll {
|
||||
scroll => $scroll();
|
||||
scroll-begin => $scroll_begin();
|
||||
scroll-end => $scroll_end();
|
||||
flags: both_axes;
|
||||
}
|
||||
|
||||
GestureClick {
|
||||
pressed => $mouse_down();
|
||||
released => $mouse_up();
|
||||
button: 0;
|
||||
}
|
||||
|
||||
DropTarget drop_target {
|
||||
drop => $drop();
|
||||
actions: copy;
|
||||
// The order matters here: we can only set this after the stack
|
||||
// pages above have been created.
|
||||
visible-child-name: bind $stack_child_name(template.error) as <string>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,7 +254,8 @@ menu context_menu_model {
|
||||
|
||||
item {
|
||||
label: _("Close Tab");
|
||||
action: "win.close-tab";
|
||||
action: "tab.close";
|
||||
target: "this";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ template $GhosttyWindow: Adw.ApplicationWindow {
|
||||
realize => $realize();
|
||||
notify::config => $notify_config();
|
||||
notify::fullscreened => $notify_fullscreened();
|
||||
notify::is-active => $notify_is_active();
|
||||
notify::maximized => $notify_maximized();
|
||||
notify::quick-terminal => $notify_quick_terminal();
|
||||
notify::scale-factor => $notify_scale_factor();
|
||||
@@ -225,6 +226,7 @@ menu main_menu {
|
||||
item {
|
||||
label: _("Close Tab");
|
||||
action: "win.close-tab";
|
||||
target: "this";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -491,7 +491,7 @@ pub fn performAction(
|
||||
.toggle_maximize => self.toggleMaximize(target),
|
||||
.toggle_fullscreen => self.toggleFullscreen(target, value),
|
||||
.new_tab => try self.newTab(target),
|
||||
.close_tab => return try self.closeTab(target),
|
||||
.close_tab => return try self.closeTab(target, value),
|
||||
.goto_tab => return self.gotoTab(target, value),
|
||||
.move_tab => self.moveTab(target, value),
|
||||
.new_split => try self.newSplit(target, value),
|
||||
@@ -585,7 +585,7 @@ fn newTab(_: *App, target: apprt.Target) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn closeTab(_: *App, target: apprt.Target) !bool {
|
||||
fn closeTab(_: *App, target: apprt.Target, value: apprt.Action.Value(.close_tab)) !bool {
|
||||
switch (target) {
|
||||
.app => return false,
|
||||
.surface => |v| {
|
||||
@@ -597,8 +597,16 @@ fn closeTab(_: *App, target: apprt.Target) !bool {
|
||||
return false;
|
||||
};
|
||||
|
||||
tab.closeWithConfirmation();
|
||||
return true;
|
||||
switch (value) {
|
||||
.this => {
|
||||
tab.closeWithConfirmation();
|
||||
return true;
|
||||
},
|
||||
.other => {
|
||||
log.warn("close-tab:other is not implemented", .{});
|
||||
return false;
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1145,7 +1153,7 @@ fn syncActionAccelerators(self: *App) !void {
|
||||
try self.syncActionAccelerator("win.close", .{ .close_window = {} });
|
||||
try self.syncActionAccelerator("win.new-window", .{ .new_window = {} });
|
||||
try self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} });
|
||||
try self.syncActionAccelerator("win.close-tab", .{ .close_tab = {} });
|
||||
try self.syncActionAccelerator("win.close-tab", .{ .close_tab = .this });
|
||||
try self.syncActionAccelerator("win.split-right", .{ .new_split = .right });
|
||||
try self.syncActionAccelerator("win.split-down", .{ .new_split = .down });
|
||||
try self.syncActionAccelerator("win.split-left", .{ .new_split = .left });
|
||||
|
||||
@@ -772,7 +772,9 @@ pub fn focusCurrentTab(self: *Window) void {
|
||||
}
|
||||
|
||||
pub fn onConfigReloaded(self: *Window) void {
|
||||
self.sendToast(i18n._("Reloaded the configuration"));
|
||||
if (self.app.config.@"app-notifications".@"config-reload") {
|
||||
self.sendToast(i18n._("Reloaded the configuration"));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sendToast(self: *Window, title: [*:0]const u8) void {
|
||||
@@ -1074,7 +1076,7 @@ fn gtkActionCloseTab(
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.{ .close_tab = {} });
|
||||
self.performBindingAction(.{ .close_tab = .this });
|
||||
}
|
||||
|
||||
fn gtkActionSplitRight(
|
||||
|
||||
@@ -131,6 +131,13 @@ pub const VTable = struct {
|
||||
};
|
||||
|
||||
test Benchmark {
|
||||
// This test fails on FreeBSD so skip:
|
||||
//
|
||||
// /home/runner/work/ghostty/ghostty/src/benchmark/Benchmark.zig:165:5: 0x3cd2de1 in decltest.Benchmark (ghostty-test)
|
||||
// try testing.expect(result.duration > 0);
|
||||
// ^
|
||||
if (builtin.os.tag == .freebsd) return error.SkipZigTest;
|
||||
|
||||
const testing = std.testing;
|
||||
const Simple = struct {
|
||||
const Self = @This();
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const args = @import("args.zig");
|
||||
const x11_color = @import("../terminal/main.zig").x11_color;
|
||||
const vaxis = @import("vaxis");
|
||||
const tui = @import("tui.zig");
|
||||
|
||||
pub const Options = struct {
|
||||
pub fn deinit(self: Options) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
/// If `true`, print without formatting even if printing to a tty
|
||||
plain: bool = false,
|
||||
|
||||
/// Enables "-h" and "--help" to work.
|
||||
pub fn help(self: Options) !void {
|
||||
_ = self;
|
||||
@@ -17,7 +24,12 @@ pub const Options = struct {
|
||||
|
||||
/// The `list-colors` command is used to list all the named RGB colors in
|
||||
/// Ghostty.
|
||||
pub fn run(alloc: std.mem.Allocator) !u8 {
|
||||
///
|
||||
/// Flags:
|
||||
///
|
||||
/// * `--plain`: will disable formatting and make the output more
|
||||
/// friendly for Unix tooling. This is default when not printing to a tty.
|
||||
pub fn run(alloc: Allocator) !u8 {
|
||||
var opts: Options = .{};
|
||||
defer opts.deinit();
|
||||
|
||||
@@ -27,7 +39,7 @@ pub fn run(alloc: std.mem.Allocator) !u8 {
|
||||
try args.parse(Options, alloc, &opts, &iter);
|
||||
}
|
||||
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
const stdout = std.io.getStdOut();
|
||||
|
||||
var keys = std.ArrayList([]const u8).init(alloc);
|
||||
defer keys.deinit();
|
||||
@@ -39,15 +51,163 @@ pub fn run(alloc: std.mem.Allocator) !u8 {
|
||||
}
|
||||
}.lessThan);
|
||||
|
||||
for (keys.items) |name| {
|
||||
const rgb = x11_color.map.get(name).?;
|
||||
try stdout.print("{s} = #{x:0>2}{x:0>2}{x:0>2}\n", .{
|
||||
name,
|
||||
rgb.r,
|
||||
rgb.g,
|
||||
rgb.b,
|
||||
});
|
||||
// Despite being under the posix namespace, this also works on Windows as of zig 0.13.0
|
||||
if (tui.can_pretty_print and !opts.plain and std.posix.isatty(stdout.handle)) {
|
||||
var arena = std.heap.ArenaAllocator.init(alloc);
|
||||
defer arena.deinit();
|
||||
return prettyPrint(arena.allocator(), keys.items);
|
||||
} else {
|
||||
const writer = stdout.writer();
|
||||
for (keys.items) |name| {
|
||||
const rgb = x11_color.map.get(name).?;
|
||||
try writer.print("{s} = #{x:0>2}{x:0>2}{x:0>2}\n", .{
|
||||
name,
|
||||
rgb.r,
|
||||
rgb.g,
|
||||
rgb.b,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn prettyPrint(alloc: Allocator, keys: [][]const u8) !u8 {
|
||||
// Set up vaxis
|
||||
var tty = try vaxis.Tty.init();
|
||||
defer tty.deinit();
|
||||
var vx = try vaxis.init(alloc, .{});
|
||||
defer vx.deinit(alloc, tty.anyWriter());
|
||||
|
||||
// We know we are ghostty, so let's enable mode 2027. Vaxis normally does this but you need an
|
||||
// event loop to auto-enable it.
|
||||
vx.caps.unicode = .unicode;
|
||||
try tty.anyWriter().writeAll(vaxis.ctlseqs.unicode_set);
|
||||
defer tty.anyWriter().writeAll(vaxis.ctlseqs.unicode_reset) catch {};
|
||||
|
||||
var buf_writer = tty.bufferedWriter();
|
||||
const writer = buf_writer.writer().any();
|
||||
|
||||
const winsize: vaxis.Winsize = switch (builtin.os.tag) {
|
||||
// We use some default, it doesn't really matter for what
|
||||
// we're doing because we don't do any wrapping.
|
||||
.windows => .{
|
||||
.rows = 24,
|
||||
.cols = 120,
|
||||
.x_pixel = 1024,
|
||||
.y_pixel = 768,
|
||||
},
|
||||
|
||||
else => try vaxis.Tty.getWinsize(tty.fd),
|
||||
};
|
||||
try vx.resize(alloc, tty.anyWriter(), winsize);
|
||||
|
||||
const win = vx.window();
|
||||
|
||||
var max_name_len: usize = 0;
|
||||
for (keys) |name| {
|
||||
if (name.len > max_name_len) max_name_len = name.len;
|
||||
}
|
||||
|
||||
// max name length plus " = #RRGGBB XX" plus " " gutter between columns
|
||||
const column_size = max_name_len + 15;
|
||||
// add two to take into account lack of gutter after last column
|
||||
const columns: usize = @divFloor(win.width + 2, column_size);
|
||||
|
||||
var i: usize = 0;
|
||||
const step = @divFloor(keys.len, columns) + 1;
|
||||
while (i < step) : (i += 1) {
|
||||
win.clear();
|
||||
|
||||
var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false };
|
||||
|
||||
for (0..columns) |j| {
|
||||
const k = i + (step * j);
|
||||
if (k >= keys.len) continue;
|
||||
|
||||
const name = keys[k];
|
||||
const rgb = x11_color.map.get(name).?;
|
||||
|
||||
const style1: vaxis.Style = .{
|
||||
.fg = .{
|
||||
.rgb = .{ rgb.r, rgb.g, rgb.b },
|
||||
},
|
||||
};
|
||||
const style2: vaxis.Style = .{
|
||||
.fg = .{
|
||||
.rgb = .{ rgb.r, rgb.g, rgb.b },
|
||||
},
|
||||
.bg = .{
|
||||
.rgb = .{ rgb.r, rgb.g, rgb.b },
|
||||
},
|
||||
};
|
||||
|
||||
// name of the color
|
||||
result = win.printSegment(
|
||||
.{ .text = name },
|
||||
.{ .col_offset = result.col },
|
||||
);
|
||||
// push the color data to the end of the column
|
||||
for (0..max_name_len - name.len) |_| {
|
||||
result = win.printSegment(
|
||||
.{ .text = " " },
|
||||
.{ .col_offset = result.col },
|
||||
);
|
||||
}
|
||||
result = win.printSegment(
|
||||
.{ .text = " = " },
|
||||
.{ .col_offset = result.col },
|
||||
);
|
||||
// rgb triple
|
||||
result = win.printSegment(.{
|
||||
.text = try std.fmt.allocPrint(
|
||||
alloc,
|
||||
"#{x:0>2}{x:0>2}{x:0>2}",
|
||||
.{
|
||||
rgb.r, rgb.g, rgb.b,
|
||||
},
|
||||
),
|
||||
.style = style1,
|
||||
}, .{ .col_offset = result.col });
|
||||
result = win.printSegment(
|
||||
.{ .text = " " },
|
||||
.{ .col_offset = result.col },
|
||||
);
|
||||
// colored block
|
||||
result = win.printSegment(
|
||||
.{
|
||||
.text = " ",
|
||||
.style = style2,
|
||||
},
|
||||
.{ .col_offset = result.col },
|
||||
);
|
||||
// add the gutter if needed
|
||||
if (j + 1 < columns) {
|
||||
result = win.printSegment(
|
||||
.{
|
||||
.text = " ",
|
||||
},
|
||||
.{ .col_offset = result.col },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// clear the rest of the line
|
||||
while (result.col != 0) {
|
||||
result = win.printSegment(
|
||||
.{
|
||||
.text = " ",
|
||||
},
|
||||
.{ .col_offset = result.col },
|
||||
);
|
||||
}
|
||||
|
||||
// output the data
|
||||
try vx.prettyPrint(writer);
|
||||
}
|
||||
|
||||
// be sure to flush!
|
||||
try buf_writer.flush();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -767,6 +767,22 @@ palette: Palette = .{},
|
||||
/// the mouse is shown again when a new window, tab, or split is created.
|
||||
@"mouse-hide-while-typing": bool = false,
|
||||
|
||||
/// When to scroll the surface to the bottom. The format of this is a list of
|
||||
/// options to enable separated by commas. If you prefix an option with `no-`
|
||||
/// then it is disabled. If you omit an option, its default value is used.
|
||||
///
|
||||
/// Available options:
|
||||
///
|
||||
/// - `keystroke` If set, scroll the surface to the bottom when the user
|
||||
/// presses a key that results in data being sent to the PTY (basically
|
||||
/// anything but modifiers or keybinds that are processed by Ghostty).
|
||||
///
|
||||
/// - `output` If set, scroll the surface to the bottom if there is new data
|
||||
/// to display. (Currently unimplemented.)
|
||||
///
|
||||
/// The default is `keystroke, no-output`.
|
||||
@"scroll-to-bottom": ScrollToBottom = .default,
|
||||
|
||||
/// Determines whether running programs can detect the shift key pressed with a
|
||||
/// mouse click. Typically, the shift key is used to extend mouse selection.
|
||||
///
|
||||
@@ -2499,6 +2515,8 @@ keybind: Keybinds = .{},
|
||||
///
|
||||
/// - `clipboard-copy` (default: true) - Show a notification when text is copied
|
||||
/// to the clipboard.
|
||||
/// - `config-reload` (default: true) - Show a notification when
|
||||
/// the configuration is reloaded.
|
||||
///
|
||||
/// To specify a notification to enable, specify the name of the notification.
|
||||
/// To specify a notification to disable, prefix the name with `no-`. For
|
||||
@@ -3017,6 +3035,13 @@ else
|
||||
/// Available since Ghostty 1.2.0.
|
||||
@"bold-color": ?BoldColor = null,
|
||||
|
||||
/// The opacity level (opposite of transparency) of the faint text. A value of
|
||||
/// 1 is fully opaque and a value of 0 is fully transparent. A value less than 0
|
||||
/// or greater than 1 will be clamped to the nearest valid value.
|
||||
///
|
||||
/// Available since Ghostty 1.2.0.
|
||||
@"faint-opacity": f64 = 0.5,
|
||||
|
||||
/// This will be used to set the `TERM` environment variable.
|
||||
/// HACK: We set this with an `xterm` prefix because vim uses that to enable key
|
||||
/// protocols (specifically this will enable `modifyOtherKeys`), among other
|
||||
@@ -3999,6 +4024,8 @@ pub fn finalize(self: *Config) !void {
|
||||
if (self.@"auto-update-channel" == null) {
|
||||
self.@"auto-update-channel" = build_config.release_channel;
|
||||
}
|
||||
|
||||
self.@"faint-opacity" = std.math.clamp(self.@"faint-opacity", 0.0, 1.0);
|
||||
}
|
||||
|
||||
/// Callback for src/cli/args.zig to allow us to handle special cases
|
||||
@@ -5596,7 +5623,7 @@ pub const Keybinds = struct {
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .unicode = 'w' }, .mods = .{ .ctrl = true, .shift = true } },
|
||||
.{ .close_tab = {} },
|
||||
.{ .close_tab = .this },
|
||||
);
|
||||
try self.set.putFlags(
|
||||
alloc,
|
||||
@@ -5902,7 +5929,7 @@ pub const Keybinds = struct {
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .unicode = 'w' }, .mods = .{ .super = true, .alt = true } },
|
||||
.{ .close_tab = {} },
|
||||
.{ .close_tab = .this },
|
||||
);
|
||||
try self.set.put(
|
||||
alloc,
|
||||
@@ -7058,6 +7085,7 @@ pub const GtkTitlebarStyle = enum(c_int) {
|
||||
/// See app-notifications
|
||||
pub const AppNotifications = packed struct {
|
||||
@"clipboard-copy": bool = true,
|
||||
@"config-reload": bool = true,
|
||||
};
|
||||
|
||||
/// See bell-features
|
||||
@@ -7195,6 +7223,53 @@ pub const QuickTerminalSize = struct {
|
||||
height: u32,
|
||||
};
|
||||
|
||||
/// C API structure for QuickTerminalSize
|
||||
pub const C = extern struct {
|
||||
primary: C.Size,
|
||||
secondary: C.Size,
|
||||
|
||||
pub const Size = extern struct {
|
||||
tag: Tag,
|
||||
value: Value,
|
||||
|
||||
pub const Tag = enum(u8) { none, percentage, pixels };
|
||||
|
||||
pub const Value = extern union {
|
||||
percentage: f32,
|
||||
pixels: u32,
|
||||
};
|
||||
|
||||
pub const none: C.Size = .{ .tag = .none, .value = undefined };
|
||||
|
||||
pub fn percentage(v: f32) C.Size {
|
||||
return .{
|
||||
.tag = .percentage,
|
||||
.value = .{ .percentage = v },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn pixels(v: u32) C.Size {
|
||||
return .{
|
||||
.tag = .pixels,
|
||||
.value = .{ .pixels = v },
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
pub fn cval(self: QuickTerminalSize) C {
|
||||
return .{
|
||||
.primary = if (self.primary) |p| switch (p) {
|
||||
.percentage => |v| .percentage(v),
|
||||
.pixels => |v| .pixels(v),
|
||||
} else .none,
|
||||
.secondary = if (self.secondary) |s| switch (s) {
|
||||
.percentage => |v| .percentage(v),
|
||||
.pixels => |v| .pixels(v),
|
||||
} else .none,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn calculate(
|
||||
self: QuickTerminalSize,
|
||||
position: QuickTerminalPosition,
|
||||
@@ -7268,6 +7343,7 @@ pub const QuickTerminalSize = struct {
|
||||
|
||||
try formatter.formatEntry([]const u8, fbs.getWritten());
|
||||
}
|
||||
|
||||
test "parse QuickTerminalSize" {
|
||||
const testing = std.testing;
|
||||
var v: QuickTerminalSize = undefined;
|
||||
@@ -7980,6 +8056,14 @@ pub const WindowPadding = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// See scroll-to-bottom
|
||||
pub const ScrollToBottom = packed struct {
|
||||
keystroke: bool = true,
|
||||
output: bool = false,
|
||||
|
||||
pub const default: ScrollToBottom = .{};
|
||||
};
|
||||
|
||||
test "parse duration" {
|
||||
inline for (Duration.units) |unit| {
|
||||
var buf: [16]u8 = undefined;
|
||||
|
||||
@@ -552,11 +552,15 @@ pub const Action = union(enum) {
|
||||
/// of the `confirm-close-surface` configuration setting.
|
||||
close_surface,
|
||||
|
||||
/// Close the current tab and all splits therein.
|
||||
/// Close the current tab and all splits therein _or_ close all tabs and
|
||||
/// splits thein of tabs _other_ than the current tab, depending on the
|
||||
/// mode.
|
||||
///
|
||||
/// If the mode is not specified, defaults to closing the current tab.
|
||||
///
|
||||
/// This might trigger a close confirmation popup, depending on the value
|
||||
/// of the `confirm-close-surface` configuration setting.
|
||||
close_tab,
|
||||
close_tab: CloseTabMode,
|
||||
|
||||
/// Close the current window and all tabs and splits therein.
|
||||
///
|
||||
@@ -858,6 +862,13 @@ pub const Action = union(enum) {
|
||||
hide,
|
||||
};
|
||||
|
||||
pub const CloseTabMode = enum {
|
||||
this,
|
||||
other,
|
||||
|
||||
pub const default: CloseTabMode = .this;
|
||||
};
|
||||
|
||||
fn parseEnum(comptime T: type, value: []const u8) !T {
|
||||
return std.meta.stringToEnum(T, value) orelse return Error.InvalidFormat;
|
||||
}
|
||||
|
||||
@@ -393,11 +393,18 @@ fn actionCommands(action: Action.Key) []const Command {
|
||||
.description = "Close the current terminal.",
|
||||
}},
|
||||
|
||||
.close_tab => comptime &.{.{
|
||||
.action = .close_tab,
|
||||
.title = "Close Tab",
|
||||
.description = "Close the current tab.",
|
||||
}},
|
||||
.close_tab => comptime &.{
|
||||
.{
|
||||
.action = .{ .close_tab = .this },
|
||||
.title = "Close Tab",
|
||||
.description = "Close the current tab.",
|
||||
},
|
||||
.{
|
||||
.action = .{ .close_tab = .other },
|
||||
.title = "Close Other Tabs",
|
||||
.description = "Close all tabs in this window except the current one.",
|
||||
},
|
||||
},
|
||||
|
||||
.close_window => comptime &.{.{
|
||||
.action = .close_window,
|
||||
|
||||
@@ -49,6 +49,7 @@ pub const locales = [_][:0]const u8{
|
||||
"ca_ES.UTF-8",
|
||||
"bg_BG.UTF-8",
|
||||
"ga_IE.UTF-8",
|
||||
"hu_HU.UTF-8",
|
||||
"he_IL.UTF-8",
|
||||
};
|
||||
|
||||
|
||||
@@ -522,6 +522,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
selection_background: ?configpkg.Config.TerminalColor,
|
||||
selection_foreground: ?configpkg.Config.TerminalColor,
|
||||
bold_color: ?configpkg.BoldColor,
|
||||
faint_opacity: u8,
|
||||
min_contrast: f32,
|
||||
padding_color: configpkg.WindowPaddingColor,
|
||||
custom_shaders: configpkg.RepeatablePath,
|
||||
@@ -584,6 +585,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
.background = config.background.toTerminalRGB(),
|
||||
.foreground = config.foreground.toTerminalRGB(),
|
||||
.bold_color = config.@"bold-color",
|
||||
.faint_opacity = @intFromFloat(@ceil(config.@"faint-opacity" * 255)),
|
||||
|
||||
.min_contrast = @floatCast(config.@"minimum-contrast"),
|
||||
.padding_color = config.@"window-padding-color",
|
||||
@@ -2225,23 +2227,44 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]);
|
||||
const cursor_height: f32 = @floatFromInt(cursor.glyph_size[1]);
|
||||
|
||||
// Left edge of the cell the cursor is in.
|
||||
var pixel_x: f32 = @floatFromInt(
|
||||
cursor.grid_pos[0] * cell.width + padding.left,
|
||||
);
|
||||
// Top edge, relative to the top of the
|
||||
// screen, of the cell the cursor is in.
|
||||
var pixel_y: f32 = @floatFromInt(
|
||||
cursor.grid_pos[1] * cell.height + padding.top,
|
||||
);
|
||||
|
||||
pixel_x += @floatFromInt(cursor.bearings[0]);
|
||||
pixel_y += @floatFromInt(cursor.bearings[1]);
|
||||
|
||||
// If +Y is up in our shaders, we need to flip the coordinate.
|
||||
// If +Y is up in our shaders, we need to flip the coordinate
|
||||
// so that it's instead the top edge of the cell relative to
|
||||
// the *bottom* of the screen.
|
||||
if (!GraphicsAPI.custom_shader_y_is_down) {
|
||||
pixel_y = @as(f32, @floatFromInt(screen.height)) - pixel_y;
|
||||
// We need to add the cursor height because we need the +Y
|
||||
// edge for the Y coordinate, and flipping means that it's
|
||||
// the -Y edge now.
|
||||
pixel_y += cursor_height;
|
||||
}
|
||||
|
||||
// Add the X bearing to get the -X (left) edge of the cursor.
|
||||
pixel_x += @floatFromInt(cursor.bearings[0]);
|
||||
|
||||
// How we deal with the Y bearing depends on which direction
|
||||
// is "up", since we want our final `pixel_y` value to be the
|
||||
// +Y edge of the cursor.
|
||||
if (GraphicsAPI.custom_shader_y_is_down) {
|
||||
// As a reminder, the Y bearing is the distance from the
|
||||
// bottom of the cell to the top of the glyph, so to get
|
||||
// the +Y edge we need to add the cell height, subtract
|
||||
// the Y bearing, and add the glyph height to get the +Y
|
||||
// (bottom) edge of the cursor.
|
||||
pixel_y += @floatFromInt(cell.height);
|
||||
pixel_y -= @floatFromInt(cursor.bearings[1]);
|
||||
pixel_y += @floatFromInt(cursor.glyph_size[1]);
|
||||
} else {
|
||||
// If the Y direction is reversed though, we instead want
|
||||
// the *top* edge of the cursor, which means we just need
|
||||
// to subtract the cell height and add the Y bearing.
|
||||
pixel_y -= @floatFromInt(cell.height);
|
||||
pixel_y += @floatFromInt(cursor.bearings[1]);
|
||||
}
|
||||
|
||||
const new_cursor: [4]f32 = .{
|
||||
@@ -2612,7 +2635,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
};
|
||||
|
||||
// Foreground alpha for this cell.
|
||||
const alpha: u8 = if (style.flags.faint) 175 else 255;
|
||||
const alpha: u8 = if (style.flags.faint) self.config.faint_opacity else 255;
|
||||
|
||||
// Set the cell's background color.
|
||||
{
|
||||
|
||||
@@ -193,7 +193,7 @@ pub const Action = union(enum) {
|
||||
/// Maximum number of intermediate characters during parsing. This is
|
||||
/// 4 because we also use the intermediates array for UTF8 decoding which
|
||||
/// can be at most 4 bytes.
|
||||
const MAX_INTERMEDIATE = 4;
|
||||
pub const MAX_INTERMEDIATE = 4;
|
||||
|
||||
/// Maximum number of CSI parameters. This is arbitrary. Practically, the
|
||||
/// only CSI command that uses more than 3 parameters is the SGR command
|
||||
@@ -206,7 +206,7 @@ const MAX_INTERMEDIATE = 4;
|
||||
/// number. I implore TUI authors to not use more than this number of CSI
|
||||
/// params, but I suspect we'll introduce a slow path with heap allocation
|
||||
/// one day.
|
||||
const MAX_PARAMS = 24;
|
||||
pub const MAX_PARAMS = 24;
|
||||
|
||||
/// Current state of the state machine
|
||||
state: State,
|
||||
@@ -949,6 +949,55 @@ test "csi: too many params" {
|
||||
}
|
||||
}
|
||||
|
||||
test "csi: sgr with up to our max parameters" {
|
||||
for (1..MAX_PARAMS + 1) |max| {
|
||||
var p = init();
|
||||
_ = p.next(0x1B);
|
||||
_ = p.next('[');
|
||||
|
||||
for (0..max - 1) |_| {
|
||||
_ = p.next('1');
|
||||
_ = p.next(';');
|
||||
}
|
||||
_ = p.next('2');
|
||||
|
||||
{
|
||||
const a = p.next('H');
|
||||
try testing.expect(p.state == .ground);
|
||||
try testing.expect(a[0] == null);
|
||||
try testing.expect(a[1].? == .csi_dispatch);
|
||||
try testing.expect(a[2] == null);
|
||||
|
||||
const csi = a[1].?.csi_dispatch;
|
||||
try testing.expectEqual(@as(usize, max), csi.params.len);
|
||||
try testing.expectEqual(@as(u16, 2), csi.params[max - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "csi: sgr beyond our max drops it" {
|
||||
// Has to be +2 for the loops below
|
||||
const max = MAX_PARAMS + 2;
|
||||
|
||||
var p = init();
|
||||
_ = p.next(0x1B);
|
||||
_ = p.next('[');
|
||||
|
||||
for (0..max - 1) |_| {
|
||||
_ = p.next('1');
|
||||
_ = p.next(';');
|
||||
}
|
||||
_ = p.next('2');
|
||||
|
||||
{
|
||||
const a = p.next('H');
|
||||
try testing.expect(p.state == .ground);
|
||||
try testing.expect(a[0] == null);
|
||||
try testing.expect(a[1] == null);
|
||||
try testing.expect(a[2] == null);
|
||||
}
|
||||
}
|
||||
|
||||
test "dcs: XTGETTCAP" {
|
||||
var p = init();
|
||||
_ = p.next(0x1B);
|
||||
|
||||
@@ -147,25 +147,28 @@ pub const Command = union(enum) {
|
||||
/// End a hyperlink (OSC 8)
|
||||
hyperlink_end: void,
|
||||
|
||||
/// Sleep (OSC 9;1)
|
||||
sleep: struct {
|
||||
/// ConEmu sleep (OSC 9;1)
|
||||
conemu_sleep: struct {
|
||||
duration_ms: u16,
|
||||
},
|
||||
|
||||
/// Show GUI message Box (OSC 9;2)
|
||||
show_message_box: []const u8,
|
||||
/// ConEmu show GUI message box (OSC 9;2)
|
||||
conemu_show_message_box: []const u8,
|
||||
|
||||
/// Change ConEmu tab (OSC 9;3)
|
||||
change_conemu_tab_title: union(enum) {
|
||||
reset: void,
|
||||
/// ConEmu change tab title (OSC 9;3)
|
||||
conemu_change_tab_title: union(enum) {
|
||||
reset,
|
||||
value: []const u8,
|
||||
},
|
||||
|
||||
/// Set progress state (OSC 9;4)
|
||||
progress_report: ProgressReport,
|
||||
/// ConEmu progress report (OSC 9;4)
|
||||
conemu_progress_report: ProgressReport,
|
||||
|
||||
/// Wait input (OSC 9;5)
|
||||
wait_input: void,
|
||||
/// ConEmu wait input (OSC 9;5)
|
||||
conemu_wait_input,
|
||||
|
||||
/// ConEmu GUI macro (OSC 9;6)
|
||||
conemu_guimacro: []const u8,
|
||||
|
||||
pub const ColorOperation = union(enum) {
|
||||
pub const Source = enum(u16) {
|
||||
@@ -208,7 +211,6 @@ pub const Command = union(enum) {
|
||||
};
|
||||
|
||||
pub const ProgressReport = struct {
|
||||
// sync with ghostty_terminal_osc_command_progressreport_state_e in include/ghostty.h
|
||||
pub const State = enum(c_int) {
|
||||
remove,
|
||||
set,
|
||||
@@ -220,7 +222,7 @@ pub const Command = union(enum) {
|
||||
state: State,
|
||||
progress: ?u8 = null,
|
||||
|
||||
// sync with ghostty_terminal_osc_command_progressreport_s in include/ghostty.h
|
||||
// sync with ghostty_action_progress_report_s
|
||||
pub const C = extern struct {
|
||||
state: c_int,
|
||||
progress: i8,
|
||||
@@ -229,7 +231,11 @@ pub const Command = union(enum) {
|
||||
pub fn cval(self: ProgressReport) C {
|
||||
return .{
|
||||
.state = @intFromEnum(self.state),
|
||||
.progress = if (self.progress) |progress| @intCast(std.math.clamp(progress, 0, 100)) else -1,
|
||||
.progress = if (self.progress) |progress| @intCast(std.math.clamp(
|
||||
progress,
|
||||
0,
|
||||
100,
|
||||
)) else -1,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -431,6 +437,7 @@ pub const Parser = struct {
|
||||
conemu_progress_state,
|
||||
conemu_progress_prevalue,
|
||||
conemu_progress_value,
|
||||
conemu_guimacro,
|
||||
};
|
||||
|
||||
pub fn init() Parser {
|
||||
@@ -957,107 +964,147 @@ pub const Parser = struct {
|
||||
.osc_9 => switch (c) {
|
||||
'1' => {
|
||||
self.state = .conemu_sleep;
|
||||
// This will end up being either a ConEmu sleep OSC 9;1,
|
||||
// or a desktop notification OSC 9 that begins with '1', so
|
||||
// mark as complete.
|
||||
self.complete = true;
|
||||
},
|
||||
'2' => {
|
||||
self.state = .conemu_message_box;
|
||||
// This will end up being either a ConEmu message box OSC 9;2,
|
||||
// or a desktop notification OSC 9 that begins with '2', so
|
||||
// mark as complete.
|
||||
self.complete = true;
|
||||
},
|
||||
'3' => {
|
||||
self.state = .conemu_tab;
|
||||
// This will end up being either a ConEmu message box OSC 9;3,
|
||||
// or a desktop notification OSC 9 that begins with '3', so
|
||||
// mark as complete.
|
||||
self.complete = true;
|
||||
},
|
||||
'4' => {
|
||||
self.state = .conemu_progress_prestate;
|
||||
// This will end up being either a ConEmu progress report
|
||||
// OSC 9;4, or a desktop notification OSC 9 that begins with
|
||||
// '4', so mark as complete.
|
||||
self.complete = true;
|
||||
},
|
||||
'5' => {
|
||||
// Note that sending an OSC 9 desktop notification that
|
||||
// starts with 5 is impossible due to this.
|
||||
self.state = .swallow;
|
||||
self.command = .{ .wait_input = {} };
|
||||
self.command = .conemu_wait_input;
|
||||
self.complete = true;
|
||||
},
|
||||
'6' => {
|
||||
self.state = .conemu_guimacro;
|
||||
// This will end up being either a ConEmu GUI macro OSC 9;6,
|
||||
// or a desktop notification OSC 9 that begins with '6', so
|
||||
// mark as complete.
|
||||
self.complete = true;
|
||||
},
|
||||
|
||||
// Todo: parse out other ConEmu operating system commands.
|
||||
// Even if we don't support them we probably don't want
|
||||
// them showing up as desktop notifications.
|
||||
// Todo: parse out other ConEmu operating system commands. Even
|
||||
// if we don't support them we probably don't want them showing
|
||||
// up as desktop notifications.
|
||||
|
||||
else => self.showDesktopNotification(),
|
||||
},
|
||||
|
||||
.conemu_sleep => switch (c) {
|
||||
';' => {
|
||||
self.command = .{ .sleep = .{ .duration_ms = 100 } };
|
||||
self.command = .{ .conemu_sleep = .{ .duration_ms = 100 } };
|
||||
self.buf_start = self.buf_idx;
|
||||
self.complete = true;
|
||||
self.state = .conemu_sleep_value;
|
||||
},
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
|
||||
.conemu_message_box => switch (c) {
|
||||
';' => {
|
||||
self.command = .{ .show_message_box = undefined };
|
||||
self.temp_state = .{ .str = &self.command.show_message_box };
|
||||
self.buf_start = self.buf_idx;
|
||||
self.complete = true;
|
||||
self.prepAllocableString();
|
||||
},
|
||||
else => self.state = .invalid,
|
||||
// OSC 9;1 <something other than semicolon> is a desktop
|
||||
// notification.
|
||||
else => self.showDesktopNotification(),
|
||||
},
|
||||
|
||||
.conemu_sleep_value => switch (c) {
|
||||
else => self.complete = true,
|
||||
},
|
||||
|
||||
.conemu_message_box => switch (c) {
|
||||
';' => {
|
||||
self.command = .{ .conemu_show_message_box = undefined };
|
||||
self.temp_state = .{ .str = &self.command.conemu_show_message_box };
|
||||
self.buf_start = self.buf_idx;
|
||||
self.complete = true;
|
||||
self.prepAllocableString();
|
||||
},
|
||||
|
||||
// OSC 9;2 <something other than semicolon> is a desktop
|
||||
// notification.
|
||||
else => self.showDesktopNotification(),
|
||||
},
|
||||
|
||||
.conemu_tab => switch (c) {
|
||||
';' => {
|
||||
self.state = .conemu_tab_txt;
|
||||
self.command = .{ .change_conemu_tab_title = .{ .reset = {} } };
|
||||
self.command = .{ .conemu_change_tab_title = .reset };
|
||||
self.buf_start = self.buf_idx;
|
||||
self.complete = true;
|
||||
},
|
||||
else => self.state = .invalid,
|
||||
|
||||
// OSC 9;3 <something other than semicolon> is a desktop
|
||||
// notification.
|
||||
else => self.showDesktopNotification(),
|
||||
},
|
||||
|
||||
.conemu_tab_txt => {
|
||||
self.command = .{ .change_conemu_tab_title = .{ .value = undefined } };
|
||||
self.temp_state = .{ .str = &self.command.change_conemu_tab_title.value };
|
||||
self.command = .{ .conemu_change_tab_title = .{ .value = undefined } };
|
||||
self.temp_state = .{ .str = &self.command.conemu_change_tab_title.value };
|
||||
self.complete = true;
|
||||
self.prepAllocableString();
|
||||
},
|
||||
|
||||
.conemu_progress_prestate => switch (c) {
|
||||
';' => {
|
||||
self.command = .{ .progress_report = .{
|
||||
self.command = .{ .conemu_progress_report = .{
|
||||
.state = undefined,
|
||||
} };
|
||||
self.state = .conemu_progress_state;
|
||||
},
|
||||
|
||||
// OSC 9;4 <something other than semicolon> is a desktop
|
||||
// notification.
|
||||
else => self.showDesktopNotification(),
|
||||
},
|
||||
|
||||
.conemu_progress_state => switch (c) {
|
||||
'0' => {
|
||||
self.command.progress_report.state = .remove;
|
||||
self.command.conemu_progress_report.state = .remove;
|
||||
self.state = .swallow;
|
||||
self.complete = true;
|
||||
},
|
||||
'1' => {
|
||||
self.command.progress_report.state = .set;
|
||||
self.command.progress_report.progress = 0;
|
||||
self.command.conemu_progress_report.state = .set;
|
||||
self.command.conemu_progress_report.progress = 0;
|
||||
self.state = .conemu_progress_prevalue;
|
||||
},
|
||||
'2' => {
|
||||
self.command.progress_report.state = .@"error";
|
||||
self.command.conemu_progress_report.state = .@"error";
|
||||
self.complete = true;
|
||||
self.state = .conemu_progress_prevalue;
|
||||
},
|
||||
'3' => {
|
||||
self.command.progress_report.state = .indeterminate;
|
||||
self.command.conemu_progress_report.state = .indeterminate;
|
||||
self.complete = true;
|
||||
self.state = .swallow;
|
||||
},
|
||||
'4' => {
|
||||
self.command.progress_report.state = .pause;
|
||||
self.command.conemu_progress_report.state = .pause;
|
||||
self.complete = true;
|
||||
self.state = .conemu_progress_prevalue;
|
||||
},
|
||||
|
||||
// OSC 9;4; <something other than 0-4> is a desktop
|
||||
// notification.
|
||||
else => self.showDesktopNotification(),
|
||||
},
|
||||
|
||||
@@ -1066,6 +1113,8 @@ pub const Parser = struct {
|
||||
self.state = .conemu_progress_value;
|
||||
},
|
||||
|
||||
// OSC 9;4;<0-4> <something other than semicolon> is a desktop
|
||||
// notification.
|
||||
else => self.showDesktopNotification(),
|
||||
},
|
||||
|
||||
@@ -1077,8 +1126,16 @@ pub const Parser = struct {
|
||||
|
||||
// If we aren't a set substate, then we don't care
|
||||
// about the value.
|
||||
const p = &self.command.progress_report;
|
||||
if (p.state != .set and p.state != .@"error" and p.state != .pause) break :value;
|
||||
const p = &self.command.conemu_progress_report;
|
||||
switch (p.state) {
|
||||
.remove,
|
||||
.indeterminate,
|
||||
=> break :value,
|
||||
.set,
|
||||
.@"error",
|
||||
.pause,
|
||||
=> {},
|
||||
}
|
||||
|
||||
if (p.state == .set)
|
||||
assert(p.progress != null)
|
||||
@@ -1104,6 +1161,20 @@ pub const Parser = struct {
|
||||
},
|
||||
},
|
||||
|
||||
.conemu_guimacro => switch (c) {
|
||||
';' => {
|
||||
self.command = .{ .conemu_guimacro = undefined };
|
||||
self.temp_state = .{ .str = &self.command.conemu_guimacro };
|
||||
self.buf_start = self.buf_idx;
|
||||
self.state = .string;
|
||||
self.complete = true;
|
||||
},
|
||||
|
||||
// OSC 9;6 <something other than semicolon> is a desktop
|
||||
// notification.
|
||||
else => self.showDesktopNotification(),
|
||||
},
|
||||
|
||||
.semantic_prompt => switch (c) {
|
||||
'A' => {
|
||||
self.state = .semantic_option_start;
|
||||
@@ -1212,6 +1283,11 @@ pub const Parser = struct {
|
||||
|
||||
self.temp_state = .{ .str = &self.command.show_desktop_notification.body };
|
||||
self.state = .string;
|
||||
// Set as complete as we've already seen one character that should be
|
||||
// part of the notification. If we wait for another character to set
|
||||
// `complete` when the state is `.string` we won't be able to send any
|
||||
// single character notifications.
|
||||
self.complete = true;
|
||||
}
|
||||
|
||||
fn prepAllocableString(self: *Parser) void {
|
||||
@@ -1332,7 +1408,7 @@ pub const Parser = struct {
|
||||
|
||||
fn endConEmuSleepValue(self: *Parser) void {
|
||||
switch (self.command) {
|
||||
.sleep => |*v| v.duration_ms = value: {
|
||||
.conemu_sleep => |*v| v.duration_ms = value: {
|
||||
const str = self.buf[self.buf_start..self.buf_idx];
|
||||
if (str.len == 0) break :value 100;
|
||||
|
||||
@@ -1595,6 +1671,26 @@ pub const Parser = struct {
|
||||
.hyperlink_uri => self.endHyperlink(),
|
||||
.string => self.endString(),
|
||||
.conemu_sleep_value => self.endConEmuSleepValue(),
|
||||
|
||||
// We received OSC 9;X ST, but nothing else, finish off as a
|
||||
// desktop notification with "X" as the body.
|
||||
.conemu_sleep,
|
||||
.conemu_message_box,
|
||||
.conemu_tab,
|
||||
.conemu_progress_prestate,
|
||||
.conemu_progress_state,
|
||||
.conemu_guimacro,
|
||||
=> {
|
||||
self.showDesktopNotification();
|
||||
self.endString();
|
||||
},
|
||||
|
||||
// A ConEmu progress report that has reached these states is
|
||||
// complete, don't do anything to them.
|
||||
.conemu_progress_prevalue,
|
||||
.conemu_progress_value,
|
||||
=> {},
|
||||
|
||||
.allocable_string => self.endAllocableString(),
|
||||
.kitty_color_protocol_key => self.endKittyColorProtocolOption(.key_only, true),
|
||||
.kitty_color_protocol_value => self.endKittyColorProtocolOption(.key_and_value, true),
|
||||
@@ -2770,7 +2866,7 @@ test "OSC: OSC104: empty palette index" {
|
||||
try std.testing.expect(it.next() == null);
|
||||
}
|
||||
|
||||
test "OSC: conemu sleep" {
|
||||
test "OSC: OSC 9;1 ConEmu sleep" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -2780,11 +2876,11 @@ test "OSC: conemu sleep" {
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
|
||||
try testing.expect(cmd == .sleep);
|
||||
try testing.expectEqual(420, cmd.sleep.duration_ms);
|
||||
try testing.expect(cmd == .conemu_sleep);
|
||||
try testing.expectEqual(420, cmd.conemu_sleep.duration_ms);
|
||||
}
|
||||
|
||||
test "OSC: conemu sleep with no value default to 100ms" {
|
||||
test "OSC: OSC 9;1 ConEmu sleep with no value default to 100ms" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -2794,11 +2890,11 @@ test "OSC: conemu sleep with no value default to 100ms" {
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
|
||||
try testing.expect(cmd == .sleep);
|
||||
try testing.expectEqual(100, cmd.sleep.duration_ms);
|
||||
try testing.expect(cmd == .conemu_sleep);
|
||||
try testing.expectEqual(100, cmd.conemu_sleep.duration_ms);
|
||||
}
|
||||
|
||||
test "OSC: conemu sleep cannot exceed 10000ms" {
|
||||
test "OSC: OSC 9;1 conemu sleep cannot exceed 10000ms" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -2808,11 +2904,11 @@ test "OSC: conemu sleep cannot exceed 10000ms" {
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
|
||||
try testing.expect(cmd == .sleep);
|
||||
try testing.expectEqual(10000, cmd.sleep.duration_ms);
|
||||
try testing.expect(cmd == .conemu_sleep);
|
||||
try testing.expectEqual(10000, cmd.conemu_sleep.duration_ms);
|
||||
}
|
||||
|
||||
test "OSC: conemu sleep invalid input" {
|
||||
test "OSC: OSC 9;1 conemu sleep invalid input" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -2822,11 +2918,39 @@ test "OSC: conemu sleep invalid input" {
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
|
||||
try testing.expect(cmd == .sleep);
|
||||
try testing.expectEqual(100, cmd.sleep.duration_ms);
|
||||
try testing.expect(cmd == .conemu_sleep);
|
||||
try testing.expectEqual(100, cmd.conemu_sleep.duration_ms);
|
||||
}
|
||||
|
||||
test "OSC: show desktop notification" {
|
||||
test "OSC: OSC 9;1 conemu sleep -> desktop notification 1" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
|
||||
const input = "9;1";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
|
||||
try testing.expect(cmd == .show_desktop_notification);
|
||||
try testing.expectEqualStrings("1", cmd.show_desktop_notification.body);
|
||||
}
|
||||
|
||||
test "OSC: OSC 9;1 conemu sleep -> desktop notification 2" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
|
||||
const input = "9;1a";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
|
||||
try testing.expect(cmd == .show_desktop_notification);
|
||||
try testing.expectEqualStrings("1a", cmd.show_desktop_notification.body);
|
||||
}
|
||||
|
||||
test "OSC: OSC 9 show desktop notification" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -2836,11 +2960,25 @@ test "OSC: show desktop notification" {
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .show_desktop_notification);
|
||||
try testing.expectEqualStrings(cmd.show_desktop_notification.title, "");
|
||||
try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Hello world");
|
||||
try testing.expectEqualStrings("", cmd.show_desktop_notification.title);
|
||||
try testing.expectEqualStrings("Hello world", cmd.show_desktop_notification.body);
|
||||
}
|
||||
|
||||
test "OSC: show desktop notification with title" {
|
||||
test "OSC: OSC 9 show single character desktop notification" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
|
||||
const input = "9;H";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .show_desktop_notification);
|
||||
try testing.expectEqualStrings("", cmd.show_desktop_notification.title);
|
||||
try testing.expectEqualStrings("H", cmd.show_desktop_notification.body);
|
||||
}
|
||||
|
||||
test "OSC: OSC 777 show desktop notification with title" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -2854,7 +2992,7 @@ test "OSC: show desktop notification with title" {
|
||||
try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Body");
|
||||
}
|
||||
|
||||
test "OSC: conemu message box" {
|
||||
test "OSC: OSC 9;2 ConEmu message box" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -2863,11 +3001,11 @@ test "OSC: conemu message box" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .show_message_box);
|
||||
try testing.expectEqualStrings("hello world", cmd.show_message_box);
|
||||
try testing.expect(cmd == .conemu_show_message_box);
|
||||
try testing.expectEqualStrings("hello world", cmd.conemu_show_message_box);
|
||||
}
|
||||
|
||||
test "OSC: conemu message box invalid input" {
|
||||
test "OSC: 9;2 ConEmu message box invalid input" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -2875,11 +3013,12 @@ test "OSC: conemu message box invalid input" {
|
||||
const input = "9;2";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b');
|
||||
try testing.expect(cmd == null);
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .show_desktop_notification);
|
||||
try testing.expectEqualStrings("2", cmd.show_desktop_notification.body);
|
||||
}
|
||||
|
||||
test "OSC: conemu message box empty message" {
|
||||
test "OSC: 9;2 ConEmu message box empty message" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -2888,11 +3027,11 @@ test "OSC: conemu message box empty message" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .show_message_box);
|
||||
try testing.expectEqualStrings("", cmd.show_message_box);
|
||||
try testing.expect(cmd == .conemu_show_message_box);
|
||||
try testing.expectEqualStrings("", cmd.conemu_show_message_box);
|
||||
}
|
||||
|
||||
test "OSC: conemu message box spaces only message" {
|
||||
test "OSC: 9;2 ConEmu message box spaces only message" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -2901,11 +3040,39 @@ test "OSC: conemu message box spaces only message" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .show_message_box);
|
||||
try testing.expectEqualStrings(" ", cmd.show_message_box);
|
||||
try testing.expect(cmd == .conemu_show_message_box);
|
||||
try testing.expectEqualStrings(" ", cmd.conemu_show_message_box);
|
||||
}
|
||||
|
||||
test "OSC: conemu change tab title" {
|
||||
test "OSC: OSC 9;2 message box -> desktop notification 1" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
|
||||
const input = "9;2";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
|
||||
try testing.expect(cmd == .show_desktop_notification);
|
||||
try testing.expectEqualStrings("2", cmd.show_desktop_notification.body);
|
||||
}
|
||||
|
||||
test "OSC: OSC 9;2 message box -> desktop notification 2" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
|
||||
const input = "9;2a";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
|
||||
try testing.expect(cmd == .show_desktop_notification);
|
||||
try testing.expectEqualStrings("2a", cmd.show_desktop_notification.body);
|
||||
}
|
||||
|
||||
test "OSC: 9;3 ConEmu change tab title" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -2914,11 +3081,11 @@ test "OSC: conemu change tab title" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .change_conemu_tab_title);
|
||||
try testing.expectEqualStrings("foo bar", cmd.change_conemu_tab_title.value);
|
||||
try testing.expect(cmd == .conemu_change_tab_title);
|
||||
try testing.expectEqualStrings("foo bar", cmd.conemu_change_tab_title.value);
|
||||
}
|
||||
|
||||
test "OSC: conemu change tab reset title" {
|
||||
test "OSC: 9;3 ConEmu change tab title reset" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -2928,11 +3095,11 @@ test "OSC: conemu change tab reset title" {
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
|
||||
const expected_command: Command = .{ .change_conemu_tab_title = .{ .reset = {} } };
|
||||
const expected_command: Command = .{ .conemu_change_tab_title = .reset };
|
||||
try testing.expectEqual(expected_command, cmd);
|
||||
}
|
||||
|
||||
test "OSC: conemu change tab spaces only title" {
|
||||
test "OSC: 9;3 ConEmu change tab title spaces only" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -2942,11 +3109,11 @@ test "OSC: conemu change tab spaces only title" {
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
|
||||
try testing.expect(cmd == .change_conemu_tab_title);
|
||||
try testing.expectEqualStrings(" ", cmd.change_conemu_tab_title.value);
|
||||
try testing.expect(cmd == .conemu_change_tab_title);
|
||||
try testing.expectEqualStrings(" ", cmd.conemu_change_tab_title.value);
|
||||
}
|
||||
|
||||
test "OSC: conemu change tab invalid input" {
|
||||
test "OSC: OSC 9;3 change tab title -> desktop notification 1" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -2954,11 +3121,27 @@ test "OSC: conemu change tab invalid input" {
|
||||
const input = "9;3";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b');
|
||||
try testing.expect(cmd == null);
|
||||
const cmd = p.end('\x1b').?;
|
||||
|
||||
try testing.expect(cmd == .show_desktop_notification);
|
||||
try testing.expectEqualStrings("3", cmd.show_desktop_notification.body);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress set" {
|
||||
test "OSC: OSC 9;3 message box -> desktop notification 2" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
|
||||
const input = "9;3a";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
|
||||
try testing.expect(cmd == .show_desktop_notification);
|
||||
try testing.expectEqualStrings("3a", cmd.show_desktop_notification.body);
|
||||
}
|
||||
|
||||
test "OSC: OSC 9;4 ConEmu progress set" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -2967,12 +3150,12 @@ test "OSC: OSC9 progress set" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress_report);
|
||||
try testing.expect(cmd.progress_report.state == .set);
|
||||
try testing.expect(cmd.progress_report.progress == 100);
|
||||
try testing.expect(cmd == .conemu_progress_report);
|
||||
try testing.expect(cmd.conemu_progress_report.state == .set);
|
||||
try testing.expect(cmd.conemu_progress_report.progress == 100);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress set overflow" {
|
||||
test "OSC: OSC 9;4 ConEmu progress set overflow" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -2981,12 +3164,12 @@ test "OSC: OSC9 progress set overflow" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress_report);
|
||||
try testing.expect(cmd.progress_report.state == .set);
|
||||
try testing.expect(cmd.progress_report.progress == 100);
|
||||
try testing.expect(cmd == .conemu_progress_report);
|
||||
try testing.expect(cmd.conemu_progress_report.state == .set);
|
||||
try testing.expectEqual(100, cmd.conemu_progress_report.progress);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress set single digit" {
|
||||
test "OSC: OSC 9;4 ConEmu progress set single digit" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -2995,12 +3178,12 @@ test "OSC: OSC9 progress set single digit" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress_report);
|
||||
try testing.expect(cmd.progress_report.state == .set);
|
||||
try testing.expect(cmd.progress_report.progress == 9);
|
||||
try testing.expect(cmd == .conemu_progress_report);
|
||||
try testing.expect(cmd.conemu_progress_report.state == .set);
|
||||
try testing.expect(cmd.conemu_progress_report.progress == 9);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress set double digit" {
|
||||
test "OSC: OSC 9;4 ConEmu progress set double digit" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -3009,12 +3192,12 @@ test "OSC: OSC9 progress set double digit" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress_report);
|
||||
try testing.expect(cmd.progress_report.state == .set);
|
||||
try testing.expect(cmd.progress_report.progress == 94);
|
||||
try testing.expect(cmd == .conemu_progress_report);
|
||||
try testing.expect(cmd.conemu_progress_report.state == .set);
|
||||
try testing.expectEqual(94, cmd.conemu_progress_report.progress);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress set extra semicolon ignored" {
|
||||
test "OSC: OSC 9;4 ConEmu progress set extra semicolon ignored" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -3023,12 +3206,12 @@ test "OSC: OSC9 progress set extra semicolon ignored" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress_report);
|
||||
try testing.expect(cmd.progress_report.state == .set);
|
||||
try testing.expect(cmd.progress_report.progress == 100);
|
||||
try testing.expect(cmd == .conemu_progress_report);
|
||||
try testing.expect(cmd.conemu_progress_report.state == .set);
|
||||
try testing.expectEqual(100, cmd.conemu_progress_report.progress);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress remove with no progress" {
|
||||
test "OSC: OSC 9;4 ConEmu progress remove with no progress" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -3037,12 +3220,12 @@ test "OSC: OSC9 progress remove with no progress" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress_report);
|
||||
try testing.expect(cmd.progress_report.state == .remove);
|
||||
try testing.expect(cmd.progress_report.progress == null);
|
||||
try testing.expect(cmd == .conemu_progress_report);
|
||||
try testing.expect(cmd.conemu_progress_report.state == .remove);
|
||||
try testing.expect(cmd.conemu_progress_report.progress == null);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress remove with double semicolon" {
|
||||
test "OSC: OSC 9;4 ConEmu progress remove with double semicolon" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -3051,12 +3234,12 @@ test "OSC: OSC9 progress remove with double semicolon" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress_report);
|
||||
try testing.expect(cmd.progress_report.state == .remove);
|
||||
try testing.expect(cmd.progress_report.progress == null);
|
||||
try testing.expect(cmd == .conemu_progress_report);
|
||||
try testing.expect(cmd.conemu_progress_report.state == .remove);
|
||||
try testing.expect(cmd.conemu_progress_report.progress == null);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress remove ignores progress" {
|
||||
test "OSC: OSC 9;4 ConEmu progress remove ignores progress" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -3065,12 +3248,12 @@ test "OSC: OSC9 progress remove ignores progress" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress_report);
|
||||
try testing.expect(cmd.progress_report.state == .remove);
|
||||
try testing.expect(cmd.progress_report.progress == null);
|
||||
try testing.expect(cmd == .conemu_progress_report);
|
||||
try testing.expect(cmd.conemu_progress_report.state == .remove);
|
||||
try testing.expect(cmd.conemu_progress_report.progress == null);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress remove extra semicolon" {
|
||||
test "OSC: OSC 9;4 ConEmu progress remove extra semicolon" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -3079,11 +3262,11 @@ test "OSC: OSC9 progress remove extra semicolon" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress_report);
|
||||
try testing.expect(cmd.progress_report.state == .remove);
|
||||
try testing.expect(cmd == .conemu_progress_report);
|
||||
try testing.expect(cmd.conemu_progress_report.state == .remove);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress error" {
|
||||
test "OSC: OSC 9;4 ConEmu progress error" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -3092,12 +3275,12 @@ test "OSC: OSC9 progress error" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress_report);
|
||||
try testing.expect(cmd.progress_report.state == .@"error");
|
||||
try testing.expect(cmd.progress_report.progress == null);
|
||||
try testing.expect(cmd == .conemu_progress_report);
|
||||
try testing.expect(cmd.conemu_progress_report.state == .@"error");
|
||||
try testing.expect(cmd.conemu_progress_report.progress == null);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress error with progress" {
|
||||
test "OSC: OSC 9;4 ConEmu progress error with progress" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -3106,12 +3289,12 @@ test "OSC: OSC9 progress error with progress" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress_report);
|
||||
try testing.expect(cmd.progress_report.state == .@"error");
|
||||
try testing.expect(cmd.progress_report.progress == 100);
|
||||
try testing.expect(cmd == .conemu_progress_report);
|
||||
try testing.expect(cmd.conemu_progress_report.state == .@"error");
|
||||
try testing.expect(cmd.conemu_progress_report.progress == 100);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress pause" {
|
||||
test "OSC: OSC 9;4 progress pause" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -3120,12 +3303,12 @@ test "OSC: OSC9 progress pause" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress_report);
|
||||
try testing.expect(cmd.progress_report.state == .pause);
|
||||
try testing.expect(cmd.progress_report.progress == null);
|
||||
try testing.expect(cmd == .conemu_progress_report);
|
||||
try testing.expect(cmd.conemu_progress_report.state == .pause);
|
||||
try testing.expect(cmd.conemu_progress_report.progress == null);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 progress pause with progress" {
|
||||
test "OSC: OSC 9;4 ConEmu progress pause with progress" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -3134,12 +3317,68 @@ test "OSC: OSC9 progress pause with progress" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress_report);
|
||||
try testing.expect(cmd.progress_report.state == .pause);
|
||||
try testing.expect(cmd.progress_report.progress == 100);
|
||||
try testing.expect(cmd == .conemu_progress_report);
|
||||
try testing.expect(cmd.conemu_progress_report.state == .pause);
|
||||
try testing.expect(cmd.conemu_progress_report.progress == 100);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 conemu wait input" {
|
||||
test "OSC: OSC 9;4 progress -> desktop notification 1" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
|
||||
const input = "9;4";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
|
||||
try testing.expect(cmd == .show_desktop_notification);
|
||||
try testing.expectEqualStrings("4", cmd.show_desktop_notification.body);
|
||||
}
|
||||
|
||||
test "OSC: OSC 9;4 progress -> desktop notification 2" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
|
||||
const input = "9;4;";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
|
||||
try testing.expect(cmd == .show_desktop_notification);
|
||||
try testing.expectEqualStrings("4;", cmd.show_desktop_notification.body);
|
||||
}
|
||||
|
||||
test "OSC: OSC 9;4 progress -> desktop notification 3" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
|
||||
const input = "9;4;5";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
|
||||
try testing.expect(cmd == .show_desktop_notification);
|
||||
try testing.expectEqualStrings("4;5", cmd.show_desktop_notification.body);
|
||||
}
|
||||
|
||||
test "OSC: OSC 9;4 progress -> desktop notification 4" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
|
||||
const input = "9;4;5a";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
|
||||
try testing.expect(cmd == .show_desktop_notification);
|
||||
try testing.expectEqualStrings("4;5a", cmd.show_desktop_notification.body);
|
||||
}
|
||||
|
||||
test "OSC: OSC 9;5 ConEmu wait input" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -3148,10 +3387,10 @@ test "OSC: OSC9 conemu wait input" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .wait_input);
|
||||
try testing.expect(cmd == .conemu_wait_input);
|
||||
}
|
||||
|
||||
test "OSC: OSC9 conemu wait ignores trailing characters" {
|
||||
test "OSC: OSC 9;5 ConEmu wait ignores trailing characters" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init();
|
||||
@@ -3160,7 +3399,7 @@ test "OSC: OSC9 conemu wait ignores trailing characters" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .wait_input);
|
||||
try testing.expect(cmd == .conemu_wait_input);
|
||||
}
|
||||
|
||||
test "OSC: empty param" {
|
||||
@@ -3415,3 +3654,45 @@ test "OSC: kitty color protocol no key" {
|
||||
try testing.expect(cmd == .kitty_color_protocol);
|
||||
try testing.expectEqual(0, cmd.kitty_color_protocol.list.items.len);
|
||||
}
|
||||
|
||||
test "OSC: 9;6: ConEmu guimacro 1" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .initAlloc(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "9;6;a";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .conemu_guimacro);
|
||||
try testing.expectEqualStrings("a", cmd.conemu_guimacro);
|
||||
}
|
||||
|
||||
test "OSC: 9;6: ConEmu guimacro 2" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .initAlloc(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "9;6;ab";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .conemu_guimacro);
|
||||
try testing.expectEqualStrings("ab", cmd.conemu_guimacro);
|
||||
}
|
||||
|
||||
test "OSC: 9;6: ConEmu guimacro 3 incomplete -> desktop notification" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .initAlloc(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "9;6";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .show_desktop_notification);
|
||||
try testing.expectEqualStrings("6", cmd.show_desktop_notification.body);
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ pub const Parser = struct {
|
||||
self.idx += 1;
|
||||
return .{ .unknown = .{
|
||||
.full = self.params,
|
||||
.partial = slice[0 .. self.idx - start + 1],
|
||||
.partial = slice[0..@min(self.idx - start + 1, slice.len)],
|
||||
} };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -249,7 +249,7 @@ pub fn Stream(comptime Handler: type) type {
|
||||
// the parser state to ground.
|
||||
0x18, 0x1A => self.parser.state = .ground,
|
||||
// A parameter digit:
|
||||
'0'...'9' => if (self.parser.params_idx < 16) {
|
||||
'0'...'9' => if (self.parser.params_idx < Parser.MAX_PARAMS) {
|
||||
self.parser.param_acc *|= 10;
|
||||
self.parser.param_acc +|= c - '0';
|
||||
// The parser's CSI param action uses param_acc_idx
|
||||
@@ -259,7 +259,7 @@ pub fn Stream(comptime Handler: type) type {
|
||||
self.parser.param_acc_idx |= 1;
|
||||
},
|
||||
// A parameter separator:
|
||||
':', ';' => if (self.parser.params_idx < 16) {
|
||||
':', ';' => if (self.parser.params_idx < Parser.MAX_PARAMS) {
|
||||
self.parser.params[self.parser.params_idx] = self.parser.param_acc;
|
||||
if (c == ':') self.parser.params_sep.set(self.parser.params_idx);
|
||||
self.parser.params_idx += 1;
|
||||
@@ -1598,14 +1598,19 @@ pub fn Stream(comptime Handler: type) type {
|
||||
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||
},
|
||||
|
||||
.progress_report => |v| {
|
||||
.conemu_progress_report => |v| {
|
||||
if (@hasDecl(T, "handleProgressReport")) {
|
||||
try self.handler.handleProgressReport(v);
|
||||
return;
|
||||
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||
},
|
||||
|
||||
.sleep, .show_message_box, .change_conemu_tab_title, .wait_input => {
|
||||
.conemu_sleep,
|
||||
.conemu_show_message_box,
|
||||
.conemu_change_tab_title,
|
||||
.conemu_wait_input,
|
||||
.conemu_guimacro,
|
||||
=> {
|
||||
log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||
},
|
||||
|
||||
@@ -2596,3 +2601,22 @@ test "stream CSI ? W reset tab stops" {
|
||||
try s.nextSlice("\x1b[?1;2;3W");
|
||||
try testing.expect(s.handler.reset);
|
||||
}
|
||||
|
||||
test "stream: SGR with 17+ parameters for underline color" {
|
||||
const H = struct {
|
||||
attrs: ?sgr.Attribute = null,
|
||||
called: bool = false,
|
||||
|
||||
pub fn setAttribute(self: *@This(), attr: sgr.Attribute) !void {
|
||||
self.attrs = attr;
|
||||
self.called = true;
|
||||
}
|
||||
};
|
||||
|
||||
var s: Stream(H) = .init(.{});
|
||||
|
||||
// Kakoune-style SGR with underline color as 17th parameter
|
||||
// This tests the fix where param 17 was being dropped
|
||||
try s.nextSlice("\x1b[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136;0m");
|
||||
try testing.expect(s.handler.called);
|
||||
}
|
||||
|
||||
@@ -1513,7 +1513,8 @@ fn execCommand(
|
||||
}
|
||||
|
||||
return switch (command) {
|
||||
.direct => |v| v,
|
||||
// We need to clone the command since there's no guarantee the config remains valid.
|
||||
.direct => |_| (try command.clone(alloc)).direct,
|
||||
|
||||
.shell => |v| shell: {
|
||||
var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 4);
|
||||
@@ -1688,3 +1689,35 @@ test "execCommand: direct command, error passwd" {
|
||||
try testing.expectEqualStrings(result[0], "foo");
|
||||
try testing.expectEqualStrings(result[1], "bar baz");
|
||||
}
|
||||
|
||||
test "execCommand: direct command, config freed" {
|
||||
if (comptime builtin.os.tag == .windows) return error.SkipZigTest;
|
||||
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var command_arena = ArenaAllocator.init(alloc);
|
||||
const command_alloc = command_arena.allocator();
|
||||
const command = try (configpkg.Command{
|
||||
.direct = &.{
|
||||
"foo",
|
||||
"bar baz",
|
||||
},
|
||||
}).clone(command_alloc);
|
||||
|
||||
const result = try execCommand(alloc, command, struct {
|
||||
fn get(_: Allocator) !PasswdEntry {
|
||||
// Failed passwd entry means we can't construct a macOS
|
||||
// login command and falls back to POSIX behavior.
|
||||
return error.Fail;
|
||||
}
|
||||
});
|
||||
|
||||
command_arena.deinit();
|
||||
|
||||
try testing.expectEqual(2, result.len);
|
||||
try testing.expectEqualStrings(result[0], "foo");
|
||||
try testing.expectEqualStrings(result[1], "bar baz");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user