diff options
| author | RME <ruben@rme.gg> | 2025-06-30 15:05:01 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-30 15:05:01 +0200 |
| commit | 9aa2383e05703f2d0bdf84a33ead5853089f8071 (patch) | |
| tree | 8490b40db0a332adb286bf93938648561523759a /src | |
| parent | 6484df913435377ef9aec6b9519d639af57a4ab4 (diff) | |
| parent | 259228698873c0c934741445ec6790cfafb64502 (diff) | |
Merge branch 'main' into ko_kr
Diffstat (limited to 'src')
| -rw-r--r-- | src/Surface.zig | 5 | ||||
| -rw-r--r-- | src/apprt/gtk/ClipboardConfirmationWindow.zig | 36 | ||||
| -rw-r--r-- | src/apprt/gtk/adw_version.zig | 4 | ||||
| -rw-r--r-- | src/apprt/gtk/style.css | 6 | ||||
| -rw-r--r-- | src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp | 92 | ||||
| -rw-r--r-- | src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp | 90 | ||||
| -rw-r--r-- | src/cli.zig | 2 | ||||
| -rw-r--r-- | src/cli/args.zig | 200 | ||||
| -rw-r--r-- | src/config/Config.zig | 43 | ||||
| -rw-r--r-- | src/input/Binding.zig | 5 | ||||
| -rw-r--r-- | src/input/command.zig | 10 | ||||
| -rw-r--r-- | src/os/i18n.zig | 2 | ||||
| -rw-r--r-- | src/terminal/Screen.zig | 309 | ||||
| -rw-r--r-- | src/termio/Termio.zig | 3 | ||||
| -rw-r--r-- | src/termio/stream_handler.zig | 12 |
15 files changed, 466 insertions, 353 deletions
diff --git a/src/Surface.zig b/src/Surface.zig index 286d81383..5acec8c00 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4845,6 +4845,11 @@ fn writeScreenFile( const path = try tmp_dir.dir.realpath(filename, &path_buf); switch (write_action) { + .copy => { + const pathZ = try self.alloc.dupeZ(u8, path); + defer self.alloc.free(pathZ); + try self.rt_surface.setClipboardString(pathZ, .standard, false); + }, .open => try internal_os.open(self.alloc, .text, path), .paste => self.io.queueMessage(try termio.Message.writeReq( self.alloc, diff --git a/src/apprt/gtk/ClipboardConfirmationWindow.zig b/src/apprt/gtk/ClipboardConfirmationWindow.zig index fab1aa893..bf1549021 100644 --- a/src/apprt/gtk/ClipboardConfirmationWindow.zig +++ b/src/apprt/gtk/ClipboardConfirmationWindow.zig @@ -17,7 +17,7 @@ const adw_version = @import("adw_version.zig"); const log = std.log.scoped(.gtk); -const DialogType = if (adw_version.atLeast(1, 5, 0)) adw.AlertDialog else adw.MessageDialog; +const DialogType = if (adw_version.supportsDialogs()) adw.AlertDialog else adw.MessageDialog; app: *App, dialog: *DialogType, @@ -28,6 +28,7 @@ text_view: *gtk.TextView, text_view_scroll: *gtk.ScrolledWindow, reveal_button: *gtk.Button, hide_button: *gtk.Button, +remember_choice: if (adw_version.supportsSwitchRow()) ?*adw.SwitchRow else ?*anyopaque, pub fn create( app: *App, @@ -89,6 +90,10 @@ fn init( const reveal_button = builder.getObject(gtk.Button, "reveal_button").?; const hide_button = builder.getObject(gtk.Button, "hide_button").?; const text_view_scroll = builder.getObject(gtk.ScrolledWindow, "text_view_scroll").?; + const remember_choice = if (adw_version.supportsSwitchRow()) + builder.getObject(adw.SwitchRow, "remember_choice") + else + null; const copy = try app.core_app.alloc.dupeZ(u8, data); errdefer app.core_app.alloc.free(copy); @@ -102,6 +107,7 @@ fn init( .text_view_scroll = text_view_scroll, .reveal_button = reveal_button, .hide_button = hide_button, + .remember_choice = remember_choice, }; const buffer = gtk.TextBuffer.new(null); @@ -152,8 +158,10 @@ fn init( } } -fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.c) void { - if (std.mem.orderZ(u8, response, "ok") == .eq) { +fn handleResponse(self: *ClipboardConfirmation, response: [*:0]const u8) void { + const is_ok = std.mem.orderZ(u8, response, "ok") == .eq; + + if (is_ok) { self.core_surface.completeClipboardRequest( self.pending_req, self.data, @@ -162,8 +170,30 @@ fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) log.err("Failed to requeue clipboard request: {}", .{err}); }; } + + if (self.remember_choice) |remember| remember: { + if (!adw_version.supportsSwitchRow()) break :remember; + if (remember.getActive() == 0) break :remember; + + switch (self.pending_req) { + .osc_52_read => self.core_surface.config.clipboard_read = if (is_ok) .allow else .deny, + .osc_52_write => self.core_surface.config.clipboard_write = if (is_ok) .allow else .deny, + .paste => {}, + } + } + self.destroy(); } +fn gtkChoose(dialog_: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.C) void { + const dialog = gobject.ext.cast(DialogType, dialog_.?).?; + const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud.?)); + const response = dialog.chooseFinish(result); + self.handleResponse(response); +} + +fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.C) void { + self.handleResponse(response); +} fn gtkRevealButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.c) void { self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true)); diff --git a/src/apprt/gtk/adw_version.zig b/src/apprt/gtk/adw_version.zig index ff7439a21..7ce88f585 100644 --- a/src/apprt/gtk/adw_version.zig +++ b/src/apprt/gtk/adw_version.zig @@ -109,6 +109,10 @@ pub inline fn supportsTabOverview() bool { return atLeast(1, 4, 0); } +pub inline fn supportsSwitchRow() bool { + return atLeast(1, 4, 0); +} + pub inline fn supportsToolbarView() bool { return atLeast(1, 4, 0); } diff --git a/src/apprt/gtk/style.css b/src/apprt/gtk/style.css index 7c4b53d03..2051ab1e3 100644 --- a/src/apprt/gtk/style.css +++ b/src/apprt/gtk/style.css @@ -64,14 +64,18 @@ window.ssd.no-border-radius { padding: 0; } +.clipboard-overlay { + border-radius: 10px; +} + .clipboard-content-view { filter: blur(0px); transition: filter 0.3s ease; + border-radius: 10px; } .clipboard-content-view.blurred { filter: blur(5px); - transition: filter 0.3s ease; } .command-palette-search { diff --git a/src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp b/src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp index 640556535..ad0b5c01f 100644 --- a/src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp +++ b/src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp @@ -14,58 +14,72 @@ Adw.AlertDialog clipboard_confirmation_window { default-response: "cancel"; close-response: "cancel"; - extra-child: Overlay { + extra-child: ListBox { + selection-mode: none; + styles [ - "osd", + "boxed-list-separate", ] - ScrolledWindow text_view_scroll { - width-request: 500; - height-request: 250; + Overlay { + styles [ + "osd", + "clipboard-overlay", + ] - TextView text_view { - cursor-visible: false; - editable: false; - monospace: true; - top-margin: 8; - left-margin: 8; - bottom-margin: 8; - right-margin: 8; + ScrolledWindow text_view_scroll { + width-request: 500; + height-request: 200; - styles [ - "clipboard-content-view", - ] + TextView text_view { + cursor-visible: false; + editable: false; + monospace: true; + top-margin: 8; + left-margin: 8; + bottom-margin: 8; + right-margin: 8; + + styles [ + "clipboard-content-view", + ] + } } - } - [overlay] - Button reveal_button { - visible: false; - halign: end; - valign: start; - margin-end: 12; - margin-top: 12; + [overlay] + Button reveal_button { + visible: false; + halign: end; + valign: start; + margin-end: 12; + margin-top: 12; - Image { - icon-name: "view-reveal-symbolic"; + Image { + icon-name: "view-reveal-symbolic"; + } } - } - [overlay] - Button hide_button { - visible: false; - halign: end; - valign: start; - margin-end: 12; - margin-top: 12; + [overlay] + Button hide_button { + visible: false; + halign: end; + valign: start; + margin-end: 12; + margin-top: 12; - styles [ - "opaque", - ] + styles [ + "opaque", + ] - Image { - icon-name: "view-conceal-symbolic"; + Image { + icon-name: "view-conceal-symbolic"; + } } } + + Adw.SwitchRow remember_choice { + title: _("Remember choice for this split"); + subtitle: _("Reload configuration to show this prompt again"); + } }; } diff --git a/src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp b/src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp index 2e28359ff..b71131940 100644 --- a/src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp +++ b/src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp @@ -14,58 +14,68 @@ Adw.AlertDialog clipboard_confirmation_window { default-response: "cancel"; close-response: "cancel"; - extra-child: Overlay { + extra-child: ListBox { + selection-mode: none; + styles [ - "osd", + "boxed-list-separate", ] - ScrolledWindow text_view_scroll { - width-request: 500; - height-request: 250; + Overlay { + styles [ + "osd", + "clipboard-overlay", + ] - TextView text_view { - cursor-visible: false; - editable: false; - monospace: true; - top-margin: 8; - left-margin: 8; - bottom-margin: 8; - right-margin: 8; + ScrolledWindow text_view_scroll { + width-request: 500; + height-request: 200; - styles [ - "clipboard-content-view", - ] + TextView text_view { + cursor-visible: false; + editable: false; + monospace: true; + top-margin: 8; + left-margin: 8; + bottom-margin: 8; + right-margin: 8; + + styles [ + "clipboard-content-view", + ] + } } - } - [overlay] - Button reveal_button { - visible: false; - halign: end; - valign: start; - margin-end: 12; - margin-top: 12; + [overlay] + Button reveal_button { + visible: false; + halign: end; + valign: start; + margin-end: 12; + margin-top: 12; - Image { - icon-name: "view-reveal-symbolic"; + Image { + icon-name: "view-reveal-symbolic"; + } } - } - [overlay] - Button hide_button { - visible: false; - halign: end; - valign: start; - margin-end: 12; - margin-top: 12; + [overlay] + Button hide_button { + visible: false; + halign: end; + valign: start; + margin-end: 12; + margin-top: 12; - styles [ - "opaque", - ] - - Image { - icon-name: "view-conceal-symbolic"; + styles [ + "opaque", + ] } } + + Adw.SwitchRow remember_choice { + title: _("Remember choice for this split"); + subtitle: _("Reload configuration to show this prompt again"); + } }; } diff --git a/src/cli.zig b/src/cli.zig index 4336501a8..151e6e648 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -2,6 +2,8 @@ const diags = @import("cli/diagnostics.zig"); pub const args = @import("cli/args.zig"); pub const Action = @import("cli/action.zig").Action; +pub const CompatibilityHandler = args.CompatibilityHandler; +pub const compatibilityRenamed = args.compatibilityRenamed; pub const DiagnosticList = diags.DiagnosticList; pub const Diagnostic = diags.Diagnostic; pub const Location = diags.Location; diff --git a/src/cli/args.zig b/src/cli/args.zig index 3c34e17fe..1af74df69 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -40,11 +40,14 @@ pub const Error = error{ /// "DiagnosticList" and any diagnostic messages will be added to that list. /// When diagnostics are present, only allocation errors will be returned. /// -/// If the destination type has a decl "renamed", it must be of type -/// std.StaticStringMap([]const u8) and contains a mapping from the old -/// field name to the new field name. This is used to allow renaming fields -/// while still supporting the old name. If a renamed field is set, parsing -/// will automatically set the new field name. +/// If the destination type has a decl "compatibility", it must be of type +/// std.StaticStringMap(CompatibilityHandler(T)), and it will be used to +/// handle backwards compatibility for fields with the given name. The +/// field name doesn't need to exist (so you can setup compatibility for +/// removed fields). The value is a function that will be called when +/// all other parsing fails for that field. If a field changes such that +/// the old values would NOT error, then the caller should handle that +/// downstream after parsing is done, not through this method. /// /// Note: If the arena is already non-null, then it will be used. In this /// case, in the case of an error some memory might be leaked into the arena. @@ -57,24 +60,6 @@ pub fn parse( const info = @typeInfo(T); assert(info == .@"struct"); - comptime { - // Verify all renamed fields are valid (source does not exist, - // destination does exist). - if (@hasDecl(T, "renamed")) { - for (T.renamed.keys(), T.renamed.values()) |key, value| { - if (@hasField(T, key)) { - @compileLog(key); - @compileError("renamed field source exists"); - } - - if (!@hasField(T, value)) { - @compileLog(value); - @compileError("renamed field destination does not exist"); - } - } - } - } - // Make an arena for all our allocations if we support it. Otherwise, // use an allocator that always fails. If the arena is already set on // the config, then we reuse that. See memory note in parse docs. @@ -147,7 +132,23 @@ pub fn parse( break :value null; }; - parseIntoField(T, arena_alloc, dst, key, value) catch |err| { + parseIntoField(T, arena_alloc, dst, key, value) catch |err| err: { + // If we get an error parsing a field, then we try to fall + // back to compatibility handlers if able. + if (@hasDecl(T, "compatibility")) { + // If we have a compatibility handler for this key, then + // we call it and see if it handles the error. + if (T.compatibility.get(key)) |handler| { + if (handler(dst, arena_alloc, key, value)) { + log.info( + "compatibility handler for {s} handled error, you may be using a deprecated field: {}", + .{ key, err }, + ); + break :err; + } + } + } + if (comptime !canTrackDiags(T)) return err; // The error set is dependent on comptime T, so we always add @@ -177,6 +178,58 @@ pub fn parse( } } +/// The function type for a compatibility handler. The compatibility +/// handler is documented in the `parse` function documentation. +/// +/// The function type should return bool if the compatibility was +/// handled, and false otherwise. If false is returned then the +/// naturally occurring error will continue to be processed as if +/// this compatibility handler was not present. +/// +/// Compatibility handlers aren't allowed to return errors because +/// they're generally only called in error cases, so we already have +/// an error message to show users. If there is an error in handling +/// the compatibility, then the handler should return false. +pub fn CompatibilityHandler(comptime T: type) type { + return *const fn ( + dst: *T, + alloc: Allocator, + key: []const u8, + value: ?[]const u8, + ) bool; +} + +/// Convenience function to create a compatibility handler that +/// renames a field from `from` to `to`. +pub fn compatibilityRenamed( + comptime T: type, + comptime to: []const u8, +) CompatibilityHandler(T) { + comptime assert(@hasField(T, to)); + + return (struct { + fn compat( + dst: *T, + alloc: Allocator, + key: []const u8, + value: ?[]const u8, + ) bool { + _ = key; + + parseIntoField(T, alloc, dst, to, value) catch |err| { + log.warn("error parsing renamed field {s}: {}", .{ + to, + err, + }); + + return false; + }; + + return true; + } + }).compat; +} + fn formatValueRequired( comptime T: type, arena_alloc: std.mem.Allocator, @@ -401,16 +454,6 @@ pub fn parseIntoField( } } - // Unknown field, is the field renamed? - if (@hasDecl(T, "renamed")) { - for (T.renamed.keys(), T.renamed.values()) |old, new| { - if (mem.eql(u8, old, key)) { - try parseIntoField(T, alloc, dst, new, value); - return; - } - } - } - return error.InvalidField; } @@ -752,6 +795,77 @@ test "parse: diagnostic location" { } } +test "parse: compatibility handler" { + const testing = std.testing; + + var data: struct { + a: bool = false, + _arena: ?ArenaAllocator = null, + + pub const compatibility: std.StaticStringMap( + CompatibilityHandler(@This()), + ) = .initComptime(&.{ + .{ "a", compat }, + }); + + fn compat( + self: *@This(), + alloc: Allocator, + key: []const u8, + value: ?[]const u8, + ) bool { + _ = alloc; + if (std.mem.eql(u8, key, "a")) { + if (value) |v| { + if (mem.eql(u8, v, "yuh")) { + self.a = true; + return true; + } + } + } + + return false; + } + } = .{}; + defer if (data._arena) |arena| arena.deinit(); + + var iter = try std.process.ArgIteratorGeneral(.{}).init( + testing.allocator, + "--a=yuh", + ); + defer iter.deinit(); + try parse(@TypeOf(data), testing.allocator, &data, &iter); + try testing.expect(data._arena != null); + try testing.expect(data.a); +} + +test "parse: compatibility renamed" { + const testing = std.testing; + + var data: struct { + a: bool = false, + b: bool = false, + _arena: ?ArenaAllocator = null, + + pub const compatibility: std.StaticStringMap( + CompatibilityHandler(@This()), + ) = .initComptime(&.{ + .{ "old", compatibilityRenamed(@This(), "a") }, + }); + } = .{}; + defer if (data._arena) |arena| arena.deinit(); + + var iter = try std.process.ArgIteratorGeneral(.{}).init( + testing.allocator, + "--old=true --b=true", + ); + defer iter.deinit(); + try parse(@TypeOf(data), testing.allocator, &data, &iter); + try testing.expect(data._arena != null); + try testing.expect(data.a); + try testing.expect(data.b); +} + test "parseIntoField: ignore underscore-prefixed fields" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); @@ -1176,24 +1290,6 @@ test "parseIntoField: tagged union missing tag" { ); } -test "parseIntoField: renamed field" { - const testing = std.testing; - var arena = ArenaAllocator.init(testing.allocator); - defer arena.deinit(); - const alloc = arena.allocator(); - - var data: struct { - a: []const u8, - - const renamed = std.StaticStringMap([]const u8).initComptime(&.{ - .{ "old", "a" }, - }); - } = undefined; - - try parseIntoField(@TypeOf(data), alloc, &data, "old", "42"); - try testing.expectEqualStrings("42", data.a); -} - /// An iterator that considers its location to be CLI args. It /// iterates through an underlying iterator and increments a counter /// to track the current CLI arg index. diff --git a/src/config/Config.zig b/src/config/Config.zig index 44089fb57..14ab5219d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -46,14 +46,22 @@ const c = @cImport({ @cInclude("unistd.h"); }); -/// Renamed fields, used by cli.parse -pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{ +pub const compatibility = std.StaticStringMap( + cli.CompatibilityHandler(Config), +).initComptime(&.{ // Ghostty 1.1 introduced background-blur support for Linux which // doesn't support a specific radius value. The renaming is to let // one field be used for both platforms (macOS retained the ability // to set a radius). - .{ "background-blur-radius", "background-blur" }, - .{ "adw-toolbar-style", "gtk-toolbar-style" }, + .{ "background-blur-radius", cli.compatibilityRenamed(Config, "background-blur") }, + + // Ghostty 1.2 renamed all our adw options to gtk because we now have + // a hard dependency on libadwaita. + .{ "adw-toolbar-style", cli.compatibilityRenamed(Config, "gtk-toolbar-style") }, + + // Ghostty 1.2 removed the `hidden` value from `gtk-tabs-location` and + // moved it to `window-show-tab-bar`. + .{ "gtk-tabs-location", compatGtkTabsLocation }, }); /// The font families to use. @@ -3792,6 +3800,27 @@ pub fn parseManuallyHook( return true; } +/// parseFieldManuallyFallback is a fallback called only when +/// parsing the field directly failed. It can be used to implement +/// backward compatibility. Since this is only called when parsing +/// fails, it doesn't impact happy-path performance. +fn compatGtkTabsLocation( + self: *Config, + alloc: Allocator, + key: []const u8, + value: ?[]const u8, +) bool { + _ = alloc; + assert(std.mem.eql(u8, key, "gtk-tabs-location")); + + if (std.mem.eql(u8, value orelse "", "hidden")) { + self.@"window-show-tab-bar" = .never; + return true; + } + + return false; +} + /// Create a shallow copy of this config. This will share all the memory /// allocated with the previous config but will have a new arena for /// any changes or new allocations. The config should have `deinit` @@ -5005,6 +5034,12 @@ pub const Keybinds = struct { try self.set.put( alloc, + .{ .key = .{ .unicode = 'j' }, .mods = .{ .shift = true, .ctrl = true, .super = true } }, + .{ .write_screen_file = .copy }, + ); + + try self.set.put( + alloc, .{ .key = .{ .unicode = 'j' }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, .{ .write_screen_file = .paste }, ); diff --git a/src/input/Binding.zig b/src/input/Binding.zig index cccf12ac4..7cdb8047c 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -379,6 +379,10 @@ pub const Action = union(enum) { /// /// Valid actions are: /// + /// - `copy` + /// + /// Copy the file path into the clipboard. + /// /// - `paste` /// /// Paste the file path into the terminal. @@ -813,6 +817,7 @@ pub const Action = union(enum) { }; pub const WriteScreenAction = enum { + copy, paste, open, }; diff --git a/src/input/command.zig b/src/input/command.zig index 8ae48eda1..693d5c8d4 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -205,6 +205,11 @@ fn actionCommands(action: Action.Key) []const Command { .write_screen_file => comptime &.{ .{ + .action = .{ .write_screen_file = .copy }, + .title = "Copy Screen to Temporary File and Copy Path", + .description = "Copy the screen contents to a temporary file and copy the path to the clipboard.", + }, + .{ .action = .{ .write_screen_file = .paste }, .title = "Copy Screen to Temporary File and Paste Path", .description = "Copy the screen contents to a temporary file and paste the path to the file.", @@ -218,6 +223,11 @@ fn actionCommands(action: Action.Key) []const Command { .write_selection_file => comptime &.{ .{ + .action = .{ .write_selection_file = .copy }, + .title = "Copy Selection to Temporary File and Copy Path", + .description = "Copy the selection contents to a temporary file and copy the path to the clipboard.", + }, + .{ .action = .{ .write_selection_file = .paste }, .title = "Copy Selection to Temporary File and Paste Path", .description = "Copy the selection contents to a temporary file and paste the path to the file.", diff --git a/src/os/i18n.zig b/src/os/i18n.zig index fb8980852..f5c8ffdea 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -44,8 +44,10 @@ pub const locales = [_][:0]const u8{ "tr_TR.UTF-8", "id_ID.UTF-8", "es_BO.UTF-8", + "es_AR.UTF-8", "pt_BR.UTF-8", "ca_ES.UTF-8", + "ga_IE.UTF-8", }; /// Set for faster membership lookup of locales. diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 5b772ab84..079df37db 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -3068,6 +3068,29 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void { } } +/// Write text that's marked as a semantic prompt. +fn testWriteSemanticString(self: *Screen, text: []const u8, semantic_prompt: Row.SemanticPrompt) !void { + // Determine the first row using the cursor position. If we know that our + // first write is going to start on the next line because of a pending + // wrap, we'll proactively start there. + const start_y = if (self.cursor.pending_wrap) self.cursor.y + 1 else self.cursor.y; + + try self.testWriteString(text); + + // Determine the last row that we actually wrote by inspecting the cursor's + // position. If we're in the first column, we haven't actually written any + // characters to it, so we end at the preceding row instead. + const end_y = if (self.cursor.x > 0) self.cursor.y else self.cursor.y - 1; + + // Mark the full range of written rows with our semantic prompt. + var y = start_y; + while (y <= end_y) { + const pin = self.pages.pin(.{ .active = .{ .y = y } }).?; + pin.rowAndCell().row.semantic_prompt = semantic_prompt; + y += 1; + } +} + test "Screen read and write" { const testing = std.testing; const alloc = testing.allocator; @@ -3686,16 +3709,11 @@ test "Screen: clearPrompt" { var s = try init(alloc, 5, 3, 0); defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); // Set one of the rows to be a prompt - { - s.cursorAbsolute(0, 1); - s.cursor.page_row.semantic_prompt = .prompt; - s.cursorAbsolute(0, 2); - s.cursor.page_row.semantic_prompt = .input; - } + try s.testWriteSemanticString("1ABCD\n", .unknown); + try s.testWriteSemanticString("2EFGH\n", .prompt); + try s.testWriteSemanticString("3IJKL", .input); s.clearPrompt(); @@ -3712,18 +3730,12 @@ test "Screen: clearPrompt continuation" { var s = try init(alloc, 5, 4, 0); defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL\n4MNOP"; - try s.testWriteString(str); // Set one of the rows to be a prompt followed by a continuation row - { - s.cursorAbsolute(0, 1); - s.cursor.page_row.semantic_prompt = .prompt; - s.cursorAbsolute(0, 2); - s.cursor.page_row.semantic_prompt = .prompt_continuation; - s.cursorAbsolute(0, 3); - s.cursor.page_row.semantic_prompt = .input; - } + try s.testWriteSemanticString("1ABCD\n", .unknown); + try s.testWriteSemanticString("2EFGH\n", .prompt); + try s.testWriteSemanticString("3IJKL\n", .prompt_continuation); + try s.testWriteSemanticString("4MNOP", .input); s.clearPrompt(); @@ -3734,22 +3746,17 @@ test "Screen: clearPrompt continuation" { } } -test "Screen: clearPrompt consecutive prompts" { +test "Screen: clearPrompt consecutive inputs" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 5, 3, 0); defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - // Set both rows to be prompts - { - s.cursorAbsolute(0, 1); - s.cursor.page_row.semantic_prompt = .input; - s.cursorAbsolute(0, 2); - s.cursor.page_row.semantic_prompt = .input; - } + // Set both rows to be inputs + try s.testWriteSemanticString("1ABCD\n", .unknown); + try s.testWriteSemanticString("2EFGH\n", .input); + try s.testWriteSemanticString("3IJKL", .input); s.clearPrompt(); @@ -6057,26 +6064,24 @@ test "Screen: resize more cols no reflow preserves semantic prompt" { var s = try init(alloc, 5, 3, 0); defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); // Set one of the rows to be a prompt - { - s.cursorAbsolute(0, 1); - s.cursor.page_row.semantic_prompt = .prompt; - } + try s.testWriteSemanticString("1ABCD\n", .unknown); + try s.testWriteSemanticString("2EFGH\n", .prompt); + try s.testWriteSemanticString("3IJKL", .unknown); try s.resize(10, 3); + const expected = "1ABCD\n2EFGH\n3IJKL"; { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); + try testing.expectEqualStrings(expected, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); + try testing.expectEqualStrings(expected, contents); } // Our one row should still be a semantic prompt, the others should not. @@ -7507,7 +7512,9 @@ test "Screen: selectLine semantic prompt boundary" { var s = try init(alloc, 5, 10, 0); defer s.deinit(); - try s.testWriteString("ABCDE\nA > "); + try s.testWriteSemanticString("ABCDE\n", .unknown); + try s.testWriteSemanticString("A ", .prompt); + try s.testWriteSemanticString("> ", .unknown); { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); @@ -7515,12 +7522,6 @@ test "Screen: selectLine semantic prompt boundary" { try testing.expectEqualStrings("ABCDE\nA \n> ", contents); } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 1 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .prompt; - } - // Selecting output stops at the prompt even if soft-wrapped { var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{ @@ -7905,55 +7906,23 @@ test "Screen: selectOutput" { // zig fmt: off { // line number: - try s.testWriteString("output1\n"); // 0 - try s.testWriteString("output1\n"); // 1 - try s.testWriteString("prompt2\n"); // 2 - try s.testWriteString("input2\n"); // 3 - try s.testWriteString("output2output2output2output2\n"); // 4, 5, 6 due to overflow - try s.testWriteString("output2\n"); // 7 - try s.testWriteString("$ input3\n"); // 8 - try s.testWriteString("output3\n"); // 9 - try s.testWriteString("output3\n"); // 10 - try s.testWriteString("output3"); // 11 + try s.testWriteSemanticString("output1\n", .command); // 0 + try s.testWriteSemanticString("output1\n", .command); // 1 + try s.testWriteSemanticString("prompt2\n", .prompt); // 2 + try s.testWriteSemanticString("input2\n", .input); // 3 + try s.testWriteSemanticString( // + "output2output2output2output2\n", // 4, 5, 6 due to overflow + .command, // + ); // + try s.testWriteSemanticString("output2\n", .command); // 7 + try s.testWriteSemanticString("$ ", .prompt); // 8 prompt + try s.testWriteSemanticString("input3\n", .input); // 8 input + try s.testWriteSemanticString("output3\n", .command); // 9 + try s.testWriteSemanticString("output3\n", .command); // 10 + try s.testWriteSemanticString("output3", .command); // 11 } // zig fmt: on - { - const pin = s.pages.pin(.{ .screen = .{ .y = 2 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .prompt; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 3 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .input; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 4 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 5 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 6 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 8 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .input; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 9 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } - // No start marker, should select from the beginning { var sel = s.selectOutput(s.pages.pin(.{ .active = .{ @@ -8006,19 +7975,10 @@ test "Screen: selectOutput" { { s.deinit(); s = try init(alloc, 10, 5, 0); - try s.testWriteString("$ input1\n"); - try s.testWriteString("output1\n"); - try s.testWriteString("prompt2\n"); - { - const pin = s.pages.pin(.{ .screen = .{ .y = 0 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .input; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 1 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } + try s.testWriteSemanticString("$ ", .prompt); + try s.testWriteSemanticString("input1\n", .input); + try s.testWriteSemanticString("output1\n", .command); + try s.testWriteSemanticString("prompt2\n", .prompt); try testing.expect(s.selectOutput(s.pages.pin(.{ .active = .{ .x = 2, .y = 0, @@ -8035,46 +7995,21 @@ test "Screen: selectPrompt basics" { // zig fmt: off { - // line number: - try s.testWriteString("output1\n"); // 0 - try s.testWriteString("output1\n"); // 1 - try s.testWriteString("prompt2\n"); // 2 - try s.testWriteString("input2\n"); // 3 - try s.testWriteString("output2\n"); // 4 - try s.testWriteString("output2\n"); // 5 - try s.testWriteString("$ input3\n"); // 6 - try s.testWriteString("output3\n"); // 7 - try s.testWriteString("output3\n"); // 8 - try s.testWriteString("output3"); // 9 + // line number: + try s.testWriteSemanticString("output1\n", .command); // 0 + try s.testWriteSemanticString("output1\n", .command); // 1 + try s.testWriteSemanticString("prompt2\n", .prompt); // 2 + try s.testWriteSemanticString("input2\n", .input); // 3 + try s.testWriteSemanticString("output2\n", .command); // 4 + try s.testWriteSemanticString("output2\n", .command); // 5 + try s.testWriteSemanticString("$ ", .prompt); // 6 prompt + try s.testWriteSemanticString("input3\n", .input); // 6 input + try s.testWriteSemanticString("output3\n", .command); // 7 + try s.testWriteSemanticString("output3\n", .command); // 8 + try s.testWriteSemanticString("output3", .command); // 9 } // zig fmt: on - { - const pin = s.pages.pin(.{ .screen = .{ .y = 2 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .prompt; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 3 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .input; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 4 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 6 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .input; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 7 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } - // Not at a prompt { const sel = s.selectPrompt(s.pages.pin(.{ .active = .{ @@ -8135,30 +8070,14 @@ test "Screen: selectPrompt prompt at start" { // zig fmt: off { - // line number: - try s.testWriteString("prompt1\n"); // 0 - try s.testWriteString("input1\n"); // 1 - try s.testWriteString("output2\n"); // 2 - try s.testWriteString("output2\n"); // 3 + // line number: + try s.testWriteSemanticString("prompt1\n", .prompt); // 0 + try s.testWriteSemanticString("input1\n", .input); // 1 + try s.testWriteSemanticString("output2\n", .command); // 2 + try s.testWriteSemanticString("output2\n", .command); // 3 } // zig fmt: on - { - const pin = s.pages.pin(.{ .screen = .{ .y = 0 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .prompt; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 1 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .input; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 2 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } - // Not at a prompt { const sel = s.selectPrompt(s.pages.pin(.{ .active = .{ @@ -8195,25 +8114,14 @@ test "Screen: selectPrompt prompt at end" { // zig fmt: off { - // line number: - try s.testWriteString("output2\n"); // 0 - try s.testWriteString("output2\n"); // 1 - try s.testWriteString("prompt1\n"); // 2 - try s.testWriteString("input1\n"); // 3 + // line number: + try s.testWriteSemanticString("output2\n", .command); // 0 + try s.testWriteSemanticString("output2\n", .command); // 1 + try s.testWriteSemanticString("prompt1\n", .prompt); // 2 + try s.testWriteSemanticString("input1\n", .input); // 3 } // zig fmt: on - { - const pin = s.pages.pin(.{ .screen = .{ .y = 2 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .prompt; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 3 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .input; - } - // Not at a prompt { const sel = s.selectPrompt(s.pages.pin(.{ .active = .{ @@ -8250,46 +8158,21 @@ test "Screen: promptPath" { // zig fmt: off { - // line number: - try s.testWriteString("output1\n"); // 0 - try s.testWriteString("output1\n"); // 1 - try s.testWriteString("prompt2\n"); // 2 - try s.testWriteString("input2\n"); // 3 - try s.testWriteString("output2\n"); // 4 - try s.testWriteString("output2\n"); // 5 - try s.testWriteString("$ input3\n"); // 6 - try s.testWriteString("output3\n"); // 7 - try s.testWriteString("output3\n"); // 8 - try s.testWriteString("output3"); // 9 + // line number: + try s.testWriteSemanticString("output1\n", .command); // 0 + try s.testWriteSemanticString("output1\n", .command); // 1 + try s.testWriteSemanticString("prompt2\n", .prompt); // 2 + try s.testWriteSemanticString("input2\n", .input); // 3 + try s.testWriteSemanticString("output2\n", .command); // 4 + try s.testWriteSemanticString("output2\n", .command); // 5 + try s.testWriteSemanticString("$ ", .prompt); // 6 prompt + try s.testWriteSemanticString("input3\n", .input); // 6 input + try s.testWriteSemanticString("output3\n", .command); // 7 + try s.testWriteSemanticString("output3\n", .command); // 8 + try s.testWriteSemanticString("output3", .command); // 9 } // zig fmt: on - { - const pin = s.pages.pin(.{ .screen = .{ .y = 2 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .prompt; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 3 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .input; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 4 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 6 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .input; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 7 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } - // From is not in the prompt { const path = s.promptPath( diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index 865a2df86..8aaa87011 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -168,6 +168,7 @@ pub const DerivedConfig = struct { foreground: configpkg.Config.Color, background: configpkg.Config.Color, osc_color_report_format: configpkg.Config.OSCColorReportFormat, + clipboard_write: configpkg.ClipboardAccess, enquiry_response: []const u8, pub fn init( @@ -188,6 +189,7 @@ pub const DerivedConfig = struct { .foreground = config.foreground, .background = config.background, .osc_color_report_format = config.@"osc-color-report-format", + .clipboard_write = config.@"clipboard-write", .enquiry_response = try alloc.dupe(u8, config.@"enquiry-response"), // This has to be last so that we copy AFTER the arena allocations @@ -278,6 +280,7 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { .size = &self.size, .terminal = &self.terminal, .osc_color_report_format = opts.config.osc_color_report_format, + .clipboard_write = opts.config.clipboard_write, .enquiry_response = opts.config.enquiry_response, .default_foreground_color = opts.config.foreground.toTerminalRGB(), .default_background_color = opts.config.background.toTerminalRGB(), diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 90add84ae..1b4fdd3aa 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -74,6 +74,9 @@ pub const StreamHandler = struct { /// The color reporting format for OSC requests. osc_color_report_format: configpkg.Config.OSCColorReportFormat, + /// The clipboard write access configuration. + clipboard_write: configpkg.ClipboardAccess, + //--------------------------------------------------------------- // Internal state @@ -112,6 +115,7 @@ pub const StreamHandler = struct { /// Change the configuration for this handler. pub fn changeConfig(self: *StreamHandler, config: *termio.DerivedConfig) void { self.osc_color_report_format = config.osc_color_report_format; + self.clipboard_write = config.clipboard_write; self.enquiry_response = config.enquiry_response; self.default_foreground_color = config.foreground.toTerminalRGB(); self.default_background_color = config.background.toTerminalRGB(); @@ -723,7 +727,13 @@ pub const StreamHandler = struct { // a 420 because we don't support DCS sequences. switch (req) { .primary => self.messageWriter(.{ - .write_stable = "\x1B[?62;22c", + // 62 = Level 2 conformance + // 22 = Color text + // 52 = Clipboard access + .write_stable = if (self.clipboard_write != .deny) + "\x1B[?62;22;52c" + else + "\x1B[?62;22c", }), .secondary => self.messageWriter(.{ |
