地圖 (Jido) is a lightweight Unix TUI file explorer designed for speed and simplicity.
1const std = @import("std");
2const builtin = @import("builtin");
3
4const Logger = @import("./log.zig").Logger;
5const environment = @import("./environment.zig");
6const Notification = @import("./notification.zig");
7const config = &@import("./config.zig").config;
8const List = @import("./list.zig").List;
9const Directories = @import("./directories.zig");
10const CircStack = @import("./circ_stack.zig").CircularStack;
11
12const zuid = @import("zuid");
13
14const vaxis = @import("vaxis");
15const TextInput = @import("vaxis").widgets.TextInput;
16const Cell = vaxis.Cell;
17const Key = vaxis.Key;
18
19pub const State = enum {
20 normal,
21 fuzzy,
22 new_dir,
23 new_file,
24 change_dir,
25 rename,
26};
27
28const InputReturnStatus = enum {
29 exit,
30 default,
31};
32
33const ActionPaths = struct {
34 /// Allocated.
35 old: []const u8,
36 /// Allocated.
37 new: []const u8,
38};
39
40pub const Action = union(enum) {
41 delete: ActionPaths,
42 rename: ActionPaths,
43};
44
45const Event = union(enum) {
46 key_press: vaxis.Key,
47 winsize: vaxis.Winsize,
48};
49
50const top_div = 1;
51const info_div = 1;
52const bottom_div = 1;
53const actions_len = 100;
54
55const App = @This();
56
57alloc: std.mem.Allocator,
58vx: vaxis.Vaxis = undefined,
59tty: vaxis.Tty = undefined,
60logger: Logger,
61state: State = .normal,
62actions: CircStack(Action, actions_len),
63
64// Used to detect whether to re-render an image.
65current_item_path_buf: [std.fs.max_path_bytes]u8 = undefined,
66current_item_path: []u8 = "",
67last_item_path_buf: [std.fs.max_path_bytes]u8 = undefined,
68last_item_path: []u8 = "",
69
70directories: Directories,
71notification: Notification,
72
73text_input: TextInput,
74text_input_buf: [std.fs.max_path_bytes]u8 = undefined,
75
76image: ?vaxis.Image = null,
77last_known_height: usize,
78
79pub fn init(alloc: std.mem.Allocator) !App {
80 var vx = try vaxis.init(alloc, .{
81 .kitty_keyboard_flags = .{
82 .report_text = false,
83 .disambiguate = false,
84 .report_events = false,
85 .report_alternate_keys = false,
86 .report_all_as_ctl_seqs = false,
87 },
88 });
89
90 return App{
91 .alloc = alloc,
92 .vx = vx,
93 .tty = try vaxis.Tty.init(),
94 .directories = try Directories.init(alloc),
95 .logger = Logger{},
96 .text_input = TextInput.init(alloc, &vx.unicode),
97 .notification = Notification{},
98 .actions = CircStack(Action, actions_len).init(),
99 .last_known_height = vx.window().height,
100 };
101}
102
103pub fn deinit(self: *App) void {
104 for (self.actions.buf[0..self.actions.count]) |action| {
105 switch (action) {
106 .delete, .rename => |a| {
107 self.alloc.free(a.new);
108 self.alloc.free(a.old);
109 },
110 }
111 }
112
113 self.directories.deinit();
114 self.text_input.deinit();
115 self.vx.deinit(self.alloc, self.tty.anyWriter());
116 self.tty.deinit();
117}
118
119pub fn run(self: *App) !void {
120 self.logger.init();
121 self.notification.init();
122
123 try self.directories.populate_entries("");
124
125 var loop: vaxis.Loop(Event) = .{ .vaxis = &self.vx, .tty = &self.tty };
126 try loop.start();
127 defer loop.stop();
128
129 try self.vx.enterAltScreen(self.tty.anyWriter());
130 try self.vx.queryTerminal(self.tty.anyWriter(), 1 * std.time.ns_per_s);
131 self.vx.queueRefresh();
132
133 while (true) {
134 self.notification.reset();
135 const event = loop.nextEvent();
136
137 switch (self.state) {
138 .normal => {
139 switch (try self.handle_normal_event(event, &loop)) {
140 .exit => return,
141 .default => {},
142 }
143 },
144 .fuzzy, .new_file, .new_dir, .rename, .change_dir => {
145 switch (try self.handle_input_event(event)) {
146 .exit => return,
147 .default => {},
148 }
149 },
150 }
151
152 try self.draw();
153 }
154}
155
156pub fn inputToSlice(self: *App) []const u8 {
157 self.text_input.cursor_idx = self.text_input.grapheme_count;
158 return self.text_input.sliceToCursor(&self.text_input_buf);
159}
160
161pub fn handle_normal_event(self: *App, event: Event, loop: *vaxis.Loop(Event)) !InputReturnStatus {
162 switch (event) {
163 .key_press => |key| {
164 if ((key.codepoint == 'c' and key.mods.ctrl) or key.codepoint == 'q') {
165 return .exit;
166 }
167
168 switch (key.codepoint) {
169 '-', 'h', Key.left => {
170 self.text_input.clearAndFree();
171
172 if (self.directories.dir.openDir("../", .{ .iterate = true })) |dir| {
173 self.directories.dir = dir;
174
175 self.directories.cleanup();
176 const fuzzy = self.inputToSlice();
177 self.directories.populate_entries(fuzzy) catch |err| {
178 switch (err) {
179 error.AccessDenied => try self.notification.write_err(.PermissionDenied),
180 else => try self.notification.write_err(.UnknownError),
181 }
182 };
183
184 if (self.directories.history.pop()) |history| {
185 self.directories.entries.selected = history.selected;
186 self.directories.entries.offset = history.offset;
187 }
188 } else |err| {
189 switch (err) {
190 error.AccessDenied => try self.notification.write_err(.PermissionDenied),
191 else => try self.notification.write_err(.UnknownError),
192 }
193 }
194 },
195 Key.enter, 'l', Key.right => {
196 const entry = try self.directories.get_selected();
197
198 switch (entry.kind) {
199 .directory => {
200 self.text_input.clearAndFree();
201
202 if (self.directories.dir.openDir(entry.name, .{ .iterate = true })) |dir| {
203 self.directories.dir = dir;
204
205 self.directories.history.push(.{
206 .selected = self.directories.entries.selected,
207 .offset = self.directories.entries.offset,
208 });
209
210 self.directories.cleanup();
211 const fuzzy = self.inputToSlice();
212 self.directories.populate_entries(fuzzy) catch |err| {
213 switch (err) {
214 error.AccessDenied => try self.notification.write_err(.PermissionDenied),
215 else => try self.notification.write_err(.UnknownError),
216 }
217 };
218 } else |err| {
219 switch (err) {
220 error.AccessDenied => try self.notification.write_err(.PermissionDenied),
221 else => try self.notification.write_err(.UnknownError),
222 }
223 }
224 },
225 .file => {
226 if (environment.get_editor()) |editor| {
227 try self.vx.exitAltScreen(self.tty.anyWriter());
228 try self.vx.resetState(self.tty.anyWriter());
229 loop.stop();
230
231 environment.open_file(self.alloc, self.directories.dir, entry.name, editor) catch {
232 try self.notification.write_err(.UnableToOpenFile);
233 };
234
235 try loop.start();
236 try self.vx.enterAltScreen(self.tty.anyWriter());
237 try self.vx.enableDetectedFeatures(self.tty.anyWriter());
238 self.vx.queueRefresh();
239 } else {
240 try self.notification.write_err(.EditorNotSet);
241 }
242 },
243 else => {},
244 }
245 },
246 'j', Key.down => {
247 self.directories.entries.next(self.last_known_height);
248 },
249 'k', Key.up => {
250 self.directories.entries.previous(self.last_known_height);
251 },
252 'G' => {
253 self.directories.entries.select_last(self.last_known_height);
254 },
255 'g' => {
256 self.directories.entries.select_first();
257 },
258 'D' => {
259 const entry = self.directories.get_selected() catch {
260 try self.notification.write_err(.UnableToDeleteItem);
261 return .default;
262 };
263
264 var old_path_buf: [std.fs.max_path_bytes]u8 = undefined;
265 const old_path = try self.alloc.dupe(u8, try self.directories.dir.realpath(entry.name, &old_path_buf));
266 var tmp_path_buf: [std.fs.max_path_bytes]u8 = undefined;
267 const tmp_path = try self.alloc.dupe(u8, try std.fmt.bufPrint(&tmp_path_buf, "/tmp/{s}-{s}", .{ entry.name, zuid.new.v4().toString() }));
268
269 try self.notification.write("Deleting item...", .info);
270 if (self.directories.dir.rename(entry.name, tmp_path)) {
271 // TODO: Will leak memory if pushing to a full stack.
272 self.actions.push(.{
273 .delete = .{ .old = old_path, .new = tmp_path },
274 });
275 try self.notification.write("Deleted item.", .info);
276
277 self.directories.remove_selected();
278 } else |_| {
279 try self.notification.write_err(.UnableToDeleteItem);
280 }
281 },
282 'd' => {
283 self.state = .new_dir;
284 },
285 '%' => {
286 self.state = .new_file;
287 },
288 'u' => {
289 if (self.actions.pop()) |action| {
290 const selected = self.directories.entries.selected;
291
292 switch (action) {
293 .delete => |a| {
294 // TODO: Will overwrite an item if it has the same name.
295 if (self.directories.dir.rename(a.new, a.old)) {
296 defer self.alloc.free(a.new);
297 defer self.alloc.free(a.old);
298
299 self.directories.cleanup();
300 const fuzzy = self.inputToSlice();
301 self.directories.populate_entries(fuzzy) catch |err| {
302 switch (err) {
303 error.AccessDenied => try self.notification.write_err(.PermissionDenied),
304 else => try self.notification.write_err(.UnknownError),
305 }
306 };
307 try self.notification.write("Restored deleted item.", .info);
308 } else |_| {
309 try self.notification.write_err(.UnableToUndo);
310 }
311 },
312 .rename => |a| {
313 // TODO: Will overwrite an item if it has the same name.
314 if (self.directories.dir.rename(a.new, a.old)) {
315 defer self.alloc.free(a.new);
316 defer self.alloc.free(a.old);
317
318 self.directories.cleanup();
319 const fuzzy = self.inputToSlice();
320 self.directories.populate_entries(fuzzy) catch |err| {
321 switch (err) {
322 error.AccessDenied => try self.notification.write_err(.PermissionDenied),
323 else => try self.notification.write_err(.UnknownError),
324 }
325 };
326 try self.notification.write("Restored previous item name.", .info);
327 } else |_| {
328 try self.notification.write_err(.UnableToUndo);
329 }
330 },
331 }
332
333 self.directories.entries.selected = selected;
334 } else {
335 try self.notification.write("Nothing to undo.", .info);
336 }
337 },
338 '/' => {
339 self.state = .fuzzy;
340 },
341 'R' => {
342 self.state = .rename;
343
344 const entry = try self.directories.get_selected();
345 self.text_input.insertSliceAtCursor(entry.name) catch {
346 self.state = .normal;
347 try self.notification.write_err(.UnableToRename);
348 };
349 },
350 'c' => {
351 self.state = .change_dir;
352 },
353 else => {
354 // log.debug("codepoint: {d}\n", .{key.codepoint});
355 },
356 }
357 },
358 .winsize => |ws| {
359 try self.vx.resize(self.alloc, self.tty.anyWriter(), ws);
360 },
361 }
362
363 return .default;
364}
365
366pub fn handle_input_event(self: *App, event: Event) !InputReturnStatus {
367 switch (event) {
368 .key_press => |key| {
369 if ((key.codepoint == 'c' and key.mods.ctrl) or key.codepoint == 'q') {
370 return .exit;
371 }
372
373 switch (key.codepoint) {
374 Key.escape => {
375 switch (self.state) {
376 .fuzzy => {
377 self.directories.cleanup();
378 self.directories.populate_entries("") catch |err| {
379 switch (err) {
380 error.AccessDenied => try self.notification.write_err(.PermissionDenied),
381 else => try self.notification.write_err(.UnknownError),
382 }
383 };
384 },
385 else => {},
386 }
387
388 self.text_input.clearAndFree();
389 self.state = .normal;
390 },
391 Key.enter => {
392 const selected = self.directories.entries.selected;
393 switch (self.state) {
394 .new_dir => {
395 const dir = self.inputToSlice();
396 if (self.directories.dir.makeDir(dir)) {
397 self.directories.cleanup();
398 self.directories.populate_entries("") catch |err| {
399 switch (err) {
400 error.AccessDenied => try self.notification.write_err(.PermissionDenied),
401 else => try self.notification.write_err(.UnknownError),
402 }
403 };
404 } else |err| {
405 switch (err) {
406 error.AccessDenied => try self.notification.write_err(.PermissionDenied),
407 error.PathAlreadyExists => try self.notification.write_err(.ItemAlreadyExists),
408 else => try self.notification.write_err(.UnknownError),
409 }
410 }
411 self.text_input.clearAndFree();
412 },
413 .new_file => {
414 const file = self.inputToSlice();
415 if (environment.file_exists(self.directories.dir, file)) {
416 try self.notification.write_err(.ItemAlreadyExists);
417 } else {
418 if (self.directories.dir.createFile(file, .{})) |f| {
419 f.close();
420 self.directories.cleanup();
421 self.directories.populate_entries("") catch |err| {
422 switch (err) {
423 error.AccessDenied => try self.notification.write_err(.PermissionDenied),
424 else => try self.notification.write_err(.UnknownError),
425 }
426 };
427 } else |err| {
428 switch (err) {
429 error.AccessDenied => try self.notification.write_err(.PermissionDenied),
430 else => try self.notification.write_err(.UnknownError),
431 }
432 }
433 }
434 self.text_input.clearAndFree();
435 },
436 .rename => {
437 var dir_prefix_buf: [std.fs.max_path_bytes]u8 = undefined;
438 const dir_prefix = try self.directories.dir.realpath(".", &dir_prefix_buf);
439
440 const old = try self.directories.get_selected();
441 const new = self.inputToSlice();
442
443 if (environment.file_exists(self.directories.dir, new)) {
444 try self.notification.write_err(.ItemAlreadyExists);
445 } else {
446 self.directories.dir.rename(old.name, new) catch |err| switch (err) {
447 error.AccessDenied => try self.notification.write_err(.PermissionDenied),
448 error.PathAlreadyExists => try self.notification.write_err(.ItemAlreadyExists),
449 else => try self.notification.write_err(.UnknownError),
450 };
451 // TODO: Will leak memory if pushing to a full stack.
452 self.actions.push(.{
453 .rename = .{
454 .old = try std.fs.path.join(self.alloc, &.{ dir_prefix, old.name }),
455 .new = try std.fs.path.join(self.alloc, &.{ dir_prefix, new }),
456 },
457 });
458
459 self.directories.cleanup();
460 self.directories.populate_entries("") catch |err| {
461 switch (err) {
462 error.AccessDenied => try self.notification.write_err(.PermissionDenied),
463 else => try self.notification.write_err(.UnknownError),
464 }
465 };
466 }
467 self.text_input.clearAndFree();
468 },
469 .change_dir => {
470 const path = self.inputToSlice();
471 if (self.directories.dir.openDir(path, .{ .iterate = true })) |dir| {
472 self.directories.dir = dir;
473
474 self.directories.cleanup();
475 self.directories.populate_entries("") catch |err| {
476 switch (err) {
477 error.AccessDenied => try self.notification.write_err(.PermissionDenied),
478 else => try self.notification.write_err(.UnknownError),
479 }
480 };
481 self.directories.history.reset();
482 } else |err| {
483 switch (err) {
484 error.AccessDenied => try self.notification.write_err(.PermissionDenied),
485 error.FileNotFound => try self.notification.write_err(.IncorrectPath),
486 error.NotDir => try self.notification.write_err(.IncorrectPath),
487 else => try self.notification.write_err(.UnknownError),
488 }
489 }
490
491 self.text_input.clearAndFree();
492 },
493 else => {},
494 }
495 self.state = .normal;
496 self.directories.entries.selected = selected;
497 },
498 else => {
499 try self.text_input.update(.{ .key_press = key });
500
501 switch (self.state) {
502 .fuzzy => {
503 self.directories.cleanup();
504 const fuzzy = self.inputToSlice();
505 self.directories.populate_entries(fuzzy) catch |err| {
506 switch (err) {
507 error.AccessDenied => try self.notification.write_err(.PermissionDenied),
508 else => try self.notification.write_err(.UnknownError),
509 }
510 };
511 },
512 else => {},
513 }
514 },
515 }
516 },
517 .winsize => |ws| {
518 try self.vx.resize(self.alloc, self.tty.anyWriter(), ws);
519 },
520 }
521
522 return .default;
523}
524
525pub fn draw(self: *App) !void {
526 const win = self.vx.window();
527 win.clear();
528
529 const abs_file_path_bar = try self.draw_abs_file_path(win);
530 var file_info_buf: [1024]u8 = undefined;
531 const file_info_bar = try self.draw_file_info(win, &file_info_buf);
532 try self.draw_current_dir_list(win, abs_file_path_bar, file_info_bar);
533
534 if (config.preview_file == true) {
535 var file_name_buf: [std.fs.MAX_NAME_BYTES + 2]u8 = undefined;
536 const file_name_bar = try self.draw_file_name(win, &file_name_buf);
537 try self.draw_preview(win, file_name_bar);
538 }
539
540 try self.draw_info(win);
541
542 try self.vx.render(self.tty.anyWriter());
543}
544
545fn draw_file_name(self: *App, win: vaxis.Window, buf: []u8) !vaxis.Window {
546 const file_name_bar = win.child(.{
547 .x_off = win.width / 2,
548 .y_off = 0,
549 .width = .{ .limit = win.width },
550 .height = .{ .limit = top_div },
551 });
552
553 if (self.directories.get_selected()) |entry| {
554 const file_name = try std.fmt.bufPrint(buf, "[{s}]", .{entry.name});
555 _ = try file_name_bar.print(&.{vaxis.Segment{
556 .text = file_name,
557 .style = config.styles.file_name,
558 }}, .{});
559 } else |_| {}
560
561 return file_name_bar;
562}
563
564fn draw_preview(self: *App, win: vaxis.Window, file_name_win: vaxis.Window) !void {
565 const preview_win = win.child(.{
566 .x_off = win.width / 2,
567 .y_off = top_div + 1,
568 .width = .{ .limit = win.width / 2 },
569 .height = .{ .limit = win.height - (file_name_win.height + top_div + bottom_div) },
570 });
571
572 // Populate preview bar
573 if (self.directories.entries.len() > 0 and config.preview_file == true) {
574 const entry = try self.directories.get_selected();
575
576 @memcpy(&self.last_item_path_buf, &self.current_item_path_buf);
577 self.last_item_path = self.last_item_path_buf[0..self.current_item_path.len];
578 self.current_item_path = try std.fmt.bufPrint(&self.current_item_path_buf, "{s}/{s}", .{ try self.directories.full_path("."), entry.name });
579
580 switch (entry.kind) {
581 .directory => {
582 self.directories.cleanup_sub();
583 if (self.directories.populate_sub_entries(entry.name)) {
584 try self.directories.write_sub_entries(preview_win, config.styles.list_item);
585 } else |err| {
586 switch (err) {
587 error.AccessDenied => try self.notification.write_err(.PermissionDenied),
588 else => try self.notification.write_err(.UnknownError),
589 }
590 }
591 },
592 .file => file: {
593 var file = self.directories.dir.openFile(entry.name, .{ .mode = .read_only }) catch |err| {
594 switch (err) {
595 error.AccessDenied => try self.notification.write_err(.PermissionDenied),
596 else => try self.notification.write_err(.UnknownError),
597 }
598
599 _ = try preview_win.print(&.{
600 .{
601 .text = "No preview available.",
602 },
603 }, .{});
604
605 break :file;
606 };
607 defer file.close();
608 const bytes = try file.readAll(&self.directories.file_contents);
609
610 // Handle image.
611 if (config.show_images == true) unsupported_terminal: {
612 const supported: [1][]const u8 = .{".png"};
613
614 for (supported) |ext| {
615 if (std.mem.eql(u8, std.fs.path.extension(entry.name), ext)) {
616 if (!std.mem.eql(u8, self.last_item_path, self.current_item_path)) {
617 if (self.vx.loadImage(self.alloc, self.tty.anyWriter(), .{ .path = self.current_item_path })) |img| {
618 self.image = img;
619 } else |_| {
620 self.image = null;
621 break :unsupported_terminal;
622 }
623 }
624
625 if (self.image) |img| {
626 try img.draw(preview_win, .{ .scale = .fit });
627 }
628
629 break :file;
630 } else {
631 // Free any image we might have already.
632 if (self.image) |img| {
633 self.vx.freeImage(self.tty.anyWriter(), img.id);
634 }
635 }
636 }
637 }
638
639 // Handle pdf.
640 if (std.mem.eql(u8, std.fs.path.extension(entry.name), ".pdf")) {
641 var child = std.process.Child.init(&.{ "pdftotext", self.current_item_path, "-" }, self.alloc);
642 child.stdout_behavior = .Pipe;
643 child.stderr_behavior = .Pipe;
644 try child.spawn();
645
646 var stdout = std.ArrayList(u8).init(self.alloc);
647 defer stdout.deinit();
648 var stderr = std.ArrayList(u8).init(self.alloc);
649 defer stderr.deinit();
650 try child.collectOutput(&stdout, &stderr, 4096);
651
652 const term = try child.wait();
653 if (term.Exited != 0) {
654 _ = try preview_win.print(&.{
655 .{
656 .text = "No preview available. Install pdftotext to get PDF previews.",
657 },
658 }, .{});
659 break :file;
660 }
661
662 if (self.directories.pdf_contents) |pdf_contents| {
663 self.directories.alloc.free(pdf_contents);
664 }
665 self.directories.pdf_contents = try stdout.toOwnedSlice();
666 _ = try preview_win.print(&.{
667 .{
668 .text = self.directories.pdf_contents.?,
669 },
670 }, .{});
671 break :file;
672 }
673
674 // Handle utf-8.
675 if (std.unicode.utf8ValidateSlice(self.directories.file_contents[0..bytes])) {
676 _ = try preview_win.print(&.{
677 .{
678 .text = self.directories.file_contents[0..bytes],
679 },
680 }, .{});
681 break :file;
682 }
683
684 // Fallback to no preview.
685 _ = try preview_win.print(&.{
686 .{
687 .text = "No preview available.",
688 },
689 }, .{});
690 },
691 else => {
692 _ = try preview_win.print(&.{vaxis.Segment{ .text = self.current_item_path }}, .{});
693 },
694 }
695 }
696}
697
698fn draw_file_info(self: *App, win: vaxis.Window, file_info_buf: []u8) !vaxis.Window {
699 const file_info = try std.fmt.bufPrint(file_info_buf, "{d}/{d} {s} {s}", .{
700 self.directories.entries.selected + 1,
701 self.directories.entries.len(),
702 std.fs.path.extension(if (self.directories.get_selected()) |entry| entry.name else |_| ""),
703 std.fmt.fmtIntSizeDec((try self.directories.dir.metadata()).size()),
704 });
705
706 const file_info_win = win.child(.{
707 .x_off = 0,
708 .y_off = win.height - bottom_div,
709 .width = if (config.preview_file) .{ .limit = win.width / 2 } else .{ .limit = win.width },
710 .height = .{ .limit = bottom_div },
711 });
712 file_info_win.fill(vaxis.Cell{ .style = config.styles.file_information });
713 _ = try file_info_win.print(&.{vaxis.Segment{ .text = file_info, .style = config.styles.file_information }}, .{});
714
715 return file_info_win;
716}
717
718fn draw_current_dir_list(self: *App, win: vaxis.Window, abs_file_path: vaxis.Window, file_information: vaxis.Window) !void {
719 const current_dir_list_win = win.child(.{
720 .x_off = 0,
721 .y_off = top_div + 1,
722 .width = if (config.preview_file) .{ .limit = win.width / 2 } else .{ .limit = win.width },
723 .height = .{ .limit = win.height - (abs_file_path.height + file_information.height + top_div + bottom_div) },
724 });
725 try self.directories.write_entries(current_dir_list_win, config.styles.selected_list_item, config.styles.list_item);
726
727 self.last_known_height = current_dir_list_win.height;
728}
729
730fn draw_abs_file_path(self: *App, win: vaxis.Window) !vaxis.Window {
731 const abs_file_path_bar = win.child(.{
732 .x_off = 0,
733 .y_off = 0,
734 .width = .{ .limit = win.width },
735 .height = .{ .limit = top_div },
736 });
737 _ = try abs_file_path_bar.print(&.{vaxis.Segment{ .text = try self.directories.full_path(".") }}, .{});
738
739 return abs_file_path_bar;
740}
741
742fn draw_info(self: *App, win: vaxis.Window) !void {
743 const info_win = win.child(.{
744 .x_off = 0,
745 .y_off = top_div,
746 .width = .{ .limit = win.width },
747 .height = .{ .limit = info_div },
748 });
749
750 // Display info box.
751 if (self.notification.len > 0) {
752 if (self.text_input.grapheme_count > 0) {
753 self.text_input.clearAndFree();
754 }
755
756 _ = try info_win.print(&.{
757 .{
758 .text = self.notification.slice(),
759 .style = switch (self.notification.style) {
760 .info => config.styles.info_bar,
761 .err => config.styles.error_bar,
762 },
763 },
764 }, .{});
765 }
766
767 // Display user input box.
768 switch (self.state) {
769 .fuzzy, .new_file, .new_dir, .rename, .change_dir => {
770 self.notification.reset();
771 self.text_input.draw(info_win);
772 },
773 .normal => {
774 if (self.text_input.grapheme_count > 0) {
775 self.text_input.draw(info_win);
776 }
777
778 win.hideCursor();
779 },
780 }
781}