a modern tui library written in zig
1const std = @import("std");
2
3const Screen = @import("Screen.zig");
4const Cell = @import("Cell.zig");
5const Mouse = @import("Mouse.zig");
6const Segment = @import("Cell.zig").Segment;
7const Unicode = @import("Unicode.zig");
8const gw = @import("gwidth.zig");
9
10const log = std.log.scoped(.window);
11
12const Window = @This();
13
14pub const Size = union(enum) {
15 expand,
16 limit: usize,
17};
18
19/// horizontal offset from the screen
20x_off: usize,
21/// vertical offset from the screen
22y_off: usize,
23/// width of the window. This can't be larger than the terminal screen
24width: usize,
25/// height of the window. This can't be larger than the terminal screen
26height: usize,
27
28screen: *Screen,
29
30/// Deprecated. Use `child` instead
31///
32/// Creates a new window with offset relative to parent and size clamped to the
33/// parent's size. Windows do not retain a reference to their parent and are
34/// unaware of resizes.
35pub fn initChild(
36 self: Window,
37 x_off: usize,
38 y_off: usize,
39 width: Size,
40 height: Size,
41) Window {
42 const resolved_width = switch (width) {
43 .expand => self.width -| x_off,
44 .limit => |w| blk: {
45 if (w + x_off > self.width) {
46 break :blk self.width -| x_off;
47 }
48 break :blk w;
49 },
50 };
51 const resolved_height = switch (height) {
52 .expand => self.height -| y_off,
53 .limit => |h| blk: {
54 if (h + y_off > self.height) {
55 break :blk self.height -| y_off;
56 }
57 break :blk h;
58 },
59 };
60 return Window{
61 .x_off = x_off + self.x_off,
62 .y_off = y_off + self.y_off,
63 .width = resolved_width,
64 .height = resolved_height,
65 .screen = self.screen,
66 };
67}
68
69pub const ChildOptions = struct {
70 x_off: usize = 0,
71 y_off: usize = 0,
72 /// the width of the resulting child, including any borders
73 width: Size = .expand,
74 /// the height of the resulting child, including any borders
75 height: Size = .expand,
76 border: BorderOptions = .{},
77};
78
79pub const BorderOptions = struct {
80 style: Cell.Style = .{},
81 where: union(enum) {
82 none,
83 all,
84 top,
85 right,
86 bottom,
87 left,
88 other: Locations,
89 } = .none,
90 glyphs: Glyphs = .single_rounded,
91
92 pub const Locations = packed struct {
93 top: bool = false,
94 right: bool = false,
95 bottom: bool = false,
96 left: bool = false,
97 };
98
99 pub const Glyphs = union(enum) {
100 single_rounded,
101 single_square,
102 /// custom border glyphs. each glyph should be one cell wide and the
103 /// following indices apply:
104 /// [0] = top left
105 /// [1] = horizontal
106 /// [2] = top right
107 /// [3] = vertical
108 /// [4] = bottom right
109 /// [5] = bottom left
110 custom: [6][]const u8,
111 };
112
113 const single_rounded: [6][]const u8 = .{ "╭", "─", "╮", "│", "╯", "╰" };
114 const single_square: [6][]const u8 = .{ "┌", "─", "┐", "│", "┘", "└" };
115};
116
117/// create a child window
118pub fn child(self: Window, opts: ChildOptions) Window {
119 var result = self.initChild(opts.x_off, opts.y_off, opts.width, opts.height);
120
121 const glyphs = switch (opts.border.glyphs) {
122 .single_rounded => BorderOptions.single_rounded,
123 .single_square => BorderOptions.single_square,
124 .custom => |custom| custom,
125 };
126
127 const top_left: Cell.Character = .{ .grapheme = glyphs[0], .width = 1 };
128 const horizontal: Cell.Character = .{ .grapheme = glyphs[1], .width = 1 };
129 const top_right: Cell.Character = .{ .grapheme = glyphs[2], .width = 1 };
130 const vertical: Cell.Character = .{ .grapheme = glyphs[3], .width = 1 };
131 const bottom_right: Cell.Character = .{ .grapheme = glyphs[4], .width = 1 };
132 const bottom_left: Cell.Character = .{ .grapheme = glyphs[5], .width = 1 };
133 const style = opts.border.style;
134
135 const h = result.height;
136 const w = result.width;
137
138 const loc: BorderOptions.Locations = switch (opts.border.where) {
139 .none => return result,
140 .all => .{ .top = true, .bottom = true, .right = true, .left = true },
141 .bottom => .{ .bottom = true },
142 .right => .{ .right = true },
143 .left => .{ .left = true },
144 .top => .{ .top = true },
145 .other => |loc| loc,
146 };
147 if (loc.top) {
148 var i: usize = 0;
149 while (i < w) : (i += 1) {
150 result.writeCell(i, 0, .{ .char = horizontal, .style = style });
151 }
152 }
153 if (loc.bottom) {
154 var i: usize = 0;
155 while (i < w) : (i += 1) {
156 result.writeCell(i, h -| 1, .{ .char = horizontal, .style = style });
157 }
158 }
159 if (loc.left) {
160 var i: usize = 0;
161 while (i < h) : (i += 1) {
162 result.writeCell(0, i, .{ .char = vertical, .style = style });
163 }
164 }
165 if (loc.right) {
166 var i: usize = 0;
167 while (i < h) : (i += 1) {
168 result.writeCell(w -| 1, i, .{ .char = vertical, .style = style });
169 }
170 }
171 // draw corners
172 if (loc.top and loc.left)
173 result.writeCell(0, 0, .{ .char = top_left, .style = style });
174 if (loc.top and loc.right)
175 result.writeCell(w - 1, 0, .{ .char = top_right, .style = style });
176 if (loc.bottom and loc.left)
177 result.writeCell(0, h -| 1, .{ .char = bottom_left, .style = style });
178 if (loc.bottom and loc.right)
179 result.writeCell(w - 1, h -| 1, .{ .char = bottom_right, .style = style });
180
181 const x_off: usize = if (loc.left) 1 else 0;
182 const y_off: usize = if (loc.top) 1 else 0;
183 const h_delt: usize = if (loc.bottom) 1 else 0;
184 const w_delt: usize = if (loc.right) 1 else 0;
185 const h_ch: usize = h - y_off - h_delt;
186 const w_ch: usize = w - x_off - w_delt;
187 return result.initChild(x_off, y_off, .{ .limit = w_ch }, .{ .limit = h_ch });
188}
189
190/// writes a cell to the location in the window
191pub fn writeCell(self: Window, col: usize, row: usize, cell: Cell) void {
192 if (self.height == 0 or self.width == 0) return;
193 if (self.height <= row or self.width <= col) return;
194 self.screen.writeCell(col + self.x_off, row + self.y_off, cell);
195}
196
197/// reads a cell at the location in the window
198pub fn readCell(self: Window, col: usize, row: usize) ?Cell {
199 if (self.height == 0 or self.width == 0) return null;
200 if (self.height <= row or self.width <= col) return null;
201 return self.screen.readCell(col + self.x_off, row + self.y_off);
202}
203
204/// fills the window with the default cell
205pub fn clear(self: Window) void {
206 self.fill(.{ .default = true });
207}
208
209/// returns the width of the grapheme. This depends on the terminal capabilities
210pub fn gwidth(self: Window, str: []const u8) usize {
211 return gw.gwidth(str, self.screen.width_method, &self.screen.unicode.width_data) catch 1;
212}
213
214/// fills the window with the provided cell
215pub fn fill(self: Window, cell: Cell) void {
216 if (self.screen.width < self.x_off)
217 return;
218 if (self.screen.height < self.y_off)
219 return;
220 if (self.x_off == 0 and self.width == self.screen.width) {
221 // we have a full width window, therefore contiguous memory.
222 const start = self.y_off * self.width;
223 const end = start + (self.height * self.width);
224 @memset(self.screen.buf[start..end], cell);
225 } else {
226 // Non-contiguous. Iterate over rows an memset
227 var row: usize = self.y_off;
228 const last_row = @min(self.height + self.y_off, self.screen.height);
229 while (row < last_row) : (row += 1) {
230 const start = self.x_off + (row * self.screen.width);
231 const end = @min(start + self.width, start + (self.screen.width - self.x_off));
232 @memset(self.screen.buf[start..end], cell);
233 }
234 }
235}
236
237/// hide the cursor
238pub fn hideCursor(self: Window) void {
239 self.screen.cursor_vis = false;
240}
241
242/// show the cursor at the given coordinates, 0 indexed
243pub fn showCursor(self: Window, col: usize, row: usize) void {
244 if (self.height == 0 or self.width == 0) return;
245 if (self.height <= row or self.width <= col) return;
246 self.screen.cursor_vis = true;
247 self.screen.cursor_row = row + self.y_off;
248 self.screen.cursor_col = col + self.x_off;
249}
250
251pub fn setCursorShape(self: Window, shape: Cell.CursorShape) void {
252 self.screen.cursor_shape = shape;
253}
254
255/// Options to use when printing Segments to a window
256pub const PrintOptions = struct {
257 /// vertical offset to start printing at
258 row_offset: usize = 0,
259
260 /// wrap behavior for printing
261 wrap: enum {
262 /// wrap at grapheme boundaries
263 grapheme,
264 /// wrap at word boundaries
265 word,
266 /// stop printing after one line
267 none,
268 } = .grapheme,
269
270 /// when true, print will write to the screen for rendering. When false,
271 /// nothing is written. The return value describes the size of the wrapped
272 /// text
273 commit: bool = true,
274};
275
276pub const PrintResult = struct {
277 col: usize,
278 row: usize,
279 overflow: bool,
280};
281
282/// prints segments to the window. returns true if the text overflowed with the
283/// given wrap strategy and size.
284pub fn print(self: Window, segments: []const Segment, opts: PrintOptions) !PrintResult {
285 var row = opts.row_offset;
286 switch (opts.wrap) {
287 .grapheme => {
288 var col: usize = 0;
289 const overflow: bool = blk: for (segments) |segment| {
290 var iter = self.screen.unicode.graphemeIterator(segment.text);
291 while (iter.next()) |grapheme| {
292 if (col >= self.width) {
293 row += 1;
294 col = 0;
295 }
296 if (row >= self.height) break :blk true;
297 const s = grapheme.bytes(segment.text);
298 if (std.mem.eql(u8, s, "\n")) {
299 row +|= 1;
300 col = 0;
301 continue;
302 }
303 const w = self.gwidth(s);
304 if (w == 0) continue;
305 if (opts.commit) self.writeCell(col, row, .{
306 .char = .{
307 .grapheme = s,
308 .width = w,
309 },
310 .style = segment.style,
311 .link = segment.link,
312 });
313 col += w;
314 }
315 } else false;
316 if (col >= self.width) {
317 row += 1;
318 col = 0;
319 }
320 return .{
321 .row = row,
322 .col = col,
323 .overflow = overflow,
324 };
325 },
326 .word => {
327 var col: usize = 0;
328 var overflow: bool = false;
329 var soft_wrapped: bool = false;
330 for (segments) |segment| {
331 var start: usize = 0;
332 var i: usize = 0;
333 while (i < segment.text.len) : (i += 1) {
334 // for (segment.text, 0..) |b, i| {
335 const b = segment.text[i];
336 const end = switch (b) {
337 ' ',
338 '\r',
339 '\n',
340 => i,
341 else => if (i != segment.text.len - 1) continue else i + 1,
342 };
343 const word = segment.text[start..end];
344 // find the start of the next word
345 start = while (i + 1 < segment.text.len) : (i += 1) {
346 if (segment.text[i + 1] == ' ') continue;
347 break i + 1;
348 } else i;
349 const width = self.gwidth(word);
350 const non_wsp_width: usize = for (word, 0..) |wb, wi| {
351 if (wb == '\r' or wb == '\n') {
352 row += 1;
353 col = 0;
354 break width -| wi -| 1;
355 }
356 if (wb != ' ') break width - wi;
357 } else 0;
358
359 if (width + col > self.width and non_wsp_width < self.width) {
360 // wrap
361 row += 1;
362 col = 0;
363 soft_wrapped = true;
364 }
365 if (row >= self.height) {
366 overflow = true;
367 break;
368 }
369 // if we are soft wrapped, (col == 0 and row > 0), then trim
370 // leading spaces
371 const printed_word = if (soft_wrapped)
372 std.mem.trimLeft(u8, word, " ")
373 else
374 word;
375 defer soft_wrapped = false;
376 var iter = self.screen.unicode.graphemeIterator(printed_word);
377 while (iter.next()) |grapheme| {
378 const s = grapheme.bytes(printed_word);
379 const w = self.gwidth(s);
380 if (opts.commit) self.writeCell(col, row, .{
381 .char = .{
382 .grapheme = s,
383 .width = w,
384 },
385 .style = segment.style,
386 .link = segment.link,
387 });
388 col += w;
389 if (col >= self.width) {
390 row += 1;
391 col = 0;
392 }
393 }
394 switch (b) {
395 ' ' => {
396 if (col > 0) {
397 if (opts.commit) self.writeCell(col, row, .{
398 .char = .{
399 .grapheme = " ",
400 .width = 1,
401 },
402 .style = segment.style,
403 .link = segment.link,
404 });
405 col += 1;
406 }
407 },
408 '\r',
409 '\n',
410 => {
411 col = 0;
412 row += 1;
413 },
414 else => {},
415 }
416 }
417 }
418 return .{
419 // remove last row counter
420 .row = row,
421 .col = col,
422 .overflow = overflow,
423 };
424 },
425 .none => {
426 var col: usize = 0;
427 const overflow: bool = blk: for (segments) |segment| {
428 var iter = self.screen.unicode.graphemeIterator(segment.text);
429 while (iter.next()) |grapheme| {
430 if (col >= self.width) break :blk true;
431 const s = grapheme.bytes(segment.text);
432 if (std.mem.eql(u8, s, "\n")) break :blk true;
433 const w = self.gwidth(s);
434 if (w == 0) continue;
435 if (opts.commit) self.writeCell(col, row, .{
436 .char = .{
437 .grapheme = s,
438 .width = w,
439 },
440 .style = segment.style,
441 .link = segment.link,
442 });
443 col +|= w;
444 }
445 } else false;
446 return .{
447 .row = row,
448 .col = col,
449 .overflow = overflow,
450 };
451 },
452 }
453 return false;
454}
455
456/// print a single segment. This is just a shortcut for print(&.{segment}, opts)
457pub fn printSegment(self: Window, segment: Segment, opts: PrintOptions) !PrintResult {
458 return self.print(&.{segment}, opts);
459}
460
461/// scrolls the window down one row (IE inserts a blank row at the bottom of the
462/// screen and shifts all rows up one)
463pub fn scroll(self: Window, n: usize) void {
464 if (n > self.height) return;
465 var row = self.y_off;
466 while (row < self.height - n) : (row += 1) {
467 const dst_start = (row * self.width) + self.x_off;
468 const dst_end = dst_start + self.width;
469
470 const src_start = ((row + n) * self.width) + self.x_off;
471 const src_end = src_start + self.width;
472 @memcpy(self.screen.buf[dst_start..dst_end], self.screen.buf[src_start..src_end]);
473 }
474 const last_row = self.child(.{
475 .y_off = self.height - n,
476 });
477 last_row.clear();
478}
479
480/// returns the mouse event if the mouse event occurred within the window. If
481/// the mouse event occurred outside the window, null is returned
482pub fn hasMouse(win: Window, mouse: ?Mouse) ?Mouse {
483 const event = mouse orelse return null;
484 if (event.col >= win.x_off and
485 event.col < (win.x_off + win.width) and
486 event.row >= win.y_off and
487 event.row < (win.y_off + win.height)) return event else return null;
488}
489
490test "Window size set" {
491 var parent = Window{
492 .x_off = 0,
493 .y_off = 0,
494 .width = 20,
495 .height = 20,
496 .screen = undefined,
497 };
498
499 const ch = parent.initChild(1, 1, .expand, .expand);
500 try std.testing.expectEqual(19, ch.width);
501 try std.testing.expectEqual(19, ch.height);
502}
503
504test "Window size set too big" {
505 var parent = Window{
506 .x_off = 0,
507 .y_off = 0,
508 .width = 20,
509 .height = 20,
510 .screen = undefined,
511 };
512
513 const ch = parent.initChild(0, 0, .{ .limit = 21 }, .{ .limit = 21 });
514 try std.testing.expectEqual(20, ch.width);
515 try std.testing.expectEqual(20, ch.height);
516}
517
518test "Window size set too big with offset" {
519 var parent = Window{
520 .x_off = 0,
521 .y_off = 0,
522 .width = 20,
523 .height = 20,
524 .screen = undefined,
525 };
526
527 const ch = parent.initChild(10, 10, .{ .limit = 21 }, .{ .limit = 21 });
528 try std.testing.expectEqual(10, ch.width);
529 try std.testing.expectEqual(10, ch.height);
530}
531
532test "Window size nested offsets" {
533 var parent = Window{
534 .x_off = 1,
535 .y_off = 1,
536 .width = 20,
537 .height = 20,
538 .screen = undefined,
539 };
540
541 const ch = parent.initChild(10, 10, .{ .limit = 21 }, .{ .limit = 21 });
542 try std.testing.expectEqual(11, ch.x_off);
543 try std.testing.expectEqual(11, ch.y_off);
544}
545
546test "print: grapheme" {
547 const alloc = std.testing.allocator_instance.allocator();
548 const unicode = try Unicode.init(alloc);
549 defer unicode.deinit();
550 var screen: Screen = .{ .width_method = .unicode, .unicode = &unicode };
551 const win: Window = .{
552 .x_off = 0,
553 .y_off = 0,
554 .width = 4,
555 .height = 2,
556 .screen = &screen,
557 };
558 const opts: PrintOptions = .{
559 .commit = false,
560 .wrap = .grapheme,
561 };
562
563 {
564 var segments = [_]Segment{
565 .{ .text = "a" },
566 };
567 const result = try win.print(&segments, opts);
568 try std.testing.expectEqual(1, result.col);
569 try std.testing.expectEqual(0, result.row);
570 try std.testing.expectEqual(false, result.overflow);
571 }
572 {
573 var segments = [_]Segment{
574 .{ .text = "abcd" },
575 };
576 const result = try win.print(&segments, opts);
577 try std.testing.expectEqual(0, result.col);
578 try std.testing.expectEqual(1, result.row);
579 try std.testing.expectEqual(false, result.overflow);
580 }
581 {
582 var segments = [_]Segment{
583 .{ .text = "abcde" },
584 };
585 const result = try win.print(&segments, opts);
586 try std.testing.expectEqual(1, result.col);
587 try std.testing.expectEqual(1, result.row);
588 try std.testing.expectEqual(false, result.overflow);
589 }
590 {
591 var segments = [_]Segment{
592 .{ .text = "abcdefgh" },
593 };
594 const result = try win.print(&segments, opts);
595 try std.testing.expectEqual(0, result.col);
596 try std.testing.expectEqual(2, result.row);
597 try std.testing.expectEqual(false, result.overflow);
598 }
599 {
600 var segments = [_]Segment{
601 .{ .text = "abcdefghi" },
602 };
603 const result = try win.print(&segments, opts);
604 try std.testing.expectEqual(0, result.col);
605 try std.testing.expectEqual(2, result.row);
606 try std.testing.expectEqual(true, result.overflow);
607 }
608}
609
610test "print: word" {
611 const alloc = std.testing.allocator_instance.allocator();
612 const unicode = try Unicode.init(alloc);
613 defer unicode.deinit();
614 var screen: Screen = .{
615 .width_method = .unicode,
616 .unicode = &unicode,
617 };
618 const win: Window = .{
619 .x_off = 0,
620 .y_off = 0,
621 .width = 4,
622 .height = 2,
623 .screen = &screen,
624 };
625 const opts: PrintOptions = .{
626 .commit = false,
627 .wrap = .word,
628 };
629
630 {
631 var segments = [_]Segment{
632 .{ .text = "a" },
633 };
634 const result = try win.print(&segments, opts);
635 try std.testing.expectEqual(1, result.col);
636 try std.testing.expectEqual(0, result.row);
637 try std.testing.expectEqual(false, result.overflow);
638 }
639 {
640 var segments = [_]Segment{
641 .{ .text = "a b" },
642 };
643 const result = try win.print(&segments, opts);
644 try std.testing.expectEqual(3, result.col);
645 try std.testing.expectEqual(0, result.row);
646 try std.testing.expectEqual(false, result.overflow);
647 }
648 {
649 var segments = [_]Segment{
650 .{ .text = "a b c" },
651 };
652 const result = try win.print(&segments, opts);
653 try std.testing.expectEqual(1, result.col);
654 try std.testing.expectEqual(1, result.row);
655 try std.testing.expectEqual(false, result.overflow);
656 }
657 {
658 var segments = [_]Segment{
659 .{ .text = "hello" },
660 };
661 const result = try win.print(&segments, opts);
662 try std.testing.expectEqual(1, result.col);
663 try std.testing.expectEqual(1, result.row);
664 try std.testing.expectEqual(false, result.overflow);
665 }
666 {
667 var segments = [_]Segment{
668 .{ .text = "hi tim" },
669 };
670 const result = try win.print(&segments, opts);
671 try std.testing.expectEqual(3, result.col);
672 try std.testing.expectEqual(1, result.row);
673 try std.testing.expectEqual(false, result.overflow);
674 }
675 {
676 var segments = [_]Segment{
677 .{ .text = "hello tim" },
678 };
679 const result = try win.print(&segments, opts);
680 try std.testing.expectEqual(0, result.col);
681 try std.testing.expectEqual(2, result.row);
682 try std.testing.expectEqual(true, result.overflow);
683 }
684 {
685 var segments = [_]Segment{
686 .{ .text = "hello ti" },
687 };
688 const result = try win.print(&segments, opts);
689 try std.testing.expectEqual(0, result.col);
690 try std.testing.expectEqual(2, result.row);
691 try std.testing.expectEqual(false, result.overflow);
692 }
693 {
694 var segments = [_]Segment{
695 .{ .text = "h" },
696 .{ .text = "e" },
697 };
698 const result = try win.print(&segments, opts);
699 try std.testing.expectEqual(2, result.col);
700 try std.testing.expectEqual(0, result.row);
701 try std.testing.expectEqual(false, result.overflow);
702 }
703 {
704 var segments = [_]Segment{
705 .{ .text = "h" },
706 .{ .text = "e" },
707 .{ .text = "l" },
708 .{ .text = "l" },
709 .{ .text = "o" },
710 };
711 const result = try win.print(&segments, opts);
712 try std.testing.expectEqual(1, result.col);
713 try std.testing.expectEqual(1, result.row);
714 try std.testing.expectEqual(false, result.overflow);
715 }
716 {
717 var segments = [_]Segment{
718 .{ .text = "he\n" },
719 };
720 const result = try win.print(&segments, opts);
721 try std.testing.expectEqual(0, result.col);
722 try std.testing.expectEqual(1, result.row);
723 try std.testing.expectEqual(false, result.overflow);
724 }
725 {
726 var segments = [_]Segment{
727 .{ .text = "he\n\n" },
728 };
729 const result = try win.print(&segments, opts);
730 try std.testing.expectEqual(0, result.col);
731 try std.testing.expectEqual(2, result.row);
732 try std.testing.expectEqual(false, result.overflow);
733 }
734 {
735 var segments = [_]Segment{
736 .{ .text = "not now" },
737 };
738 const result = try win.print(&segments, opts);
739 try std.testing.expectEqual(3, result.col);
740 try std.testing.expectEqual(1, result.row);
741 try std.testing.expectEqual(false, result.overflow);
742 }
743 {
744 var segments = [_]Segment{
745 .{ .text = "note now" },
746 };
747 const result = try win.print(&segments, opts);
748 try std.testing.expectEqual(3, result.col);
749 try std.testing.expectEqual(1, result.row);
750 try std.testing.expectEqual(false, result.overflow);
751 }
752}