#include "elna/interactive.hpp" #include #include #include #include namespace elna { static termios original_termios; interactive_exception::interactive_exception() : runtime_error("read") { } template static std::pair read_number() { T position{ 0 }; unsigned char c{ 0 }; while (read(STDIN_FILENO, &c, 1) == 1 && std::isdigit(c)) { position = position * 10 + (c - '0'); } return std::make_pair(position, c); } void loop() { editor_history history; do { auto line = read_line(history); if (!line.has_value()) { write(STDOUT_FILENO, "\r\n", 2); break; } history.push(line.value().command_line()); if (!execute(line.value().command_line())) { break; } } while (true); } std::optional read_line(editor_history& history) { editor_state state; std::string buffer; std::string saved; state.draw(std::back_inserter(buffer)); write(STDOUT_FILENO, buffer.data(), buffer.size()); while (true) { buffer.clear(); auto pressed_key = read_key(); if (pressed_key.empty()) { continue; } switch (state.process_keypress(pressed_key)) { case action::finalize: if (pressed_key == key('d', modifier::ctrl)) { return std::optional(); } else if (pressed_key == '\r') { write(STDOUT_FILENO, "\r\n", 2); return std::make_optional(state); } break; case action::complete: write(STDOUT_FILENO, "\x1b[1E\x1b[2K", 8); state.insert(complete(state.command_line(), state.relative_position())); write(STDOUT_FILENO, "\x1b[1A", 4); state.draw(std::back_inserter(buffer)); write(STDOUT_FILENO, buffer.data(), buffer.size()); break; case action::redraw: state.draw(std::back_inserter(buffer)); write(STDOUT_FILENO, buffer.data(), buffer.size()); break; case action::write: write(STDOUT_FILENO, &pressed_key, 1); break; case action::move: buffer = cursor_to_column(state.absolute_position()); write(STDOUT_FILENO, buffer.data(), buffer.size()); break; case action::ignore: break; case action::history: editor_history::const_iterator history_iterator = history.cend(); if (pressed_key == key(special_key::arrow_up, modifier::ctrl)) { if (history.current() == history.cend()) { saved = state.command_line(); } history_iterator = history.prev(); } else if (pressed_key == key(special_key::arrow_down, modifier::ctrl)) { history_iterator = history.next(); if (history_iterator == history.cend()) { state.load(saved); } } if (history_iterator != history.cend()) { state.load(*history_iterator); } state.draw(std::back_inserter(buffer)); write(STDOUT_FILENO, buffer.data(), buffer.size()); break; } } } key read_key() { char c{ 0 }; if (read(STDIN_FILENO, &c, 1) == -1 && errno != EAGAIN) { throw interactive_exception(); } if (c != '\x1b') { return key(c); } if (read(STDIN_FILENO, &c, 1) != 1 || c != '[') { return key(); } auto [number, last_char] = read_number(); std::uint8_t modifiers{ 0 }; if (last_char == ';') { auto modifier_response = read_number(); modifiers = modifier_response.first; last_char = modifier_response.second; } if (number == 0 || number == 1) { switch (last_char) { case 'A': return key(special_key::arrow_up, modifiers); case 'B': return key(special_key::arrow_down, modifiers); case 'C': return key(special_key::arrow_right, modifiers); case 'D': return key(special_key::arrow_left, modifiers); case 'H': return key(special_key::home_key, modifiers); case 'F': return key(special_key::end_key, modifiers); } } else if (last_char == '~') { switch (number) { case 5: return key(special_key::page_up, modifiers); case 6: return key(special_key::page_up, modifiers); case 7: return key(special_key::home_key, modifiers); case 8: return key(special_key::end_key, modifiers); } } else if (last_char == 'u') { return key(number, modifiers); } return key(); } void print_exception(const std::exception& exception) { std::string message{ exception.what() }; message += "\r\n"; write(STDERR_FILENO, message.data(), message.size()); } bool execute(const std::string& line) { if (line.empty()) { return true; } if (line == "exit") { return false; } else if (boost::starts_with(line, "cd ")) { try { std::filesystem::current_path(line.substr(strlen("cd "))); } catch (const std::filesystem::filesystem_error& exception) { print_exception(exception); } return true; } launch(line); return true; } void launch(const std::string& program) { try { boost::process::system(program); } catch (const boost::process::process_error& exception) { print_exception(exception); } enable_raw_mode(); } bool enable_raw_mode() { if (tcgetattr(STDIN_FILENO, &original_termios) == -1) { return false; } termios raw = original_termios; raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); raw.c_oflag &= ~(OPOST); raw.c_cflag |= CS8; raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); write(STDOUT_FILENO, start_kitty_keybaord, strlen(start_kitty_keybaord)); return tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) != -1; } void disable_raw_mode() { write(STDOUT_FILENO, end_kitty_keybaord, strlen(end_kitty_keybaord)); tcsetattr(STDIN_FILENO, TCSAFLUSH, &original_termios); } std::string complete(const std::string& input, const std::size_t position) { std::filesystem::path program = boost::process::search_path("fzf"); if (program.empty()) { return ""; } boost::process::ipstream output; boost::process::system(program, "--height=10", "--layout=reverse", "-1", "--no-multi", boost::process::std_out > output); std::string selections; std::getline(output, selections, '\n'); return selections; } }