From 75727c5cbe8282aada4d3a8122cd3271a97c263d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20=C4=8Cert=C3=ADk?= Date: Wed, 2 Oct 2019 16:48:26 -0600 Subject: [PATCH 1/8] Multiline editing --- examples/menu_window.cpp | 7 +- examples/prompt.cpp | 152 +++++++++++++++++++++++++++++---------- terminal.h | 92 ++++++++++++++++++++---- terminal_base.h | 1 + 4 files changed, 199 insertions(+), 53 deletions(-) diff --git a/examples/menu_window.cpp b/examples/menu_window.cpp index b9e549b0..2f2e4b41 100644 --- a/examples/menu_window.cpp +++ b/examples/menu_window.cpp @@ -30,9 +30,10 @@ std::string render(Term::Window &scr, int rows, int cols, int y = menuy0 + menuheight + 5; scr.print_str(1, y, "Selected item: " + std::to_string(menupos)); scr.print_str(1, y+1, "Menu width: " + std::to_string(menuwidth)); - scr.print_str(1, y+2, "Menu height: " + std::to_string(menuheight)); + scr.print_str(1, y+2, "Menu height: " + std::to_string(menuheight), + 0, true); - return scr.render(); + return scr.render(1, 1); } int main() { @@ -45,7 +46,7 @@ int main() { int h = 10; int w = 10; bool on = true; - Term::Window scr(1, 1, cols, rows); + Term::Window scr(cols, rows); while (on) { std::cout << render(scr, rows, cols, h, w, pos) << std::flush; int key = term.read_key(); diff --git a/examples/prompt.cpp b/examples/prompt.cpp index 57aeac91..80a138da 100644 --- a/examples/prompt.cpp +++ b/examples/prompt.cpp @@ -12,23 +12,71 @@ using Term::cursor_on; // abstract way, irrespective of where or how it is rendered. struct Model { std::string prompt_string; // The string to show as the prompt - std::string input; // The current input string in the prompt + std::vector lines; // The current input string in the prompt as + // a vector of lines, without '\n' at the end. // The current cursor position in the "input" string, starting from (1,1) size_t cursor_col, cursor_row; }; -std::string render(const Model &m, int prompt_row, int term_cols) { - std::string out; - out = cursor_off(); - out += move_cursor(prompt_row, 1) + m.prompt_string + m.input; - size_t last_col = m.prompt_string.size() + m.input.size(); - for (size_t i=0; i < term_cols-last_col; i++) { - out.append(" "); +std::string concat(const std::vector &lines) { + std::string s; + for (auto &line: lines) { + s.append(line + "\n"); } - out.append(move_cursor(prompt_row+m.cursor_row-1, - m.prompt_string.size() + m.cursor_col)); - out.append(cursor_on()); - return out; + return s; +} + +std::vector split(const std::string &s) { + size_t j = 0; + std::vector lines; + lines.push_back(""); + if (s[s.size()-1] != '\n') throw std::runtime_error("\\n is required"); + for (size_t i=0; i hist = history; size_t history_pos = hist.size(); - hist.push_back(m.input); // Push back empty input + hist.push_back(concat(m.lines)); // Push back empty input + Term::Window scr(cols, 1); int key; - std::cout << render(m, row, cols) << std::flush; + render(scr, m, cols); + std::cout << scr.render(1, row) << std::flush; while ((key = term.read_key()) != Key::ENTER) { if ( (key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z') || (!iscntrl(key) && key < 128) ) { - std::string before = m.input.substr(0, m.cursor_col-1); + std::string before = m.lines[m.cursor_row-1].substr(0, + m.cursor_col-1); std::string newchar; newchar.push_back(key); - std::string after = m.input.substr(m.cursor_col-1); - m.input = before + newchar + after; + std::string after = m.lines[m.cursor_row-1].substr(m.cursor_col-1); + m.lines[m.cursor_row-1] = before + newchar + after; m.cursor_col++; } else if (key == CTRL_KEY('d')) { - if (m.input.size() == 0) { - m.input.push_back(CTRL_KEY('d')); - break; + if (m.lines.size() == 1 && m.lines[m.cursor_row-1].size() == 0) { + m.lines[m.cursor_row-1].push_back(CTRL_KEY('d')); + std::cout << "\n" << std::flush; + history.push_back(m.lines[0]); + return m.lines[0]; } } else { switch (key) { case Key::BACKSPACE: if (m.cursor_col > 1) { - std::string before = m.input.substr(0, m.cursor_col-2); - std::string after = m.input.substr(m.cursor_col-1); - m.input = before + after; + std::string before = m.lines[m.cursor_row-1].substr(0, + m.cursor_col-2); + std::string after = m.lines[m.cursor_row-1] + .substr(m.cursor_col-1); + m.lines[m.cursor_row-1] = before + after; m.cursor_col--; } break; @@ -81,7 +137,7 @@ std::string prompt(const Terminal &term, const std::string &prompt_string, } break; case Key::ARROW_RIGHT: - if (m.cursor_col <= m.input.size()) { + if (m.cursor_col <= m.lines[m.cursor_row-1].size()) { m.cursor_col++; } break; @@ -89,31 +145,51 @@ std::string prompt(const Terminal &term, const std::string &prompt_string, m.cursor_col = 1; break; case Key::END: - m.cursor_col = m.input.size()+1; + m.cursor_col = m.lines[m.cursor_row-1].size()+1; break; case Key::ARROW_UP: if (history_pos > 0) { - hist[history_pos] = m.input; + hist[history_pos] = concat(m.lines); history_pos--; - m.input = hist[history_pos]; - m.cursor_col = m.input.size()+1; + m.lines = split(hist[history_pos]); + m.cursor_row = 1; + m.cursor_col = m.lines[0].size()+1; + scr.set_h(m.lines.size()); } break; case Key::ARROW_DOWN: if (history_pos < hist.size()-1) { - hist[history_pos] = m.input; + hist[history_pos] = concat(m.lines); history_pos++; - m.input = hist[history_pos]; - m.cursor_col = m.input.size()+1; + m.lines = split(hist[history_pos]); + m.cursor_row = 1; + m.cursor_col = m.lines[0].size()+1; + scr.set_h(m.lines.size()); } break; + case ALT_KEY('n'): + case Key::ALT_ENTER: + std::string before = m.lines[m.cursor_row-1].substr(0, + m.cursor_col-1); + std::string after = m.lines[m.cursor_row-1] + .substr(m.cursor_col-1); + m.lines[m.cursor_row-1] = before; + m.lines.push_back(after); + m.cursor_col = after.size()+1; + m.cursor_row++; + scr.set_h(scr.get_h()+1); } } - std::cout << render(m, row, cols) << std::flush; + render(scr, m, cols); + std::cout << scr.render(1, row) << std::flush; + // FIXME: this is a hack + if (key == Key::ALT_ENTER || key == ALT_KEY('n')) { + if (row+(int)scr.get_h()-1 > rows) row--; + } } std::cout << "\n" << std::flush; - history.push_back(m.input); - return m.input; + history.push_back(concat(m.lines)); + return concat(m.lines); } int main() { @@ -125,21 +201,21 @@ int main() { std::cout << " * Features:" << std::endl; std::cout << " - Editing (Keys: Left, Right, Home, End, Backspace)" << std::endl; std::cout << " - History (Keys: Up, Down)" << std::endl; + std::cout << " - Multi-line editing (use Alt-Enter or Alt-N to add a new line)" << std::endl; // TODO: - //std::cout << " - Multi-line editing (use Alt-Enter to add a new line)" << std::endl; //std::cout << " - Syntax highlighting" << std::endl; std::vector history; while (true) { - std::string answer = prompt(term, "> ", history); + std::string answer = prompt(term, ">>> ", history); if (answer.size() == 1 && answer[0] == CTRL_KEY('d')) break; std::cout << "Submitted text: " << answer << std::endl; } } catch(const std::runtime_error& re) { std::cerr << "Runtime error: " << re.what() << std::endl; - return 2; + return 1; } catch(...) { std::cerr << "Unknown error." << std::endl; - return 1; + throw; } return 0; } diff --git a/terminal.h b/terminal.h index 29c8b187..aeab3517 100644 --- a/terminal.h +++ b/terminal.h @@ -101,6 +101,7 @@ std::string cursor_on() // If an attempt is made to move the cursor out of the window, the result is // undefined. +// TODO: switch col/row std::string move_cursor(int row, int col) { return "\x1b[" + std::to_string(row) + ";" + std::to_string(col) + "H"; @@ -378,6 +379,7 @@ class Terminal: public BaseTerminal { } } + // TODO: switch rows, cols void get_cursor_position(int& rows, int& cols) const { char buf[32]; @@ -403,6 +405,7 @@ class Terminal: public BaseTerminal { } // This function takes about 23ms, so it should only be used as a fallback + // TODO: switch rows / cols void get_term_size_slow(int& rows, int& cols) const { struct CursorOff { @@ -553,6 +556,13 @@ std::string utf32_to_utf8(const std::u32string &s) } +// TODO: +// * Rename Window to Screen, as it really is a model of the terminal screen, +// and should only be used once per application. If an application has windows, +// those should be built on top, and they should render to Screen. +// * Remember cursor position also (and if it is on or off) +// * Implement screen diffing (taking another Window instance as an argument in +// render()) /* Represents a rectangular window, as a 2D array of characters and their @@ -567,15 +577,15 @@ std::string utf32_to_utf8(const std::u32string &s) class Window { private: - size_t x0, y0; // top-left corner of the window on the screen size_t w, h; // width and height of the window + size_t cursor_x, cursor_y; // current cursor position std::vector chars; // the characters in row first order std::vector m_fg; std::vector m_bg; std::vector