// License: MPL-2.0 // (c) 2022 Julian Hurst use libtui; use libtui::widget; use memio; use strconv; use strings; use wcwidth; use set; use io; use fmt; use os; use encoding::utf8; let linestofree: set::set = set::set {...}; export type editorwidget = struct { widget: widget::widget, lines: []str, cursor: (size, size), marked: set::set, frame: frame, sz: ttysize, mode: mode, }; export type frame = struct { start: u16, // largest value is nb of lines end: u16, }; export type mode = enum { NORMAL, INSERT, }; export type ttysize = struct { rows: u16, cols: u16, }; // An input listener on an editor 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 an editorwidget, use [[addlistener]]. export type listener = *fn(l: *editorwidget, r: (rune | libtui::specialkey)) bool; // Create a new editor with the given lines. export fn neweditor(ui: libtui::ttyui, lines: str...) editorwidget = { let sz = libtui::getwinsize(ui)!; let rows = sz.rows - 2; //let rows: (u16 | size) = if (sz.rows - 2 < len(items)) { //yield sz.rows - 2; //} else { //yield len(items); //}; let w = editorwidget { widget = widget::widget { print = &print, finish = &finish, ui = ui, ... }, lines = lines, marked = set::set {...}, cursor = (0z, 0z), frame = frame { start = 0u16, end = rows: u16, }, sz = ttysize { rows = rows: u16, cols = sz.columns, }, mode = mode::INSERT, }; return w; }; // Notify (call) the editorwidget's editoreners with the editorwidget and r as a // parameter. Returns true if a editorener returned true, false otherwise. //export fn notify(l: *editorwidget, r: rune) bool = { //for (let i = 0z; i < len(l.widget.listeners); i += 1) { //if (l.widget.listeners[i](l, r)) { //return true; //}; //}; //return false; //}; //const SELECTED: str = "\x1B[104;1m\x1B[30m"; const SELECTED: str = "\x1B[7m"; const MARKED: str = "\x1B[46;1m\x1B[30m"; const RESET: str = "\x1B[0m"; // Print the editor's lines while truncating the lines to not be wider than the // editor.sz.cols. export fn print(editor: *widget::widget) (void | widget::error) = { const editor = editor: *editorwidget; //let sz = libtui::getwinsize(editor.ui)?; //let rows: (u16 | size) = if (sz.rows - 2 < len(editor.lines)) { //yield sz.rows - 2; //} else { //yield len(editor.lines); //}; //fmt::fprintln(os::stderr, rows)!; //editor.frame.end = editor.frame.start + editor.sz.rows; let st = memio::dynamic(); memio::concat(&st, "\r")?; let end = if (editor.frame.end < len(editor.lines)) { yield editor.frame.end; } else { yield len(editor.lines); }; const maxlinenosz = len(strconv::ztos(len(editor.lines))); for (let i = editor.frame.start; i < end: u16; i += 1) { let lineno = strconv::ztos(i+1); lineno = strings::padstart(lineno, ' ', maxlinenosz); const line = strings::concat(lineno, "| ", editor.lines[i])!; const truncitem = wcwidth::truncate(line, editor.sz.cols); defer free(truncitem); if (editor.cursor.0 == i) { //memio::concat(&st, SELECTED, truncitem, RESET)?; const linepos = editor.cursor.1 + maxlinenosz + 2; memio::concat(&st,strings::sub(truncitem, 0, linepos))?; memio::concat(&st, SELECTED, strings::sub(truncitem, linepos, linepos+1), RESET)?; memio::concat(&st, strings::sub(truncitem, linepos+1, strings::end))?; //libtui::print(editor.ui, strings::concat("\x1B[31;1m> ", editor.lines[i], "\x1B[0m")); } else if (set::contains(editor.marked, i) is size){ memio::concat(&st, MARKED, truncitem, RESET)?; //libtui::print(editor.ui, editor.lines[i]); } else { memio::concat(&st, truncitem)?; }; memio::concat(&st, "\r\n")?; }; // unsupported? //io::copy(editor.ui.f, &st)?; let s = memio::string(&st); libtui::print(editor.widget.ui, s); io::close(&st)?; }; // Free the editor's lines, marked lines and call the common widget finish // function [[widget::finishcommon]]. export fn finish(editor: *widget::widget) void = { const editor = editor: *editorwidget; for (let i = 0z; i < len(linestofree.items); i += 1) { free(editor.lines[linestofree.items[i]]); }; free(editor.lines); //strings::freeall(editor.lines); set::finish(&editor.marked); set::finish(&linestofree); widget::finishcommon(editor); }; // Reset the editor's frame based on the cursor. Returns whether the frame was // updated. export fn reframe(l: *editorwidget) bool = { let reframed: bool = false; if (l.cursor.0 < l.frame.start) { l.frame.start = l.cursor.0: u16; l.frame.end = l.frame.start + l.sz.rows; //l.frame.end -= l.frame.start - l.cursor: u16; reframed = true; }; if (l.cursor.0 >= l.frame.end) { l.frame.start += l.cursor.0: u16 - l.frame.end + 1; l.frame.end = l.cursor.0: u16 + 1; reframed = true; }; return reframed; }; // Move the editor's cursor up one line. Returns the new cursor. export fn up(l: *editorwidget) (size, size) = { if (l.cursor.0 > 0) { l.cursor.0 -= 1; reframe(l); resetcursorlinepos(l); }; return l.cursor; }; // Move the editor's cursor down one line. Returns the new cursor. export fn down(l: *editorwidget) (size, size) = { if (l.cursor.0 < len(l.lines) - 1) { l.cursor.0 += 1; reframe(l); resetcursorlinepos(l); }; return l.cursor; }; fn resetcursorlinepos(l: *editorwidget) (size, size) = { const line = l.lines[l.cursor.0]; if (l.cursor.1 >= len(line)) { l.cursor.1 = if (len(line) > 0) { yield len(line) - 1; } else { yield firstcharindex(line); }; } else { const firstcharidx = firstcharindex(line); if (firstcharidx > l.cursor.1) { l.cursor.1 = firstcharidx; }; }; return l.cursor; }; // Move the editor's cursor to the right one character. Returns the new cursor. export fn right(l: *editorwidget) (size, size) = { if (l.cursor.1 < len(l.lines[l.cursor.0]) - 1) { l.cursor.1 += 1; }; return l.cursor; }; // Move the editor's cursor to the left one character. Returns the new cursor. export fn left(l: *editorwidget) (size, size) = { if (l.cursor.1 > 0) { l.cursor.1 -= 1; }; return l.cursor; }; export fn insertmode(l: *editorwidget, lt: widget::listener) void = { l.mode = mode::INSERT; widget::clearlisteners(l); widget::addlistener(l, lt); }; export fn normalmode(l: *editorwidget, lt: widget::listener) void = { l.mode = mode::NORMAL; widget::clearlisteners(l); widget::addlistener(l, lt); }; export fn insertrune(l: *editorwidget, r: rune) (void | io::error) = { //match (sanitizerune(r)) { //case let k: libtui::keycode => //specialkey(l, k)?; //case let r: rune => let line = l.lines[l.cursor.0]; let st = memio::dynamic(); memio::concat(&st, strings::sub(line, 0, l.cursor.1))?; memio::appendrune(&st, r)?; memio::concat(&st, strings::sub(line, l.cursor.1, strings::end))?; l.lines[l.cursor.0] = strings::dup(memio::string(&st))!; right(l); set::add(&linestofree, l.cursor.0); io::close(&st)?; //}; }; fn specialkey(l: *editorwidget, keycode: libtui::keycode) (void | io::error) = { switch (keycode) { case libtui::keycode::BACKSPACE => if (l.cursor.1 > 0) { let line = l.lines[l.cursor.0]; let st = memio::dynamic(); memio::concat(&st, strings::sub(line, 0, l.cursor.1-1))?; if (len(line) > l.cursor.1) { memio::concat(&st, strings::sub(line, l.cursor.1, strings::end))?; }; l.lines[l.cursor.0] = strings::dup(memio::string(&st))!; left(l); set::add(&linestofree, l.cursor.0); io::close(&st)?; }; case libtui::keycode::RIGHT => right(l); //fmt::fprintln(os::stderr, "poopy")!; }; }; // Doesn't work, RIGHT ends up being 3 separate runes not one with 3 bytes... //fn sanitizerune(r: rune) (rune | libtui::keycode) = { //if (libtui::iskey(r, libtui::BACKSPACE)) { //return libtui::keycode::BACKSPACE; //}; //if (libtui::iskey(r, libtui::RIGHT)) { //return libtui::keycode::RIGHT; //}; //return r; //}; fn firstcharindex(s: str) size = { let it = strings::iter(s); let i = 0z; for (true) { match (strings::next(&it)) { case let r: rune => if (r != ' ' && r != '\t') { return i; }; case void => return 0z; }; i += 1; }; return 0z; };