summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRME <ruben@rme.gg>2025-06-30 15:05:01 +0200
committerGitHub <noreply@github.com>2025-06-30 15:05:01 +0200
commit9aa2383e05703f2d0bdf84a33ead5853089f8071 (patch)
tree8490b40db0a332adb286bf93938648561523759a /src
parent6484df913435377ef9aec6b9519d639af57a4ab4 (diff)
parent259228698873c0c934741445ec6790cfafb64502 (diff)
Merge branch 'main' into ko_kr
Diffstat (limited to 'src')
-rw-r--r--src/Surface.zig5
-rw-r--r--src/apprt/gtk/ClipboardConfirmationWindow.zig36
-rw-r--r--src/apprt/gtk/adw_version.zig4
-rw-r--r--src/apprt/gtk/style.css6
-rw-r--r--src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp92
-rw-r--r--src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp90
-rw-r--r--src/cli.zig2
-rw-r--r--src/cli/args.zig200
-rw-r--r--src/config/Config.zig43
-rw-r--r--src/input/Binding.zig5
-rw-r--r--src/input/command.zig10
-rw-r--r--src/os/i18n.zig2
-rw-r--r--src/terminal/Screen.zig309
-rw-r--r--src/termio/Termio.zig3
-rw-r--r--src/termio/stream_handler.zig12
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(.{