地圖 (Jido) is a lightweight Unix TUI file explorer designed for speed and simplicity.
at v0.4.0 781 lines 33 kB view raw
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}