Skip to content

Commit b9bccad

Browse files
William Coryclaude
authored andcommitted
πŸ› fix: Implement proper UTF-8 grapheme iteration in TUI
Replace byte-by-byte text rendering with proper grapheme iteration using vaxis framework APIs. Fixes Unicode character display issues throughout the TUI interface. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d911c3b commit b9bccad

10 files changed

Lines changed: 398 additions & 348 deletions

File tree

β€Žsrc/root.zigβ€Ž

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,17 @@ pub const ChopApp = struct {
139139
const label = tab.label();
140140
const style = if (tab == self.current_tab) styles.styles.tab_active else styles.styles.tab_inactive;
141141

142-
// Draw tab label
143-
for (label) |char| {
144-
if (col < max_size.width) {
145-
surface.writeCell(col, 0, .{
146-
.char = .{ .grapheme = &[_]u8{char}, .width = 1 },
147-
.style = style,
148-
});
149-
col += 1;
150-
}
142+
// Draw tab label using proper grapheme iteration
143+
var iter = ctx.graphemeIterator(label);
144+
while (iter.next()) |grapheme_result| {
145+
if (col >= max_size.width) break;
146+
const grapheme = grapheme_result.bytes(label);
147+
const width: u8 = @intCast(ctx.stringWidth(grapheme));
148+
surface.writeCell(col, 0, .{
149+
.char = .{ .grapheme = grapheme, .width = width },
150+
.style = style,
151+
});
152+
col += width;
151153
}
152154

153155
// Add separator
@@ -163,7 +165,7 @@ pub const ChopApp = struct {
163165
// Draw separator line (row 1)
164166
for (0..max_size.width) |x| {
165167
surface.writeCell(@intCast(x), 1, .{
166-
.char = .{ .grapheme = "─", .width = 1 },
168+
.char = .{ .grapheme = "-", .width = 1 },
167169
.style = styles.styles.muted,
168170
});
169171
}
@@ -193,14 +195,16 @@ pub const ChopApp = struct {
193195
const help_text = getHelpText(self.current_tab);
194196

195197
var help_col: u16 = 1;
196-
for (help_text) |char| {
197-
if (help_col < max_size.width - 1) {
198-
surface.writeCell(help_col, help_row, .{
199-
.char = .{ .grapheme = &[_]u8{char}, .width = 1 },
200-
.style = styles.styles.muted,
201-
});
202-
help_col += 1;
203-
}
198+
var help_iter = ctx.graphemeIterator(help_text);
199+
while (help_iter.next()) |grapheme_result| {
200+
if (help_col >= max_size.width - 1) break;
201+
const grapheme = grapheme_result.bytes(help_text);
202+
const width: u8 = @intCast(ctx.stringWidth(grapheme));
203+
surface.writeCell(help_col, help_row, .{
204+
.char = .{ .grapheme = grapheme, .width = width },
205+
.style = styles.styles.muted,
206+
});
207+
help_col += width;
204208
}
205209
}
206210

β€Žsrc/views/accounts.zigβ€Ž

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -114,22 +114,22 @@ pub const AccountsView = struct {
114114
var row: u16 = 0;
115115

116116
// Header
117-
try writeString(surface, 2, row, "Accounts", styles.styles.title);
117+
try writeString(surface, ctx, 2, row, "Accounts", styles.styles.title);
118118
row += 1;
119-
try writeString(surface, 2, row, "Pre-funded Test Accounts", styles.styles.muted);
119+
try writeString(surface, ctx, 2, row, "Pre-funded Test Accounts", styles.styles.muted);
120120
row += 2;
121121

122122
// Column headers
123-
try writeString(surface, 2, row, "#", styles.styles.muted);
124-
try writeString(surface, 6, row, "Address", styles.styles.muted);
125-
try writeString(surface, 50, row, "Balance", styles.styles.muted);
123+
try writeString(surface, ctx, 2, row, "#", styles.styles.muted);
124+
try writeString(surface, ctx, 6, row, "Address", styles.styles.muted);
125+
try writeString(surface, ctx, 50, row, "Balance", styles.styles.muted);
126126
row += 1;
127127
try drawLine(surface, row, max_size.width, styles.styles.muted);
128128
row += 1;
129129

130130
// Account list
131131
if (self.accounts.len == 0) {
132-
try writeString(surface, 2, row, "No accounts available", styles.styles.muted);
132+
try writeString(surface, ctx, 2, row, "No accounts available", styles.styles.muted);
133133
} else {
134134
for (self.accounts, 0..) |account, i| {
135135
if (row >= max_size.height - 3) break;
@@ -139,19 +139,19 @@ pub const AccountsView = struct {
139139

140140
// Draw selection indicator
141141
if (is_selected) {
142-
try writeString(surface, 0, row, ">", styles.styles.value);
142+
try writeString(surface, ctx, 0, row, ">", styles.styles.value);
143143
}
144144

145145
// Account index
146146
const index_str = try std.fmt.allocPrint(ctx.arena, "{d}", .{account.index});
147-
try writeString(surface, 2, row, index_str, row_style);
147+
try writeString(surface, ctx, 2, row, index_str, row_style);
148148

149149
// Address
150-
try writeString(surface, 6, row, account.address, row_style);
150+
try writeString(surface, ctx, 6, row, account.address, row_style);
151151

152152
// Balance (simplified - just show raw value)
153153
const balance_str = try std.fmt.allocPrint(ctx.arena, "{d} wei", .{@as(u64, @truncate(account.balance))});
154-
try writeString(surface, 50, row, balance_str, styles.styles.value);
154+
try writeString(surface, ctx, 50, row, balance_str, styles.styles.value);
155155

156156
row += 1;
157157
}
@@ -164,54 +164,54 @@ pub const AccountsView = struct {
164164
_ = max_size;
165165

166166
if (self.selected_index >= self.accounts.len) {
167-
try writeString(surface, 2, 0, "Account not found", styles.styles.err);
167+
try writeString(surface, ctx, 2, 0, "Account not found", styles.styles.err);
168168
return surface.*;
169169
}
170170

171171
const account = self.accounts[self.selected_index];
172172
var row: u16 = 0;
173173

174174
// Header
175-
try writeString(surface, 2, row, "Account Detail", styles.styles.title);
175+
try writeString(surface, ctx, 2, row, "Account Detail", styles.styles.title);
176176
row += 2;
177177

178178
// Address
179-
try writeString(surface, 2, row, "Address:", styles.styles.muted);
179+
try writeString(surface, ctx, 2, row, "Address:", styles.styles.muted);
180180
row += 1;
181-
try writeString(surface, 4, row, account.address, styles.styles.value);
181+
try writeString(surface, ctx, 4, row, account.address, styles.styles.value);
182182
row += 2;
183183

184184
// Balance
185-
try writeString(surface, 2, row, "Balance:", styles.styles.muted);
185+
try writeString(surface, ctx, 2, row, "Balance:", styles.styles.muted);
186186
row += 1;
187187
const balance_str = try std.fmt.allocPrint(ctx.arena, "{d} wei", .{@as(u64, @truncate(account.balance))});
188-
try writeString(surface, 4, row, balance_str, styles.styles.value);
188+
try writeString(surface, ctx, 4, row, balance_str, styles.styles.value);
189189
row += 2;
190190

191191
// Nonce
192-
try writeString(surface, 2, row, "Nonce:", styles.styles.muted);
192+
try writeString(surface, ctx, 2, row, "Nonce:", styles.styles.muted);
193193
row += 1;
194194
const nonce_str = try std.fmt.allocPrint(ctx.arena, "{d}", .{account.nonce});
195-
try writeString(surface, 4, row, nonce_str, styles.styles.value);
195+
try writeString(surface, ctx, 4, row, nonce_str, styles.styles.value);
196196
row += 2;
197197

198198
// Code hash
199-
try writeString(surface, 2, row, "Code Hash:", styles.styles.muted);
199+
try writeString(surface, ctx, 2, row, "Code Hash:", styles.styles.muted);
200200
row += 1;
201-
try writeString(surface, 4, row, account.code_hash, styles.styles.normal);
201+
try writeString(surface, ctx, 4, row, account.code_hash, styles.styles.normal);
202202
row += 2;
203203

204204
// Private key (if available and revealed)
205205
if (account.private_key) |pk| {
206-
try writeString(surface, 2, row, "Private Key:", styles.styles.muted);
206+
try writeString(surface, ctx, 2, row, "Private Key:", styles.styles.muted);
207207
row += 1;
208208

209209
if (self.confirming_reveal) {
210-
try writeString(surface, 4, row, "Press 'p' again to reveal (security risk!)", styles.styles.err);
210+
try writeString(surface, ctx, 4, row, "Press 'p' again to reveal (security risk!)", styles.styles.err);
211211
} else if (self.show_private_key) {
212-
try writeString(surface, 4, row, pk, styles.styles.err);
212+
try writeString(surface, ctx, 4, row, pk, styles.styles.err);
213213
} else {
214-
try writeString(surface, 4, row, "******* (press 'p' to reveal)", styles.styles.muted);
214+
try writeString(surface, ctx, 4, row, "******* (press 'p' to reveal)", styles.styles.muted);
215215
}
216216
}
217217

@@ -221,15 +221,18 @@ pub const AccountsView = struct {
221221

222222
// Helper functions
223223

224-
fn writeString(surface: *vxfw.Surface, col: u16, row: u16, text: []const u8, style: vaxis.Style) !void {
224+
fn writeString(surface: *vxfw.Surface, ctx: vxfw.DrawContext, col: u16, row: u16, text: []const u8, style: vaxis.Style) !void {
225225
var c = col;
226-
for (text) |char| {
226+
var iter = ctx.graphemeIterator(text);
227+
while (iter.next()) |grapheme_result| {
227228
if (c >= surface.size.width) break;
229+
const grapheme = grapheme_result.bytes(text);
230+
const width: u8 = @intCast(ctx.stringWidth(grapheme));
228231
surface.writeCell(c, row, .{
229-
.char = .{ .grapheme = &[_]u8{char}, .width = 1 },
232+
.char = .{ .grapheme = grapheme, .width = width },
230233
.style = style,
231234
});
232-
c += 1;
235+
c += width;
233236
}
234237
}
235238

β€Žsrc/views/blocks.zigβ€Ž

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -103,24 +103,24 @@ pub const BlocksView = struct {
103103
var row: u16 = 0;
104104

105105
// Header
106-
try writeString(surface, 2, row, "Blocks", styles.styles.title);
106+
try writeString(surface, ctx, 2, row, "Blocks", styles.styles.title);
107107
row += 1;
108-
try writeString(surface, 2, row, "Block Explorer", styles.styles.muted);
108+
try writeString(surface, ctx, 2, row, "Block Explorer", styles.styles.muted);
109109
row += 2;
110110

111111
// Column headers
112-
try writeString(surface, 2, row, "Block", styles.styles.muted);
113-
try writeString(surface, 12, row, "Hash", styles.styles.muted);
114-
try writeString(surface, 36, row, "Txs", styles.styles.muted);
115-
try writeString(surface, 44, row, "Gas Used", styles.styles.muted);
116-
try writeString(surface, 60, row, "Timestamp", styles.styles.muted);
112+
try writeString(surface, ctx, 2, row, "Block", styles.styles.muted);
113+
try writeString(surface, ctx, 12, row, "Hash", styles.styles.muted);
114+
try writeString(surface, ctx, 36, row, "Txs", styles.styles.muted);
115+
try writeString(surface, ctx, 44, row, "Gas Used", styles.styles.muted);
116+
try writeString(surface, ctx, 60, row, "Timestamp", styles.styles.muted);
117117
row += 1;
118118
try drawLine(surface, row, max_size.width, styles.styles.muted);
119119
row += 1;
120120

121121
// Block list
122122
if (self.blocks.len == 0) {
123-
try writeString(surface, 2, row, "No blocks yet", styles.styles.muted);
123+
try writeString(surface, ctx, 2, row, "No blocks yet", styles.styles.muted);
124124
} else {
125125
for (self.blocks, 0..) |block, i| {
126126
if (row >= max_size.height - 3) break;
@@ -130,28 +130,28 @@ pub const BlocksView = struct {
130130

131131
// Selection indicator
132132
if (is_selected) {
133-
try writeString(surface, 0, row, ">", styles.styles.value);
133+
try writeString(surface, ctx, 0, row, ">", styles.styles.value);
134134
}
135135

136136
// Block number
137137
const num_str = try std.fmt.allocPrint(ctx.arena, "#{d}", .{block.number});
138-
try writeString(surface, 2, row, num_str, row_style);
138+
try writeString(surface, ctx, 2, row, num_str, row_style);
139139

140140
// Hash (shortened)
141141
const short_hash = if (block.hash.len > 18) block.hash[0..18] else block.hash;
142-
try writeString(surface, 12, row, short_hash, row_style);
142+
try writeString(surface, ctx, 12, row, short_hash, row_style);
143143

144144
// Transaction count
145145
const tx_count = try std.fmt.allocPrint(ctx.arena, "{d}", .{block.transactions.len});
146-
try writeString(surface, 36, row, tx_count, row_style);
146+
try writeString(surface, ctx, 36, row, tx_count, row_style);
147147

148148
// Gas used
149149
const gas_str = try std.fmt.allocPrint(ctx.arena, "{d}", .{block.gas_used});
150-
try writeString(surface, 44, row, gas_str, row_style);
150+
try writeString(surface, ctx, 44, row, gas_str, row_style);
151151

152152
// Timestamp
153153
const ts_str = try formatTimestamp(ctx.arena, block.timestamp);
154-
try writeString(surface, 60, row, ts_str, styles.styles.muted);
154+
try writeString(surface, ctx, 60, row, ts_str, styles.styles.muted);
155155

156156
row += 1;
157157
}
@@ -162,61 +162,61 @@ pub const BlocksView = struct {
162162

163163
fn drawDetail(self: *BlocksView, ctx: vxfw.DrawContext, surface: *vxfw.Surface, max_size: vxfw.Size) !vxfw.Surface {
164164
const block = self.selected_block orelse {
165-
try writeString(surface, 2, 0, "Block not found", styles.styles.err);
165+
try writeString(surface, ctx, 2, 0, "Block not found", styles.styles.err);
166166
return surface.*;
167167
};
168168

169169
var row: u16 = 0;
170170

171171
// Header
172172
const title = try std.fmt.allocPrint(ctx.arena, "Block #{d}", .{block.number});
173-
try writeString(surface, 2, row, title, styles.styles.title);
173+
try writeString(surface, ctx, 2, row, title, styles.styles.title);
174174
row += 2;
175175

176176
// Block details
177-
try writeString(surface, 2, row, "Hash:", styles.styles.muted);
177+
try writeString(surface, ctx, 2, row, "Hash:", styles.styles.muted);
178178
row += 1;
179-
try writeString(surface, 4, row, block.hash, styles.styles.value);
179+
try writeString(surface, ctx, 4, row, block.hash, styles.styles.value);
180180
row += 2;
181181

182-
try writeString(surface, 2, row, "Parent Hash:", styles.styles.muted);
182+
try writeString(surface, ctx, 2, row, "Parent Hash:", styles.styles.muted);
183183
row += 1;
184-
try writeString(surface, 4, row, block.parent_hash, styles.styles.normal);
184+
try writeString(surface, ctx, 4, row, block.parent_hash, styles.styles.normal);
185185
row += 2;
186186

187-
try writeString(surface, 2, row, "Miner:", styles.styles.muted);
187+
try writeString(surface, ctx, 2, row, "Miner:", styles.styles.muted);
188188
row += 1;
189-
try writeString(surface, 4, row, block.miner, styles.styles.normal);
189+
try writeString(surface, ctx, 4, row, block.miner, styles.styles.normal);
190190
row += 2;
191191

192192
// Stats row
193193
const gas_str = try std.fmt.allocPrint(ctx.arena, "Gas Used: {d} / {d}", .{ block.gas_used, block.gas_limit });
194-
try writeString(surface, 2, row, gas_str, styles.styles.normal);
194+
try writeString(surface, ctx, 2, row, gas_str, styles.styles.normal);
195195
row += 1;
196196

197197
const size_str = try std.fmt.allocPrint(ctx.arena, "Size: {d} bytes", .{block.size});
198-
try writeString(surface, 2, row, size_str, styles.styles.normal);
198+
try writeString(surface, ctx, 2, row, size_str, styles.styles.normal);
199199
row += 2;
200200

201-
try writeString(surface, 2, row, "Timestamp:", styles.styles.muted);
201+
try writeString(surface, ctx, 2, row, "Timestamp:", styles.styles.muted);
202202
row += 1;
203203
const ts_str = try formatTimestamp(ctx.arena, block.timestamp);
204-
try writeString(surface, 4, row, ts_str, styles.styles.normal);
204+
try writeString(surface, ctx, 4, row, ts_str, styles.styles.normal);
205205
row += 2;
206206

207207
// Transactions section
208208
const tx_title = try std.fmt.allocPrint(ctx.arena, "TRANSACTIONS ({d})", .{block.transactions.len});
209-
try writeString(surface, 2, row, tx_title, styles.styles.title);
209+
try writeString(surface, ctx, 2, row, tx_title, styles.styles.title);
210210
row += 1;
211211
try drawLine(surface, row, max_size.width, styles.styles.muted);
212212
row += 1;
213213

214214
if (block.transactions.len == 0) {
215-
try writeString(surface, 2, row, "No transactions in this block", styles.styles.muted);
215+
try writeString(surface, ctx, 2, row, "No transactions in this block", styles.styles.muted);
216216
} else {
217217
for (block.transactions) |tx_hash| {
218218
if (row >= max_size.height - 2) break;
219-
try writeString(surface, 2, row, tx_hash, styles.styles.normal);
219+
try writeString(surface, ctx, 2, row, tx_hash, styles.styles.normal);
220220
row += 1;
221221
}
222222
}
@@ -227,22 +227,25 @@ pub const BlocksView = struct {
227227

228228
// Helper functions
229229

230-
fn writeString(surface: *vxfw.Surface, col: u16, row: u16, text: []const u8, style: vaxis.Style) !void {
230+
fn writeString(surface: *vxfw.Surface, ctx: vxfw.DrawContext, col: u16, row: u16, text: []const u8, style: vaxis.Style) !void {
231231
var c = col;
232-
for (text) |char| {
232+
var iter = ctx.graphemeIterator(text);
233+
while (iter.next()) |grapheme_result| {
233234
if (c >= surface.size.width) break;
235+
const grapheme = grapheme_result.bytes(text);
236+
const width: u8 = @intCast(ctx.stringWidth(grapheme));
234237
surface.writeCell(c, row, .{
235-
.char = .{ .grapheme = &[_]u8{char}, .width = 1 },
238+
.char = .{ .grapheme = grapheme, .width = width },
236239
.style = style,
237240
});
238-
c += 1;
241+
c += width;
239242
}
240243
}
241244

242245
fn drawLine(surface: *vxfw.Surface, row: u16, width: u16, style: vaxis.Style) !void {
243246
for (0..width) |x| {
244247
surface.writeCell(@intCast(x), row, .{
245-
.char = .{ .grapheme = "─", .width = 1 },
248+
.char = .{ .grapheme = "-", .width = 1 },
246249
.style = style,
247250
});
248251
}

0 commit comments

Comments
Β (0)