Merge pull request #1158 from mitchellh/grapheme-config

config: grapheme-width-method, default to "unicode"
This commit is contained in:
Mitchell Hashimoto
2023-12-25 14:36:26 -08:00
committed by GitHub
3 changed files with 48 additions and 2 deletions

View File

@ -162,6 +162,32 @@ const c = @cImport({
@"adjust-strikethrough-position": ?MetricModifier = null, @"adjust-strikethrough-position": ?MetricModifier = null,
@"adjust-strikethrough-thickness": ?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 /// 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 /// the themes that ship with Ghostty. On macOS, this list is in the
/// `Ghostty.app/Contents/Resources/themes` directory. On Linux, this /// `Ghostty.app/Contents/Resources/themes` directory. On Linux, this
@ -2781,3 +2807,9 @@ pub const WindowSaveState = enum {
never, never,
always, always,
}; };
/// See grapheme-width-method
pub const GraphemeWidthMethod = enum {
wcswidth,
unicode,
};

View File

@ -120,6 +120,9 @@ flags: packed struct {
/// then we want to capture the shift key for the mouse protocol /// then we want to capture the shift key for the mouse protocol
/// if the configuration allows it. /// if the configuration allows it.
mouse_shift_capture: enum { null, false, true } = .null, 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. /// 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()); const tracy = trace(@src());
defer tracy.end(); 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 we're not on the main display, do nothing for now
if (self.status_display != .main) return; 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 // purposely ordered in least-likely to most-likely so we can drop out
// as quickly as possible. // as quickly as possible.
if (c > 255 and 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) self.screen.cursor.x > 0)
grapheme: { grapheme: {
const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); 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) { if (prev.cell.attrs.grapheme) {
var it = row.codepointIterator(prev.x); var it = row.codepointIterator(prev.x);
while (it.next()) |cp2| { while (it.next()) |cp2| {
// log.debug("cp1={x} cp2={x}", .{ cp1, cp2 });
assert(!ziglyph.graphemeBreak( assert(!ziglyph.graphemeBreak(
cp1, cp1,
cp2, 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); 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 // If we have grapheme clustering enabled, we don't blindly attach
// any zero width character to our cells and we instead just ignore // any zero width character to our cells and we instead just ignore
// it. // 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 // 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 // print anything or even store this. Zero-width characters are ALWAYS

View File

@ -105,6 +105,7 @@ pub const DerivedConfig = struct {
background: configpkg.Config.Color, background: configpkg.Config.Color,
osc_color_report_format: configpkg.Config.OSCColorReportFormat, osc_color_report_format: configpkg.Config.OSCColorReportFormat,
term: []const u8, term: []const u8,
grapheme_width_method: configpkg.Config.GraphemeWidthMethod,
pub fn init( pub fn init(
alloc_gpa: Allocator, alloc_gpa: Allocator,
@ -122,6 +123,7 @@ pub const DerivedConfig = struct {
.background = config.background, .background = config.background,
.osc_color_report_format = config.@"osc-color-report-format", .osc_color_report_format = config.@"osc-color-report-format",
.term = config.term, .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); errdefer term.deinit(alloc);
term.default_palette = opts.config.palette; term.default_palette = opts.config.palette;
term.color_palette.colors = 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 // Set the image size limits
try term.screen.kitty_images.setLimit(alloc, opts.config.image_storage_limit); 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 // - command, working-directory: we never restart the underlying
// process so we don't care or need to know about these. // 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 // Update the default palette. Note this will only apply to new colors drawn
// since we decode all palette colors to RGB on usage. // since we decode all palette colors to RGB on usage.
self.terminal.default_palette = config.palette; self.terminal.default_palette = config.palette;