use Term (Color) use StrBuilder (StrBuilder) use Iter (map-maybe) use File (Dir, DirType(File, Directory)) use Pane (ScreenDims) # bad import, we only depend on the screen dims data structure. use Config (Config) use TextBox (TextBox) # quick and dirty impl (without the Path type and file info stuff) fn get-files-in-dir(dirname StrView, al Allocator) -> Slice StrView files =& List.mk(al) fn recurse-into-dir(dirname StrView) entries = File.list-dir(dirname, al) case entries None return Just(entries) for entry in entries case entry.type Just(File) if dirname == '.' files List.add(entry.name StrView.clone-0(al)) else files List.add('\(dirname)/\(entry.name)' StrView.from-str-0(al)) Just(Directory) if not entry.name Str.begins-with('.') full-name = if dirname == '.': entry.name StrView.clone-0(al) else '\(dirname)/\(entry.name)' StrView.from-str-0(al) recurse-into-dir(full-name) StrView.free(full-name, al) entries File.free-dir-entries(al) recurse-into-dir(dirname) return files& List.to-slice() FilePicker al total-files Slice StrView rendered-files Slice (StrView, Size) # which files to show after filtering them through a query. is 'filtered' a better name? sel Size query TextBox al # IDEA: maybe generalize textboxes (I should, since I want to control more stuff with CTRL-... which would be tedious) sel-size Size # smol thing. we trim the original string when searching and this will point to the actual query size. last-screen-height Maybe I32 # last height which we use for scrolling. if None, set on render or resize. kinda cringe but whatever. screen-sel Size # this points to the expected cursor position in the menu, kinda like vi expected-page-size = 15 fn at-dir(dir Str, al Allocator) -> FilePicker Allocator files = get-files-in-dir(dir, al) # initialize rendered files (for empty query) and populate with default values default-filtered = al Alloc.allocate(files.count) for (ff, filename) in default-filtered Slice.addresses() zip(files) ff <&= (filename, 0) return { total-files: files , rendered-files: default-filtered , sel: 0 , query: TextBox.mk(al) , sel-size: 0 , last-screen-height: None , screen-sel: 0 } # very shitty way to check if a path is a directory. fn is-dir(filename CStr) case filename File.open-dir-os() Just(dir) dir File.close-dir() return True None return False fn free(p FilePicker Allocator, al Allocator) for name in p.total-files StrView.free(name, al) al Alloc.free(p.total-files) al Alloc.free(p.rendered-files) TextBox.free(&p.query) fn query-update(p Ptr (FilePicker al)) buflist =& BufList.mk(p&.rendered-files { count: p&.total-files.count }) query = p&.query TextBox.as-str() StrView.trim() buflist BufList.add-all(p&.total-files map-maybe(fn filename: filename StrView.find(query) Misc.fmap-maybe(fn idx: (filename, idx)))) p <&.rendered-files= buflist&.slice p <&.sel = 0 # also don't forget to reset the sel cursor. (to the first position, like helix) p <&.screen-sel= 0 p <&.sel-size= query StrView.num-bytes() fn page-size (p FilePicker al) # TODO: copypasta :(((( case p.last-screen-height Just(height) searchbar-y-size = 2 num-files-to-display = Math.min(height - 2 - searchbar-y-size, expected-page-size) return num-files-to-display None return expected-page-size fn set-sel (p Ptr (FilePicker al), nusel Size) p <&.sel= nusel p <&.screen-sel= if nusel == 0: 0 else nusel Math.mod(p& page-size() Mem.i32-size()) # (also silently handles 0-size) # primitives. maybe move them to std? fn fill-rect(screen Ptr (Tui.Screen Allocator), from-x I32, from-y I32, to-x I32, to-y I32) for y in from-y to (to-y) for x in from-x to (to-x) screen Tui.put-char(' ', x, y) fn draw-box(screen Ptr (Tui.Screen Allocator), from-x I32, from-y I32, to-x I32, to-y I32) vbar = '┃' hbar = '━' tl = '┏' lj = '┣' tr = '┓' rj = '┫' bl = '┗' br = '┛' screen fill-rect(from-x + 1, from-y + 1, to-x - 1, to-y - 1) hbar-y = from-y + 2 for x in (from-x + 1) to (to-x - 1) screen Tui.put-char(hbar, x, from-y) screen Tui.put-char(hbar, x, to-y) screen Tui.put-char(hbar, x, hbar-y) for y in (from-y + 1) to (to-y - 1) screen Tui.put-char(vbar, from-x, y) screen Tui.put-char(vbar, to-x, y) screen Tui.put-char(lj, from-x, hbar-y) screen Tui.put-char(rj, to-x, hbar-y) # corners screen Tui.put-char(tl, from-x, from-y) screen Tui.put-char(tr, to-x, from-y) screen Tui.put-char(bl, from-x, to-y) screen Tui.put-char(br, to-x, to-y) fn render(screen Ptr (Tui.Screen al), p Ptr (FilePicker al), config Ptr Config) <= Allocator al # parameters default-width = 30 # ========== width = screen&.tui&.width Mem.u32-i32() - 1 height = screen&.tui&.height Mem.u32-i32() - 1 if p&.last-screen-height is-none() p <&.last-screen-height= Just(height) border-width = 1 max-possible-inner-width = width - 2 * border-width searchbar-y-size = 2 page-size = p& page-size() page = p&.sel / Mem.i32-size(page-size) # +1 cuz... trust the science start-i = page * Mem.i32-size(page-size) displayed-files = p&.rendered-files Slice.subslice(start-i, start-i + page-size Mem.i32-size()) # go to correct page max-shown-filename-width = displayed-files map(fst) map(Term.rendered-str-wcwidth) maximum() or-else(default-width) Math.max(default-width) num-files-to-display = displayed-files.count sel-cursor = ' > ' sel-cursor-off = sel-cursor Term.rendered-str-wcwidth() max-width = Math.min(max-possible-inner-width, max-shown-filename-width + sel-cursor-off) box-width = 2 + max-width box-height = 3 + num-files-to-display Mem.size-i32() default-box-height = 3 + page-size box-x = (width - box-width) / 2 box-y = (height - default-box-height) / 2 box-to-x = box-x + box-width box-to-y = box-y + box-height colors = config&.theme&.default default-text-color = config&.theme&.default screen Theme.set-screen-colors(colors) screen draw-box(box-x, box-y, box-to-x, box-to-y) screen Tui.draw-str-right-aligned('\(p&.rendered-files.count)/\(p&.total-files.count)', box-to-x - 1, box-y + 1) # draw query and cursor screen Tui.draw-str(p&.query TextBox.as-str() Term.trim-to-wcwidth(max-width), box-x + 2, box-y + 1) if p&.query.cursor <= max-shown-filename-width Mem.i32-size() screen Tui.get-cell-ptr(box-x + 2 + p&.query.cursor Mem.size-i32(), box-y + 1) if-just(fn(cell)) cell <&.fg= config&.theme&.cursor.fg cell <&.bg= config&.theme&.cursor.bg if num-files-to-display <= 0 # in case we don't have space, exit. 0 can mess up some of the calculations. return sel-size = p&.sel-size match-colors = config&.theme&.picker-match sel-colors = config&.theme&.picker-sel for (((filename, found-idx), file-i), y) in displayed-files zip(from(Mem.size-i32(start-i))) zip((box-y + 1 + searchbar-y-size) to (height - 1)) text-colors = default-text-color if file-i == p&.sel Mem.size-i32() text-colors <= { fg: sel-colors.fg, bg: sel-colors.bg or-else(text-colors.bg) } screen Theme.set-screen-colors(text-colors) screen Tui.draw-str(sel-cursor, box-x + border-width, y) filename = filename Term.trim-strview-to-wcwidth(max-width - sel-cursor-off) # display textothertest before-sel = filename StrView.byte-substr(0, found-idx) in-sel = filename StrView.byte-substr(found-idx, found-idx + sel-size) after-sel = filename StrView.byte-substr-from(found-idx + sel-size) text-from-x = box-x + border-width + sel-cursor-off screen Theme.set-screen-colors(text-colors) screen Tui.draw-str(before-sel, text-from-x, y) screen Theme.set-screen-colors({ fg: match-colors.fg, bg: match-colors.bg or-else(text-colors.bg) }) screen Tui.draw-str(in-sel, text-from-x + Mem.size-i32(found-idx), y) screen Theme.set-screen-colors(text-colors) screen Tui.draw-str(after-sel, text-from-x + Mem.size-i32(found-idx + sel-size), y) Choice PickerExit PickerFile StrView fn handle-event (p Ptr (FilePicker al), ev Tui.InputEvent) -> Maybe Choice <= Allocator al key = @undefined case ev Tui.Key(k) key <= k Tui.Resize(_, nu-height) p <&.last-screen-height= Just(nu-height Mem.u32-i32()) return None case key Tui.Escape return Just(PickerExit) Tui.Enter if p&.rendered-files.count == 0 return Just(PickerExit) assert(p&.sel < p&.rendered-files.count, '(FilePicker.handle-key) index must point to a valid list element') return Just(PickerFile(p&.rendered-files[p&.sel] fst())) Tui.Up filecount = p&.rendered-files.count if filecount == 0 return None # TODO: funny stuff due to unsigned. which type should I use for such stuff so that it wraps gracefully? if p&.sel == 0 p set-sel(filecount - 1) else p set-sel(p&.sel - 1) Tui.Down filecount = p&.rendered-files.count if filecount == 0 return None p set-sel((p&.sel + 1) Math.mod(filecount)) Tui.Ctrl('d') if p&.rendered-files.count == 0 return None page-size = p& page-size() Mem.i32-size() # check for wrap around or "trim" if p&.sel + page-size >= p&.rendered-files.count next-page-num = (p&.sel + page-size) / page-size next-page-start = next-page-num * page-size pos-on-screen = p&.screen-sel # wrap around if next-page-start >= p&.rendered-files.count p <&.sel= pos-on-screen # trim else next-page-entries = p&.rendered-files.count - next-page-start assert(next-page-entries > 0, 'math should disallow that :)') p <&.sel= next-page-start + pos-on-screen Math.min(next-page-entries - 1) else p <&.sel= p&.sel + page-size Tui.Ctrl('u') if p&.rendered-files.count == 0 return None page-size = p& page-size() Mem.i32-size() if p&.sel < page-size pos-on-screen = p&.screen-sel last-page = (p&.rendered-files.count - 1) / page-size next-page-start = last-page * page-size next-page-size = p&.rendered-files.count - next-page-start p <&.sel= next-page-start + pos-on-screen Math.min(next-page-size - 1) else p <&.sel= (p&.sel - page-size) Math.mod(p&.rendered-files.count) other if TextBox.handle-key(&p&.query, key) p query-update() return None