Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ var program = try zz.Program(Model).initWithOptions(gpa.allocator(), .{
.cursor = false, // Show cursor
.bracketed_paste = true, // Enable bracketed paste mode
.kitty_keyboard = false, // Enable Kitty keyboard protocol
.unicode_width_strategy = null, // null=auto, .legacy_wcwidth, .unicode
.suspend_enabled = true, // Enable Ctrl+Z suspend/resume
.title = "My App", // Window title
.log_file = "debug.log", // Debug log file path
Expand All @@ -434,6 +435,12 @@ var program = try zz.Program(Model).initWithOptions(gpa.allocator(), .{
});
```

Unicode width strategy can also be overridden per-process with `ZZ_UNICODE_WIDTH=auto|legacy|unicode`.
By default (`null`/`auto`), ZigZag:
- probes DEC mode `2027` and enables it when available,
- probes kitty text-sizing support,
- applies terminal/multiplexer heuristics (e.g. tmux/screen/zellij favor legacy width).

### Custom Event Loop

For applications that need to do other work between frames (network polling, background processing, etc.), use `start()` + `tick()` instead of `run()`:
Expand Down
32 changes: 16 additions & 16 deletions examples/showcase.zig
Original file line number Diff line number Diff line change
Expand Up @@ -692,25 +692,25 @@ const Model = struct {

const cjk_box = try cjk_style.render(alloc, cjk_content);

// -- Emoji Box --
var emoji_header_style = zz.Style{};
emoji_header_style = emoji_header_style.bold(true);
emoji_header_style = emoji_header_style.fg(zz.Color.hex("#4ECDC4"));
emoji_header_style = emoji_header_style.inline_style(true);
const emoji_header = try emoji_header_style.render(alloc, "Emoji");
// -- Symbol Box --
var symbol_header_style = zz.Style{};
symbol_header_style = symbol_header_style.bold(true);
symbol_header_style = symbol_header_style.fg(zz.Color.hex("#4ECDC4"));
symbol_header_style = symbol_header_style.inline_style(true);
const symbol_header = try symbol_header_style.render(alloc, "Symbols");

var emoji_style = zz.Style{};
emoji_style = emoji_style.borderAll(zz.Border.rounded);
emoji_style = emoji_style.borderForeground(zz.Color.hex("#4ECDC4"));
emoji_style = emoji_style.paddingLeft(1).paddingRight(1);
emoji_style = emoji_style.width(30);
var symbol_style = zz.Style{};
symbol_style = symbol_style.borderAll(zz.Border.rounded);
symbol_style = symbol_style.borderForeground(zz.Color.hex("#4ECDC4"));
symbol_style = symbol_style.paddingLeft(1).paddingRight(1);
symbol_style = symbol_style.width(30);

const emoji_content = try std.fmt.allocPrint(alloc, "{s}\n\n \u{1F680} Rocket \u{2615} Coffee\n \u{1F4A9} Fun \u{2B50} Star\n \u{1F600} Grinning \u{2764} Heart", .{emoji_header});
const symbol_content = try std.fmt.allocPrint(alloc, "{s}\n\n \u{03B1}\u{03B2}\u{03B3}\u{03B4}\u{03B5} Greek letters\n \u{2211}\u{221A}\u{2260}\u{2264}\u{2265} Math symbols\n \u{2605}\u{2606}\u{00A7}\u{00B6}\u{00B0} Misc symbols", .{symbol_header});

const emoji_box = try emoji_style.render(alloc, emoji_content);
const symbol_box = try symbol_style.render(alloc, symbol_content);

// -- Top row --
const top_row = try zz.joinHorizontal(alloc, &.{ cjk_box, " ", emoji_box });
const top_row = try zz.joinHorizontal(alloc, &.{ cjk_box, " ", symbol_box });

// -- Fullwidth/Halfwidth comparison box --
var fw_header_style = zz.Style{};
Expand Down Expand Up @@ -756,7 +756,7 @@ const Model = struct {
align_style = align_style.borderForeground(zz.Color.green());
align_style = align_style.paddingLeft(1).paddingRight(1);

const align_content = try std.fmt.allocPrint(alloc, "{s}\n\n |hello | 5 cols\n |\u{4F60}\u{597D} | 4 cols (2 wide chars)\n |\u{1F680}\u{1F4A9}\u{2615} | 6 cols (3 emojis)\n |caf\u{00E9} | 4 cols (precomposed)\n |cafe\u{0301} | 4 cols (combining)", .{align_header});
const align_content = try std.fmt.allocPrint(alloc, "{s}\n\n |hello | 5 cols\n |\u{4F60}\u{597D} | 4 cols (2 wide chars)\n |\u{03B1}\u{03B2}\u{03B3}\u{03B4} | 4 cols (Greek)\n |caf\u{00E9} | 4 cols (precomposed)\n |cafe\u{0301} | 4 cols (combining)", .{align_header});

const align_box = try align_style.render(alloc, align_content);

Expand All @@ -768,7 +768,7 @@ const Model = struct {
hint_style = hint_style.fg(zz.Color.gray(10));
hint_style = hint_style.italic(true);
hint_style = hint_style.inline_style(true);
const hint = try hint_style.render(alloc, "All text above is laid out using Unicode-aware display width.");
const hint = try hint_style.render(alloc, "Unicode width is terminal-dependent; this tab uses width-stable samples.");

return zz.joinVertical(alloc, &.{ top_row, "", mid_row, "", align_box, "", hint });
}
Expand Down
16 changes: 16 additions & 0 deletions src/core/context.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
const std = @import("std");
const Terminal = @import("../terminal/terminal.zig").Terminal;
const color_mod = @import("../style/color.zig");
const unicode_mod = @import("../unicode.zig");
const Logger = @import("log.zig").Logger;

/// Runtime context passed to init, update, and view functions
Expand Down Expand Up @@ -41,6 +42,15 @@ pub const Context = struct {
/// Whether the terminal has a dark background
is_dark_background: bool,

/// Active Unicode width strategy for text measurement
unicode_width_strategy: unicode_mod.WidthStrategy,

/// Whether DEC mode 2027 was successfully negotiated
terminal_mode_2027: bool,

/// Whether kitty text sizing support was detected
kitty_text_sizing: bool,

/// Access to internal state (for advanced use)
_terminal: ?*Terminal,

Expand Down Expand Up @@ -68,6 +78,9 @@ pub const Context = struct {
.color_256 = profile.supports256(),
.color_profile = profile,
.is_dark_background = color_mod.hasDarkBackground(),
.unicode_width_strategy = unicode_mod.getWidthStrategy(),
.terminal_mode_2027 = false,
.kitty_text_sizing = false,
._terminal = null,
};
}
Expand Down Expand Up @@ -153,6 +166,9 @@ pub const Options = struct {
/// Enable Kitty keyboard protocol
kitty_keyboard: bool = false,

/// Force Unicode width strategy (`null` = auto-detect)
unicode_width_strategy: ?unicode_mod.WidthStrategy = null,

/// Enable suspend/resume with Ctrl+Z
suspend_enabled: bool = true,
};
27 changes: 27 additions & 0 deletions src/core/program.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const Options = @import("context.zig").Options;
const message = @import("message.zig");
const command = @import("command.zig");
const Logger = @import("log.zig").Logger;
const unicode = @import("../unicode.zig");

pub const Cmd = command.Cmd;
pub const Msg = message;
Expand Down Expand Up @@ -164,6 +165,13 @@ pub fn Program(comptime Model: type) type {
self.context.height = size.rows;
self.context._terminal = &self.terminal.?;

const width_caps = self.terminal.?.getUnicodeWidthCapabilities();
const effective_width_strategy = self.resolveUnicodeWidthStrategy(width_caps.strategy);
self.context.unicode_width_strategy = effective_width_strategy;
self.context.terminal_mode_2027 = width_caps.mode_2027;
self.context.kitty_text_sizing = width_caps.kitty_text_sizing;
unicode.setWidthStrategy(effective_width_strategy);

// Initialize the model
const init_cmd = self.model.init(&self.context);
try self.processCommand(init_cmd);
Expand Down Expand Up @@ -324,6 +332,25 @@ pub fn Program(comptime Model: type) type {
return null;
}

fn resolveUnicodeWidthStrategy(self: *const Self, detected: unicode.WidthStrategy) unicode.WidthStrategy {
if (self.options.unicode_width_strategy) |forced| {
return forced;
}
if (envUnicodeWidthOverride()) |from_env| {
return from_env;
}
return detected;
}

fn envUnicodeWidthOverride() ?unicode.WidthStrategy {
const raw = std.process.getEnvVarOwned(std.heap.page_allocator, "ZZ_UNICODE_WIDTH") catch return null;
defer std.heap.page_allocator.free(raw);
if (std.ascii.eqlIgnoreCase(raw, "unicode")) return .unicode;
if (std.ascii.eqlIgnoreCase(raw, "legacy")) return .legacy_wcwidth;
if (std.ascii.eqlIgnoreCase(raw, "auto")) return null;
return null;
}

fn processMouseEvent(self: *Self, mouse_event: keyboard.MouseEvent) ?UserCmd {
if (@hasField(UserMsg, "mouse")) {
const user_msg = UserMsg{ .mouse = mouse_event };
Expand Down
5 changes: 5 additions & 0 deletions src/terminal/ansi.zig
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ pub const bracketed_paste_disable = CSI ++ "?2004l";
pub const sync_start = CSI ++ "?2026h";
pub const sync_end = CSI ++ "?2026l";

// Unicode width mode (DECRQM/DECSET private mode 2027)
pub const unicode_width_mode_query = CSI ++ "?2027$p";
pub const unicode_width_mode_enable = CSI ++ "?2027h";
pub const unicode_width_mode_disable = CSI ++ "?2027l";

// Kitty keyboard protocol
pub const kitty_keyboard_enable = CSI ++ ">1u";
pub const kitty_keyboard_disable = CSI ++ "<u";
Expand Down
Loading