#include "ob/readline.hh" #include "ob/string.hh" #include "ob/text.hh" #include "ob/term.hh" namespace aec = OB::Term::ANSI_Escape_Codes; #include #include #include #include #include #include #include #include #include #include #include #include #include namespace fs = std::filesystem; namespace OB { Readline& Readline::style(std::string const& style) { _style.input = style; return *this; } Readline& Readline::prompt(std::string const& str, std::string const& style) { _prompt.str = str; _style.prompt = style; _prompt.fmt = aec::wrap(str, style); return *this; } void Readline::refresh() { _prompt.lhs = _prompt.fmt; _prompt.rhs.clear(); if (_input.str.cols() + 2 > _width) { std::size_t pos {_input.off + _input.idx - 1}; std::size_t cols {0}; for (; pos != OB::Text::String::npos && cols < _width - 2; --pos) { cols += _input.str.at(pos).cols; } if (pos == OB::Text::String::npos) { pos = 0; } std::size_t end {_input.off + _input.idx - pos}; _input.fmt.str(_input.str.substr(pos, end)); if (_input.fmt.cols() > _width - 2) { while (_input.fmt.cols() > _width - 2) { _input.fmt.erase(0, 1); } _input.cur = _input.fmt.cols(); if (_input.cur == OB::Text::String::npos) { _input.cur = 0; } _prompt.lhs = aec::wrap("<", _style.prompt); } else { _input.cur = _input.fmt.cols(); if (_input.cur == OB::Text::String::npos) { _input.cur = 0; } while (_input.fmt.cols() <= _width - 2) { _input.fmt.append(std::string(_input.str.at(end++).str)); } _input.fmt.erase(_input.fmt.size() - 1, 1); } if (_input.off + _input.idx < _input.str.size()) { _prompt.rhs = aec::wrap( OB::String::repeat(_width - _input.fmt.cols() - 2, " ") + ">", _style.prompt); } } else { _input.fmt = _input.str; _input.cur = _input.fmt.cols(0, _input.idx); if (_input.cur == OB::Text::String::npos) { _input.cur = 0; } } std::cout << aec::cursor_hide << aec::cr << aec::erase_line << _style.input << OB::String::repeat(_width, " ") << aec::cr << _prompt.lhs << _style.input << _input.fmt << aec::clear << _prompt.rhs << aec::cursor_set(_input.cur + 2, _height) << aec::cursor_show << std::flush; } std::string Readline::operator()(bool& is_running) { // update width and height of terminal OB::Term::size(_width, _height); // reset input struct _input = {}; // input key as 32-bit char char32_t ch {0}; // input key as utf8 string // contains 1-4 bytes std::string utf8; bool loop {true}; bool save_local {true}; bool save_file {true}; bool clear_input {false}; auto wait {std::chrono::milliseconds(50)}; std::cout << aec::cr << aec::erase_line << _style.input << OB::String::repeat(_width, " ") << aec::cr << _prompt.fmt << std::flush; while (loop && is_running) { std::this_thread::sleep_for(wait); while ((ch = OB::Term::get_key(&utf8)) > 0) { switch (ch) { case OB::Term::Key::escape: { // exit the command prompt loop = false; save_file = false; clear_input = true; break; } case OB::Term::Key::tab: { // TODO add tab completion break; } case OB::Term::ctrl_key('c'): { // exit the command prompt loop = false; save_file = false; save_local = false; clear_input = true; break; } case OB::Term::ctrl_key('u'): { edit_clear(); break; } case OB::Term::Key::newline: { // submit the input string loop = false; break; } case OB::Term::Key::up: case OB::Term::ctrl_key('p'): { hist_prev(); break; } case OB::Term::Key::down: case OB::Term::ctrl_key('n'): { hist_next(); break; } case OB::Term::Key::right: case OB::Term::ctrl_key('f'): { curs_right(); break; } case OB::Term::Key::left: case OB::Term::ctrl_key('b'): { curs_left(); break; } case OB::Term::Key::end: case OB::Term::ctrl_key('e'): { curs_end(); break; } case OB::Term::Key::home: case OB::Term::ctrl_key('a'): { curs_begin(); break; } case OB::Term::Key::delete_: case OB::Term::ctrl_key('d'): { loop = edit_delete(); break; } case OB::Term::Key::backspace: case OB::Term::ctrl_key('h'): { loop = edit_backspace(); break; } default: { if (ch < 0xF0000 && (ch == OB::Term::Key::space || OB::Text::is_graph(static_cast(ch)))) { edit_insert(utf8); } break; } } } } auto res = normalize(_input.str); if (! res.empty()) { if (save_local) { hist_push(res); } if (save_file && _input.str.str().front() != ' ') { hist_save(res); } } hist_reset(); if (clear_input) { res.clear(); } return res; } void Readline::curs_begin() { // move cursor to start of line if (_input.idx || _input.off) { _input.idx = 0; _input.off = 0; refresh(); } } void Readline::curs_end() { // move cursor to end of line if (_input.str.empty()) { return; } if (_input.off + _input.idx < _input.str.size()) { if (_input.str.cols() + 2 > _width) { _input.off = _input.str.size() - _width + 2; _input.idx = _width - 2; } else { _input.idx = _input.str.size(); } refresh(); } } void Readline::curs_left() { // move cursor left if (_input.off || _input.idx) { if (_input.off) { --_input.off; } else { --_input.idx; } refresh(); } } void Readline::curs_right() { // move cursor right if (_input.off + _input.idx < _input.str.size()) { if (_input.idx + 2 < _width) { ++_input.idx; } else { ++_input.off; } refresh(); } } void Readline::edit_insert(std::string const& str) { // insert or append char to input buffer auto size {_input.str.size()}; _input.str.insert(_input.off + _input.idx, str); if (size != _input.str.size()) { if (_input.idx + 2 < _width) { ++_input.idx; } else { ++_input.off; } } refresh(); hist_reset(); } void Readline::edit_clear() { // clear line _input.idx = 0; _input.off = 0; _input.str.clear(); refresh(); hist_reset(); } bool Readline::edit_delete() { // erase char under cursor if (_input.str.empty()) { _input.str.clear(); return false; } if (_input.off + _input.idx < _input.str.size()) { if (_input.idx + 2 < _width) { _input.str.erase(_input.off + _input.idx, 1); } else { _input.str.erase(_input.idx, 1); } refresh(); hist_reset(); } else if (_input.off || _input.idx) { if (_input.off) { _input.str.erase(_input.off + _input.idx - 1, 1); --_input.off; } else { --_input.idx; _input.str.erase(_input.idx, 1); } refresh(); hist_reset(); } return true; } bool Readline::edit_backspace() { // erase char behind cursor if (_input.str.empty()) { _input.str.clear(); return false; } _input.str.erase(_input.off + _input.idx - 1, 1); if (_input.off || _input.idx) { if (_input.off) { --_input.off; } else if (_input.idx) { --_input.idx; } refresh(); hist_reset(); } return true; } void Readline::hist_prev() { // cycle backwards in history if (_history().empty() && _history.search().empty()) { return; } bool bounds {_history.search().empty() ? (_history.idx < _history().size() - 1) : (_history.idx < _history.search().size() - 1)}; if (bounds || _history.idx == History::npos) { if (_history.idx == History::npos) { _input.buf = _input.str; if (! _input.buf.empty()) { hist_search(_input.buf); } } ++_history.idx; if (_history.search().empty()) { // normal search _input.str = _history().at(_history.idx); } else { // fuzzy search _input.str = _history().at(_history.search().at(_history.idx).idx); } if (_input.str.size() + 1 >= _width) { _input.off = _input.str.size() - _width + 2; _input.idx = _width - 2; } else { _input.off = 0; _input.idx = _input.str.size(); } refresh(); } } void Readline::hist_next() { // cycle forwards in history if (_history.idx != History::npos) { --_history.idx; if (_history.idx == History::npos) { _input.str = _input.buf; } else if (_history.search().empty()) { // normal search _input.str = _history().at(_history.idx); } else { // fuzzy search _input.str = _history().at(_history.search().at(_history.idx).idx); } if (_input.str.size() + 1 >= _width) { _input.off = _input.str.size() - _width + 2; _input.idx = _width - 2; } else { _input.off = 0; _input.idx = _input.str.size(); } refresh(); } } void Readline::hist_reset() { _history.search.clear(); _history.idx = History::npos; } void Readline::hist_search(std::string const& str) { _history.search.clear(); OB::Text::String input {OB::Text::normalize_foldcase( std::regex_replace(OB::Text::trim(str), std::regex("\\s+"), " ", std::regex_constants::match_not_null))}; if (input.empty()) { return; } std::size_t idx {0}; std::size_t count {0}; std::size_t weight {0}; std::string prev_hist {" "}; std::string prev_input {" "}; OB::Text::String hist; for (std::size_t i = 0; i < _history().size(); ++i) { hist.str(OB::Text::normalize_foldcase(_history().at(i))); if (hist.size() <= input.size()) { continue; } idx = 0; count = 0; weight = 0; prev_hist = " "; prev_input = " "; for (std::size_t j = 0, seq = 0; j < hist.size(); ++j) { if (idx < input.size() && hist.at(j).str == input.at(idx).str) { ++seq; count += 1; if (seq > 1) { count += 1; } if (prev_hist == " " && prev_input == " ") { count += 1; } prev_input = input.at(idx).str; ++idx; // short circuit to keep history order // comment out to search according to closest match if (idx == input.size()) { break; } } else { seq = 0; weight += 2; if (prev_input == " ") { weight += 1; } } prev_hist = hist.at(j).str; } if (idx != input.size()) { continue; } while (count && weight) { --count; --weight; } _history.search().emplace_back(weight, i); } std::sort(_history.search().begin(), _history.search().end(), [](auto const& lhs, auto const& rhs) { // default to history order if score is equal return lhs.score == rhs.score ? lhs.idx < rhs.idx : lhs.score < rhs.score; }); } void Readline::hist_push(std::string const& str) { if (_history().empty()) { _history().emplace_front(str); } else if (_history().back() != str) { if (auto pos = std::find(_history().begin(), _history().end(), str); pos != _history().end()) { _history().erase(pos); } _history().emplace_front(str); } } void Readline::hist_load(fs::path const& path) { if (! path.empty()) { std::ifstream ifile {path}; if (ifile && ifile.is_open()) { std::string line; while (std::getline(ifile, line)) { hist_push(line); } } hist_open(path); } } void Readline::hist_save(std::string const& str) { if (_history.file.is_open()) { _history.file << str << "\n" << std::flush; } } void Readline::hist_open(fs::path const& path) { _history.file.open(path, std::ios::app); if (! _history.file.is_open()) { throw std::runtime_error("could not open file '" + path.string() + "'"); } } std::string Readline::normalize(std::string const& str) const { // trim leading and trailing whitespace // collapse sequential whitespace return std::regex_replace(OB::Text::trim(str), std::regex("\\s+"), " ", std::regex_constants::match_not_null); } } // namespace OB