// License: MPL-2.0 // (c) 2022 Julian Hurst use libtui; use libtui::widget; //use fmt; use os; use strings; use io; use memio; use unix::tty; use regex; use fnmatch; use wcwidth; use set; use set::sset; export type listwidget = struct { widget: widget::widget, items: []str, marked: sset::sset, cursor: size, //listeners: []listener, frame: frame, sz: ttysize, }; export type frame = struct { start: u16, // largest value is nb of items end: u16, sz: ttysize, }; export type ttysize = struct { rows: u16, cols: u16, }; // An input listener on a list widget. The returning value is intended to be // used as a signal that will be returned by [[notify]] in order to trigger // certain more global ui events (terminate the program, change widget focus, // etc.). To register a listener with a listwidget, use [[addlistener]]. export type listener = *fn(l: *listwidget, r: (rune | libtui::specialkey)) bool; // Create a new list with the given items. The given items will be duplicated // using [[strings::dupall]]], so the original items can be freed. export fn newlist(ui: libtui::ttyui, items: str...) listwidget = { let sz = libtui::getwinsize(ui)!; return newlistsz(ui, sz, items...); }; // Create a new list with the given items and size. The given items will be // duplicated using [[strings::dupall]]], so the original items can be freed. export fn newlistsz(ui: libtui::ttyui, sz: tty::ttysize, items: str...) listwidget = { //let sz = libtui::getwinsize(ui)!; //let rows = sz.rows; let rows: (u16 | size) = if (sz.rows < len(items)) { yield sz.rows; } else { yield len(items); }; let w = listwidget { widget = widget::widget { print = &print, finish = &finish, setsize = &setsize, ui = ui, ... }, items = strings::dupall(items), marked = sset::new(), cursor = 0z, //listeners = [], frame = frame { start = 0u16, end = rows: u16, sz = ttysize { rows = rows: u16, cols = sz.columns, }, }, sz = ttysize { rows = sz.rows, cols = sz.columns, }, }; return w; }; // Free the list's items, marked items and call the common widget finish // function [[widget::finishcommon]]. export fn finish(list: *widget::widget) void = { const list = list: *listwidget; strings::freeall(list.items); set::finish(&list.marked); widget::finishcommon(list); }; // Set the list's items. The given items are duplicated via [[strings::dupall]]. // If the length of the given items is smaller than the list's current items, // the cursor will be set to 0z and [[reframe]] will be called to reset the // frame. export fn setitems(list: *listwidget, items: str...) void = { const doreset = len(items) < len(list.items); list.items = strings::dupall(items); if (doreset) { reset(list); }; }; export fn reset(list: *listwidget) void = { list.cursor = 0z; list.frame.start = 0u16; list.frame.end = list.sz.rows; }; // Add a listener to the given list. //export fn addlistener(list: *listwidget, l: listener) void = { //append(list.listeners, l); //}; const SELECTED: str = "\x1B[104;1m\x1B[30m"; const MARKED: str = "\x1B[46;1m\x1B[30m"; const RESET: str = "\x1B[0m"; // Print the list's items while truncating the items to not be wider than the // list.sz.cols. export fn print(list: *widget::widget) (void | widget::error) = { const list = list: *listwidget; //let sz = libtui::getwinsize(list.ui)?; //let rows: (u16 | size) = if (sz.rows - 2 < len(list.items)) { //yield sz.rows - 2; //} else { //yield len(list.items); //}; //fmt::fprintln(os::stderr, rows)!; //list.frame.end = list.frame.start + list.sz.rows; let st = memio::dynamic(); memio::concat(&st, "\r")?; let end = list.frame.start + list.sz.rows; for (let i = list.frame.start; i < end; i += 1) { if (i < len(list.items)) { const item = list.items[i]; const truncitem = wcwidth::truncate(item, list.sz.cols); if (list.cursor == i) { memio::concat(&st, SELECTED, truncitem, RESET)?; //libtui::print(list.ui, strings::concat("\x1B[31;1m> ", list.items[i], "\x1B[0m")); } else if (set::contains(&list.marked, i)) { memio::concat(&st, MARKED, truncitem, RESET)?; //libtui::print(list.ui, list.items[i]); } else { memio::concat(&st, truncitem)?; }; }; memio::concat(&st, "\r\n")?; }; // unsupported? //io::copy(list.ui.f, &st)?; let s = memio::string(&st)?; libtui::print(&list.widget.ui, s); io::close(&st)?; }; // Notify (call) the listwidget's listeners with the listwidget and r as a // parameter. Returns true if a listener returned true, false otherwise. export fn notify(l: *listwidget, r: (rune | libtui::specialkey)) bool = { for (let i = 0z; i < len(l.widget.listeners); i += 1) { if (l.widget.listeners[i](l, r)) { return true; }; }; return false; }; // Reset the list's frame based on the cursor. Returns whether the frame was // updated. export fn reframe(l: *listwidget) bool = { let reframed: bool = false; if (l.cursor < l.frame.start) { l.frame.start = l.cursor: u16; l.frame.end = l.frame.start + l.frame.sz.rows; //l.frame.end -= l.frame.start - l.cursor: u16; reframed = true; }; if (l.cursor >= l.frame.end) { l.frame.start += l.cursor: u16 - l.frame.end + 1; l.frame.end = l.cursor: u16 + 1; reframed = true; }; return reframed; }; // Use signalfd and [[unix::signal::SIGWINCH]] and call this function in the // handler to support resizing the list. export fn resize(l: *listwidget, oldsz: ttysize) bool = { if (l.frame.end - l.frame.start != l.frame.sz.rows) { if (l.cursor < (l.frame.end / 2)) { l.frame.end = if (l.frame.start + l.frame.sz.rows > len(l.items)) { l.frame.start = len(l.items): u16 - l.frame.sz.rows; yield len(l.items): u16; } else { yield l.frame.start + l.frame.sz.rows; }; for (l.cursor > l.frame.end) { l.frame.start += 1; l.frame.end += 1; }; } else { l.frame.start = if (l.frame.end: int - l.frame.sz.rows: int < 0) { l.frame.end = 0 + l.frame.sz.rows; yield 0; } else { yield l.frame.end - l.frame.sz.rows; }; for (l.cursor < l.frame.start) { l.frame.start -= 1; l.frame.end -= 1; }; }; return true; }; return false; }; export fn setsize(l: *widget::widget, sz: tty::ttysize) void = { const l = l: *listwidget; let rows: (u16 | size) = if (sz.rows < len(l.items)) { yield sz.rows; } else { yield len(l.items); }; l.sz.rows = sz.rows; l.sz.cols = sz.columns; l.frame.sz.rows = rows: u16; l.frame.sz.cols = sz.columns; }; fn cursorinframe(l: *listwidget) bool = { return l.cursor >= l.frame.start && l.cursor < l.frame.end; }; // Move the list's cursor down one item. Returns the new cursor. export fn down(l: *listwidget) size = { if (l.cursor < len(l.items) - 1) { l.cursor += 1; reframe(l); }; return l.cursor; }; // Move the list's cursor up one item. Returns the new cursor. export fn up(l: *listwidget) size = { if (l.cursor > 0) { l.cursor -= 1; reframe(l); }; return l.cursor; }; // Move the list's cursor up one page. Returns the new cursor. export fn pageup(l: *listwidget) size = { if (l.cursor: int - l.frame.sz.rows: int >= 0) { l.cursor -= l.frame.sz.rows; } else { l.cursor = 0z; }; reframe(l); return l.cursor; }; // Move the list's cursor down one page. Returns the new cursor. export fn pagedown(l: *listwidget) size = { if (l.cursor + l.frame.sz.rows < len(l.items)) { l.cursor += l.frame.sz.rows; } else { l.cursor = len(l.items) - 1; }; reframe(l); return l.cursor; }; // Move the list's cursor to the top (first item). Returns the new cursor. export fn top(l: *listwidget) size = { l.cursor = 0; l.frame.start = 0; l.frame.end = l.frame.start + l.frame.sz.rows; return l.cursor; }; // Move the list's cursor to the bottom (last item). Returns the new cursor. export fn bottom(l: *listwidget) size = { l.cursor = len(l.items) - 1; l.frame.end = len(l.items): u16; l.frame.start = l.frame.end - l.frame.sz.rows; return l.cursor; }; // Forward search through the list's items for an item containing s. Returns the // new cursor. export fn search(l: *listwidget, s: str) size = { for (let i = l.cursor + 1; i < len(l.items); i += 1) { if (strings::contains(l.items[i], s)) { l.cursor = i; reframe(l); return l.cursor; }; }; return l.cursor; }; // Backwards search through the list's items for an item containing s. Returns // the new cursor. export fn rsearch(l: *listwidget, s: str) size = { // size wraps to max value for size when < 0 for (let i = l.cursor: int - 1; i >= 0; i -= 1) { if (strings::contains(l.items[i], s)) { l.cursor = i: size; reframe(l); return l.cursor; }; }; return l.cursor; }; // Toggles marking the currently selected item. export fn tmark(l: *listwidget) void = { if (!set::add(&l.marked, l.cursor)) { set::del(&l.marked, l.cursor); }; }; // Clears all marked items. export fn clearmarked(l: *listwidget) void = { set::clear(&l.marked); }; // Marks items that contain s (case sensitive). export fn containsmark(l: *listwidget, s: str) void = { for (let i = 0z; i < len(l.items); i += 1) { if (strings::contains(l.items[i], s)) { set::add(&l.marked, i); }; }; }; // Marks items based on fnmatch (globbing syntax). export fn fnmatchmark(l: *listwidget, s: str) void = { for (let i = 0z; i < len(l.items); i += 1) { if (fnmatch::fnmatch(s, l.items[i])) { set::add(&l.marked, i); }; }; }; // Marks items according to a regular expression (POSIX ERE). export fn regexmark(l: *listwidget, re: *regex::regex) void = { for (let i = 0z; i < len(l.items); i += 1) { if (regex::test(re, l.items[i])) { set::add(&l.marked, i); }; }; }; // Returns the selected item or marked items if there are any. The returned // value is borrowed from the list's items. If []str is returned, the slice must // be freed. export fn selected(l: listwidget) (str | []str) = { if (set::length(&l.marked) > 0) { let result: []str = []; let its = set::items(&l.marked); for (let i = 0z; i < len(its); i += 1) { append(result, l.items[its[i]]); }; return result; } else { return l.items[l.cursor]; }; };