diff --git a/src/config/Config.zig b/src/config/Config.zig index 9982cacef..4641cb8a6 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -162,6 +162,32 @@ const c = @cImport({ @"adjust-strikethrough-position": ?MetricModifier = null, @"adjust-strikethrough-thickness": ?MetricModifier = null, +/// The method to use for calculating the cell width of a grapheme cluster. +/// The default value is "unicode" which uses the Unicode standard to +/// determine grapheme width. This results in correct grapheme width but +/// may result in cursor-desync issues with some programs (such as shells) +/// that may use a legacy method such as "wcswidth". +/// +/// Valid values are: +/// +/// - "wcswidth" - Use the wcswidth function to determine grapheme width. +/// This maximizes compatibility with legacy programs but may result +/// in incorrect grapheme width for certain graphemes such as skin-tone +/// emoji, non-English characters, etc. +/// +/// Note that this "wcswidth" functionality is based on the libc wcswidth, +/// not any other libraries with that name. +/// +/// - "unicode" - Use the Unicode standard to determine grapheme width. +/// +/// If a running program explicitly enables terminal mode 2027, then +/// "unicode" width will be forced regardless of this configuration. When +/// mode 2027 is reset, this configuration will be used again. +/// +/// This configuration can be changed at runtime but will not affect existing +/// printed cells. Only new cells will use the new configuration. +@"grapheme-width-method": GraphemeWidthMethod = .unicode, + /// A named theme to use. The available themes are currently hardcoded to /// the themes that ship with Ghostty. On macOS, this list is in the /// `Ghostty.app/Contents/Resources/themes` directory. On Linux, this @@ -2781,3 +2807,9 @@ pub const WindowSaveState = enum { never, always, }; + +/// See grapheme-width-method +pub const GraphemeWidthMethod = enum { + wcswidth, + unicode, +}; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 9b6b1f42f..2f08e07a0 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -120,6 +120,9 @@ flags: packed struct { /// then we want to capture the shift key for the mouse protocol /// if the configuration allows it. mouse_shift_capture: enum { null, false, true } = .null, + + /// If true, we perform grapheme clustering even if mode 2027 is disabled. + default_grapheme_cluster: bool = false, } = .{}, /// The event types that can be reported for mouse-related activities. @@ -724,6 +727,8 @@ pub fn print(self: *Terminal, c: u21) !void { const tracy = trace(@src()); defer tracy.end(); + // log.debug("print={x} y={} x={}", .{ c, self.screen.cursor.y, self.screen.cursor.x }); + // If we're not on the main display, do nothing for now if (self.status_display != .main) return; @@ -738,7 +743,7 @@ pub fn print(self: *Terminal, c: u21) !void { // purposely ordered in least-likely to most-likely so we can drop out // as quickly as possible. if (c > 255 and - self.modes.get(.grapheme_cluster) and + (self.modes.get(.grapheme_cluster) or self.flags.default_grapheme_cluster) and self.screen.cursor.x > 0) grapheme: { const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); @@ -775,6 +780,7 @@ pub fn print(self: *Terminal, c: u21) !void { if (prev.cell.attrs.grapheme) { var it = row.codepointIterator(prev.x); while (it.next()) |cp2| { + // log.debug("cp1={x} cp2={x}", .{ cp1, cp2 }); assert(!ziglyph.graphemeBreak( cp1, cp2, @@ -785,6 +791,7 @@ pub fn print(self: *Terminal, c: u21) !void { } } + // log.debug("cp1={x} cp2={x} end", .{ cp1, c }); break :brk ziglyph.graphemeBreak(cp1, c, &state); }; @@ -868,7 +875,9 @@ pub fn print(self: *Terminal, c: u21) !void { // If we have grapheme clustering enabled, we don't blindly attach // any zero width character to our cells and we instead just ignore // it. - if (self.modes.get(.grapheme_cluster)) return; + if (self.modes.get(.grapheme_cluster) or + self.flags.default_grapheme_cluster) + return; // If we're at cell zero, then this is malformed data and we don't // print anything or even store this. Zero-width characters are ALWAYS diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 690cce752..51992a343 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -105,6 +105,7 @@ pub const DerivedConfig = struct { background: configpkg.Config.Color, osc_color_report_format: configpkg.Config.OSCColorReportFormat, term: []const u8, + grapheme_width_method: configpkg.Config.GraphemeWidthMethod, pub fn init( alloc_gpa: Allocator, @@ -122,6 +123,7 @@ pub const DerivedConfig = struct { .background = config.background, .osc_color_report_format = config.@"osc-color-report-format", .term = config.term, + .grapheme_width_method = config.@"grapheme-width-method", }; } @@ -146,6 +148,7 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { errdefer term.deinit(alloc); term.default_palette = opts.config.palette; term.color_palette.colors = opts.config.palette; + term.flags.default_grapheme_cluster = opts.config.grapheme_width_method == .unicode; // Set the image size limits try term.screen.kitty_images.setLimit(alloc, opts.config.image_storage_limit); @@ -350,6 +353,8 @@ pub fn changeConfig(self: *Exec, config: *DerivedConfig) !void { // - command, working-directory: we never restart the underlying // process so we don't care or need to know about these. + self.terminal.flags.default_grapheme_cluster = config.grapheme_width_method == .unicode; + // Update the default palette. Note this will only apply to new colors drawn // since we decode all palette colors to RGB on usage. self.terminal.default_palette = config.palette;