|
|
@@ -0,0 +1,299 @@
|
|
|
+#include "lumacs/editor_core.hpp"
|
|
|
+#include "lumacs/lua_api.hpp"
|
|
|
+#include <ftxui/component/component.hpp>
|
|
|
+#include <ftxui/component/screen_interactive.hpp>
|
|
|
+#include <ftxui/dom/elements.hpp>
|
|
|
+#include <iostream>
|
|
|
+#include <memory>
|
|
|
+
|
|
|
+using namespace ftxui;
|
|
|
+using namespace lumacs;
|
|
|
+
|
|
|
+/// FTXUI-based TUI frontend for Lumacs
|
|
|
+class TuiEditor {
|
|
|
+public:
|
|
|
+ TuiEditor() : lua_api_(core_) {
|
|
|
+ // Listen to editor events
|
|
|
+ core_.on_event([this](EditorEvent event) {
|
|
|
+ handle_editor_event(event);
|
|
|
+ });
|
|
|
+
|
|
|
+ // Load init.lua configuration
|
|
|
+ lua_api_.load_init_file();
|
|
|
+ }
|
|
|
+
|
|
|
+ void load_file(const std::filesystem::path& path) {
|
|
|
+ if (!core_.load_file(path)) {
|
|
|
+ std::cerr << "Failed to load file: " << path << std::endl;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Component create_ui(ScreenInteractive* screen) {
|
|
|
+ screen_ = screen;
|
|
|
+ auto renderer = Renderer([this] { return render(); });
|
|
|
+
|
|
|
+ // Add keyboard event handling
|
|
|
+ auto component = CatchEvent(renderer, [this](Event event) {
|
|
|
+ return handle_input(event);
|
|
|
+ });
|
|
|
+
|
|
|
+ return component;
|
|
|
+ }
|
|
|
+
|
|
|
+private:
|
|
|
+ EditorCore core_;
|
|
|
+ LuaApi lua_api_;
|
|
|
+ ScreenInteractive* screen_ = nullptr;
|
|
|
+ bool should_quit_ = false;
|
|
|
+ std::string message_line_; // For displaying messages in the UI
|
|
|
+
|
|
|
+ void handle_editor_event(EditorEvent event) {
|
|
|
+ if (event == EditorEvent::Quit) {
|
|
|
+ should_quit_ = true;
|
|
|
+ if (screen_) {
|
|
|
+ screen_->Exit();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ bool handle_input(Event event) {
|
|
|
+ // Convert FTXUI event to string key for Lua bindings
|
|
|
+ std::string key_name;
|
|
|
+
|
|
|
+ // Handle control characters first (they're not is_character())
|
|
|
+ if (!event.input().empty()) {
|
|
|
+ int code = static_cast<int>(static_cast<unsigned char>(event.input()[0]));
|
|
|
+
|
|
|
+ // Control characters (Ctrl+A through Ctrl+Z)
|
|
|
+ if (code > 0 && code < 27 && code != 9 && code != 10 && code != 13) {
|
|
|
+ // Skip tab(9), newline(10), return(13) - handle them specially
|
|
|
+ char letter = 'a' + (code - 1);
|
|
|
+ key_name = "C-" + std::string(1, letter);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Detect Meta/Alt modifier by checking raw input first
|
|
|
+ // Alt+ArrowUp sends ESC[1;3A (bytes: 27 91 49 59 51 65)
|
|
|
+ std::string input_str = event.input();
|
|
|
+
|
|
|
+ // Check for Alt+Arrow sequences before FTXUI event processing
|
|
|
+ if (input_str.size() >= 6 &&
|
|
|
+ input_str[0] == 27 && input_str[1] == 91 &&
|
|
|
+ input_str[2] == 49 && input_str[3] == 59 && input_str[4] == 51) {
|
|
|
+ // Alt modifier detected (ESC[1;3X pattern)
|
|
|
+ char arrow_key = input_str[5];
|
|
|
+ switch (arrow_key) {
|
|
|
+ case 65: key_name = "M-ArrowUp"; break; // A
|
|
|
+ case 66: key_name = "M-ArrowDown"; break; // B
|
|
|
+ case 67: key_name = "M-ArrowRight"; break; // C
|
|
|
+ case 68: key_name = "M-ArrowLeft"; break; // D
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handle special keys
|
|
|
+ if (key_name.empty()) {
|
|
|
+ if (event == Event::Escape) {
|
|
|
+ key_name = "Escape";
|
|
|
+ } else if (event == Event::ArrowUp) {
|
|
|
+ key_name = "ArrowUp";
|
|
|
+ } else if (event == Event::ArrowDown) {
|
|
|
+ key_name = "ArrowDown";
|
|
|
+ } else if (event == Event::ArrowLeft) {
|
|
|
+ key_name = "ArrowLeft";
|
|
|
+ } else if (event == Event::ArrowRight) {
|
|
|
+ key_name = "ArrowRight";
|
|
|
+ } else if (event == Event::Home) {
|
|
|
+ key_name = "Home";
|
|
|
+ } else if (event == Event::End) {
|
|
|
+ key_name = "End";
|
|
|
+ } else if (event == Event::Return) {
|
|
|
+ key_name = "Return";
|
|
|
+ } else if (event == Event::Tab) {
|
|
|
+ key_name = "Tab";
|
|
|
+ } else if (event.is_character()) {
|
|
|
+ // Regular printable characters
|
|
|
+ key_name = event.input();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Debug: show what key was pressed
|
|
|
+ if (!key_name.empty()) {
|
|
|
+ message_line_ = "Key: " + key_name;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Try Lua key binding first
|
|
|
+ if (!key_name.empty() && lua_api_.execute_key_binding(key_name)) {
|
|
|
+ message_line_ = "Executed: " + key_name;
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Fallback to default bindings
|
|
|
+ // Quit commands
|
|
|
+ if (event == Event::Escape || event == Event::Character('Q')) {
|
|
|
+ core_.request_quit();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Ctrl+C to quit
|
|
|
+ if (event.is_character()) {
|
|
|
+ auto input = event.input();
|
|
|
+ if (!input.empty() && input[0] == 3) { // Ctrl+C
|
|
|
+ core_.request_quit();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Arrow keys for cursor movement
|
|
|
+ if (event == Event::ArrowUp) {
|
|
|
+ core_.move_up();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (event == Event::ArrowDown) {
|
|
|
+ core_.move_down();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (event == Event::ArrowLeft) {
|
|
|
+ core_.move_left();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (event == Event::ArrowRight) {
|
|
|
+ core_.move_right();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Vim-style movement (h, j, k, l)
|
|
|
+ if (event == Event::Character('h')) {
|
|
|
+ core_.move_left();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (event == Event::Character('j')) {
|
|
|
+ core_.move_down();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (event == Event::Character('k')) {
|
|
|
+ core_.move_up();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (event == Event::Character('l')) {
|
|
|
+ core_.move_right();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Home/End
|
|
|
+ if (event == Event::Home) {
|
|
|
+ core_.move_to_line_start();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (event == Event::End) {
|
|
|
+ core_.move_to_line_end();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ Element render() {
|
|
|
+ const auto& buffer = core_.buffer();
|
|
|
+ const auto& cursor = core_.cursor();
|
|
|
+
|
|
|
+ // Get the current terminal dimensions
|
|
|
+ auto dimensions = Terminal::Size();
|
|
|
+ // Account for border (2 lines) + status line (2 lines including separator)
|
|
|
+ int viewport_height = std::max(1, dimensions.dimy - 4);
|
|
|
+ core_.set_viewport_size(dimensions.dimx, viewport_height);
|
|
|
+
|
|
|
+ const auto& buffer_lines = buffer.lines();
|
|
|
+ auto [start_line, end_line] = core_.visible_line_range();
|
|
|
+
|
|
|
+ Elements lines;
|
|
|
+
|
|
|
+ // Render visible lines
|
|
|
+ for (size_t i = start_line; i < end_line; ++i) {
|
|
|
+ const auto& line_text = buffer_lines[i];
|
|
|
+
|
|
|
+ // Build line with cursor highlighting
|
|
|
+ Elements line_elements;
|
|
|
+
|
|
|
+ // Add line number
|
|
|
+ line_elements.push_back(text(std::to_string(i + 1) + " │ "));
|
|
|
+
|
|
|
+ // Render line with cursor on the current line
|
|
|
+ if (i == cursor.line) {
|
|
|
+ // Split line into: before cursor | at cursor | after cursor
|
|
|
+ std::string before = line_text.substr(0, cursor.column);
|
|
|
+ std::string at_cursor;
|
|
|
+ std::string after;
|
|
|
+
|
|
|
+ if (cursor.column < line_text.size()) {
|
|
|
+ // Cursor is on a character
|
|
|
+ at_cursor = line_text.substr(cursor.column, 1);
|
|
|
+ after = line_text.substr(cursor.column + 1);
|
|
|
+ } else {
|
|
|
+ // Cursor is at end of line (show as space)
|
|
|
+ at_cursor = " ";
|
|
|
+ }
|
|
|
+
|
|
|
+ line_elements.push_back(text(before));
|
|
|
+ line_elements.push_back(text(at_cursor) | inverted); // Highlight cursor
|
|
|
+ line_elements.push_back(text(after));
|
|
|
+ } else {
|
|
|
+ // Regular line without cursor
|
|
|
+ line_elements.push_back(text(line_text));
|
|
|
+ }
|
|
|
+
|
|
|
+ lines.push_back(hbox(line_elements));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Fill remaining space if viewport is larger than content
|
|
|
+ while (lines.size() < static_cast<size_t>(viewport_height)) {
|
|
|
+ lines.push_back(text("~"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Status line
|
|
|
+ std::string status = buffer.name() +
|
|
|
+ (buffer.is_modified() ? " [+]" : "") +
|
|
|
+ " | Line " + std::to_string(cursor.line + 1) +
|
|
|
+ "/" + std::to_string(buffer.line_count()) +
|
|
|
+ " Col " + std::to_string(cursor.column + 1);
|
|
|
+
|
|
|
+ // Show scroll indicator if there's more content
|
|
|
+ if (buffer_lines.size() > static_cast<size_t>(viewport_height)) {
|
|
|
+ int percent = (cursor.line * 100) / (buffer_lines.size() - 1);
|
|
|
+ status += " " + std::to_string(percent) + "%";
|
|
|
+ }
|
|
|
+
|
|
|
+ // Message line (minibuffer)
|
|
|
+ Elements ui_elements = {
|
|
|
+ vbox(lines) | flex,
|
|
|
+ separator(),
|
|
|
+ text(status) | inverted
|
|
|
+ };
|
|
|
+
|
|
|
+ // Show message line if there's a message
|
|
|
+ if (!message_line_.empty()) {
|
|
|
+ ui_elements.push_back(text(message_line_) | dim);
|
|
|
+ }
|
|
|
+
|
|
|
+ return vbox(ui_elements) | border;
|
|
|
+ }
|
|
|
+
|
|
|
+};
|
|
|
+
|
|
|
+int main(int argc, char* argv[]) {
|
|
|
+ auto screen = ScreenInteractive::Fullscreen();
|
|
|
+ TuiEditor editor;
|
|
|
+
|
|
|
+ // Load file if provided
|
|
|
+ if (argc > 1) {
|
|
|
+ editor.load_file(argv[1]);
|
|
|
+ }
|
|
|
+
|
|
|
+ auto ui = editor.create_ui(&screen);
|
|
|
+
|
|
|
+ std::cout << "Lumacs - A modern text editor" << std::endl;
|
|
|
+ std::cout << "Controls: Arrow keys or hjkl to move, Q or Esc to quit" << std::endl;
|
|
|
+ std::cout << std::endl;
|
|
|
+
|
|
|
+ screen.Loop(ui);
|
|
|
+ return 0;
|
|
|
+}
|