tui_editor.cpp 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. #include "lumacs/tui_editor.hpp"
  2. #include "lumacs/editor_core.hpp"
  3. #include "lumacs/lua_api.hpp"
  4. #include <ncurses.h>
  5. #include <iostream>
  6. #include <fstream>
  7. #include <memory>
  8. #include <chrono>
  9. #include <string>
  10. #include <sstream>
  11. #include <algorithm>
  12. // Global debug log
  13. extern std::ofstream debug_log;
  14. std::ofstream debug_log("lumacs_debug.log");
  15. using namespace lumacs;
  16. /// ncurses-based TUI frontend for Lumacs
  17. class TuiEditor : public IEditorView {
  18. public:
  19. TuiEditor() : core_(nullptr) {}
  20. ~TuiEditor() override {
  21. endwin(); // Cleanup ncurses
  22. }
  23. // IEditorView methods
  24. void init() override;
  25. void run() override;
  26. void handle_editor_event(EditorEvent event) override;
  27. void set_core(EditorCore* core) override;
  28. private:
  29. EditorCore* core_ = nullptr; // Raw pointer to EditorCore, not owned
  30. bool should_quit_ = false;
  31. std::string message_line_;
  32. int height_ = 0, width_ = 0;
  33. // Private helper method declarations
  34. std::string resolve_key(int ch);
  35. bool handle_input(int ch);
  36. void render();
  37. void render_layout_node(std::shared_ptr<LayoutNode> node, int x, int y, int width, int height);
  38. void render_window(std::shared_ptr<Window> window, int x, int y, int width, int height);
  39. void render_window_modeline(std::shared_ptr<Window> window, int x, int y, int width, bool is_active);
  40. void render_message_line();
  41. };
  42. // --- TuiEditor Public Method Definitions ---
  43. void TuiEditor::init() {
  44. // Initialize ncurses
  45. initscr();
  46. cbreak(); // Disable line buffering
  47. noecho(); // Don't echo pressed keys
  48. keypad(stdscr, TRUE); // Enable special keys
  49. raw(); // Enable all control characters
  50. timeout(50); // Set 50ms timeout for getch() to avoid blocking forever
  51. // Color support
  52. if (has_colors()) {
  53. start_color();
  54. use_default_colors();
  55. }
  56. // Get screen dimensions
  57. getmaxyx(stdscr, height_, width_);
  58. // Initialize theme colors for ncurses
  59. if (has_colors() && core_->active_theme()) {
  60. core_->active_theme()->initialize_ncurses_colors();
  61. }
  62. // Set initial viewport size (leave room for minibuffer/message line)
  63. int minibuffer_lines = 1; // Always reserve 1 line for minibuffer/message
  64. int content_height = height_ - minibuffer_lines;
  65. bool show_line_numbers = core_->config().get<bool>("show_line_numbers", true);
  66. int line_number_width = show_line_numbers ? core_->config().get<int>("line_number_width", 6) : 0;
  67. int content_width = width_ - line_number_width;
  68. core_->set_viewport_size(content_width, content_height);
  69. debug_log << "ncurses editor initialized: " << width_ << "x" << height_ << std::endl;
  70. }
  71. void TuiEditor::run() {
  72. should_quit_ = false;
  73. // Initial render
  74. render();
  75. while (!should_quit_) {
  76. // Handle screen resize
  77. int new_height, new_width;
  78. getmaxyx(stdscr, new_height, new_width);
  79. if (new_height != height_ || new_width != width_) {
  80. height_ = new_height;
  81. width_ = new_width;
  82. int minibuffer_lines = 1; // Always reserve 1 line for minibuffer/message
  83. int content_height = height_ - minibuffer_lines;
  84. bool show_line_numbers = core_->config().get<bool>("show_line_numbers", true);
  85. int line_number_width = show_line_numbers ? core_->config().get<int>("line_number_width", 6) : 0;
  86. int content_width = width_ - line_number_width;
  87. core_->set_viewport_size(content_width, content_height);
  88. debug_log << "Screen resized to: " << width_ << "x" << height_ << std::endl;
  89. debug_log << "Content area: " << content_width << "x" << content_height << std::endl;
  90. render(); // Re-render after resize
  91. }
  92. // Get input (with timeout)
  93. int ch = getch();
  94. // Only process input and render if we got actual input (not timeout)
  95. if (ch != ERR) {
  96. handle_input(ch);
  97. render();
  98. }
  99. }
  100. }
  101. void TuiEditor::handle_editor_event(EditorEvent event) {
  102. if (event == EditorEvent::Quit) {
  103. should_quit_ = true;
  104. } else if (event == EditorEvent::Message) {
  105. message_line_ = core_->last_message(); // Still update local message_line_ for rendering
  106. } else if (event == EditorEvent::CommandMode) {
  107. core_->minibuffer_manager().activate_minibuffer(
  108. MinibufferMode::Command, ":",
  109. [this](const std::string& input) {
  110. if (input == "q" || input == "quit") {
  111. should_quit_ = true;
  112. } else {
  113. auto result = core_->command_system().execute(input);
  114. core_->set_message(result.message);
  115. }
  116. },
  117. [this]() { core_->set_message("Cancelled"); }
  118. );
  119. } else if (event == EditorEvent::BufferSwitchMode) {
  120. core_->minibuffer_manager().activate_minibuffer(
  121. MinibufferMode::BufferName, "Switch to buffer: ",
  122. [this](const std::string& input) {
  123. if (core_->switch_buffer_in_window(input)) {
  124. core_->set_message("Switched to: " + input);
  125. core_->lua_api()->execute("auto_activate_major_mode()");
  126. } else {
  127. core_->set_message("Buffer not found: " + input);
  128. }
  129. },
  130. [this]() { core_->set_message("Cancelled"); }
  131. );
  132. } else if (event == EditorEvent::KillBufferMode) {
  133. core_->minibuffer_manager().activate_minibuffer(
  134. MinibufferMode::BufferName, "Kill buffer: ",
  135. [this](const std::string& input) {
  136. // MinibufferManager should handle confirmation for modified buffers
  137. if (core_->close_buffer(input)) {
  138. core_->set_message("Closed buffer: " + input);
  139. } else {
  140. core_->set_message("Failed to close buffer: " + input);
  141. }
  142. },
  143. [this]() { core_->set_message("Cancelled"); }
  144. );
  145. } else if (event == EditorEvent::FindFileMode) {
  146. core_->minibuffer_manager().activate_minibuffer(
  147. MinibufferMode::FilePath, "Find file: ",
  148. [this](const std::string& input) {
  149. if (core_->load_file(input)) {
  150. core_->set_message("Loaded: " + input);
  151. core_->lua_api()->execute("auto_activate_major_mode()");
  152. } else {
  153. core_->set_message("Failed to load: " + input);
  154. }
  155. },
  156. [this]() { core_->set_message("Cancelled"); }
  157. );
  158. } else if (event == EditorEvent::ThemeSelectionMode) {
  159. core_->minibuffer_manager().activate_minibuffer(
  160. MinibufferMode::ThemeName, "Set theme: ",
  161. [this](const std::string& input) {
  162. auto theme_names = core_->theme_manager().theme_names();
  163. auto it = std::find(theme_names.begin(), theme_names.end(), input);
  164. if (it != theme_names.end()) {
  165. core_->set_theme(input);
  166. core_->set_message("Switched to theme: " + input);
  167. } else {
  168. core_->set_message("Theme not found: " + input);
  169. }
  170. },
  171. [this]() { core_->set_message("Cancelled"); }
  172. );
  173. } else if (event == EditorEvent::ISearchMode) {
  174. core_->minibuffer_manager().activate_minibuffer(
  175. MinibufferMode::ISearch, "I-search: ",
  176. [this](const std::string& input) { /* TODO: Implement actual isearch logic */ core_->set_message("I-search query: " + input); },
  177. [this]() { core_->set_message("Cancelled I-search"); }
  178. );
  179. } else if (event == EditorEvent::ISearchBackwardMode) {
  180. core_->minibuffer_manager().activate_minibuffer(
  181. MinibufferMode::ISearch, "I-search backward: ",
  182. [this](const std::string& input) { /* TODO: Implement actual isearch logic */ core_->set_message("I-search backward query: " + input); },
  183. [this]() { core_->set_message("Cancelled I-search backward"); }
  184. );
  185. } else if (event == EditorEvent::TransientMessageCleared) {
  186. // Redraw to clear the message from the screen
  187. render();
  188. }
  189. }
  190. void TuiEditor::set_core(EditorCore* core) {
  191. core_ = core;
  192. }
  193. // --- TuiEditor Private Helper Method Definitions ---
  194. /// Convert ncurses key code to our key name format
  195. std::string TuiEditor::resolve_key(int ch) {
  196. debug_log << "=== NCURSES INPUT DEBUG ===" << std::endl;
  197. debug_log << "Raw key code: " << ch << " (0x" << std::hex << ch << std::dec << ")" << std::endl;
  198. bool TuiEditor::handle_input(int ch) {
  199. // Resolve key name
  200. std::string key_name = resolve_key(ch);
  201. if (key_name.empty()) {
  202. debug_log << "Empty key name, ignoring input" << std::endl;
  203. return false;
  204. }
  205. // Handle Minibuffer Input Logic first
  206. if (core_->minibuffer_manager().is_active()) {
  207. return core_->minibuffer_manager().handle_key_event(key_name);
  208. }
  209. // Normal mode processing (pass to keybinding system)
  210. return core_->keybinding_manager().process_key(Key::parse(key_name)) != KeyResult::Unbound;
  211. }
  212. // process_key is removed as keybinding_manager handles it
  213. void TuiEditor::render() {
  214. // Clear and update screen info
  215. getmaxyx(stdscr, height_, width_);
  216. // Set background color from theme
  217. auto theme = core_->active_theme();
  218. if (theme) {
  219. int bg_color_pair = theme->get_color_pair(ThemeElement::Background);
  220. bkgd(bg_color_pair);
  221. }
  222. clear();
  223. // Calculate content area (leave room for message line and potentially a completion line)
  224. int minibuffer_lines = 1; // Always reserve 1 line for minibuffer/message
  225. // if (core_->minibuffer_manager().is_active() && !core_->minibuffer_manager().get_completion_candidates().empty()) {
  226. // minibuffer_lines++; // Reserve an extra line for completions
  227. // }
  228. int content_height = height_ - minibuffer_lines;
  229. int content_width = width_;
  230. // Render the layout tree recursively (now includes per-window modelines)
  231. render_layout_node(core_->root_layout(), 0, 0, content_width, content_height);
  232. // Global message/command line (last line)
  233. render_message_line();
  234. // Refresh screen
  235. refresh();
  236. }
  237. void TuiEditor::render_layout_node(std::shared_ptr<LayoutNode> node, int x, int y, int width, int height) {
  238. if (!node) return;
  239. if (node->type == LayoutNode::Type::Leaf) {
  240. // Render a single window
  241. render_window(node->window, x, y, width, height);
  242. } else if (node->type == LayoutNode::Type::HorizontalSplit) {
  243. // Split horizontally: top and bottom windows
  244. int top_height = height / 2;
  245. int bottom_height = height - top_height;
  246. render_layout_node(node->child1, x, y, width, top_height);
  247. render_layout_node(node->child2, x, y + top_height, width, bottom_height);
  248. } else if (node->type == LayoutNode::Type::VerticalSplit) {
  249. // Split vertically: left and right windows
  250. int separator_width = (width > 2) ? 1 : 0;
  251. int available_width = width - separator_width;
  252. int left_width = available_width / 2;
  253. int right_width = available_width - left_width;
  254. render_layout_node(node->child1, x, y, left_width, height);
  255. // Draw separator if enabled
  256. if (separator_width > 0) {
  257. int attrs = get_attributes_for_face("window-divider");
  258. attron(attrs);
  259. int sep_x = x + left_width;
  260. for (int i = 0; i < height; ++i) {
  261. mvaddch(y + i, sep_x, ACS_VLINE);
  262. }
  263. attroff(attrs);
  264. }
  265. render_layout_node(node->child2, x + left_width + separator_width, y, right_width, height);
  266. }
  267. }
  268. void TuiEditor::render_window(std::shared_ptr<Window> window, int x, int y, int width, int height) {
  269. if (!window) return;
  270. // Check configuration for line numbers and modeline
  271. bool show_line_numbers = core_->config().get<bool>("show_line_numbers", true);
  272. bool show_modeline = core_->config().get<bool>("show_modeline", true);
  273. int line_number_width = show_line_numbers ? core_->config().get<int>("line_number_width", 6) : 0;
  274. int modeline_height = show_modeline ? 1 : 0;
  275. // Update window viewport size (reserve space for modeline)
  276. int content_width = width - line_number_width;
  277. int content_height = height - modeline_height;
  278. window->set_viewport_size(content_width, content_height);
  279. // Get window data
  280. const auto& buffer = window->buffer();
  281. const auto cursor = window->cursor();
  282. auto [start_line, end_line] = window->visible_line_range();
  283. bool is_active = (window == core_->active_window());
  284. debug_log << "Render window at " << x << "," << y << " size " << width << "x" << height
  285. << " viewport=" << start_line << "-" << end_line
  286. << " cursor=(" << cursor.line << "," << cursor.column << ")"
  287. << " active=" << is_active << std::endl;
  288. // Render buffer lines
  289. for (int screen_y = 0; screen_y < content_height && start_line + screen_y < end_line; ++screen_y) {
  290. size_t buffer_line_idx = start_line + screen_y;
  291. const auto& line_text = buffer.line(buffer_line_idx);
  292. // Clear this line
  293. move(y + screen_y, x);
  294. for (int i = 0; i < width; ++i) addch(' ');
  295. // Line number (if enabled)
  296. if (show_line_numbers) {
  297. mvprintw(y + screen_y, x, "%3zu ", buffer_line_idx + 1);
  298. }
  299. // Line content with syntax highlighting
  300. if (!line_text.empty()) {
  301. int max_content_width = content_width - 1;
  302. const auto& styles = buffer.get_line_styles(buffer_line_idx);
  303. if (styles.empty()) {
  304. // No styles, render as plain text
  305. std::string display_text = line_text;
  306. if ((int)display_text.length() > max_content_width) {
  307. display_text = display_text.substr(0, max_content_width - 3) + "...";
  308. }
  309. mvprintw(y + screen_y, x + line_number_width, "%s", display_text.c_str());
  310. } else {
  311. // Render with syntax highlighting
  312. size_t pos = 0;
  313. int screen_x = x + line_number_width;
  314. for (const auto& styled : styles) {
  315. size_t start = styled.range.start.column;
  316. size_t end = std::min(styled.range.end.column, line_text.size());
  317. // Render unstyled text before this styled range
  318. if (pos < start && pos < line_text.size()) {
  319. size_t len = std::min(start - pos, line_text.size() - pos);
  320. std::string unstyled = line_text.substr(pos, len);
  321. mvprintw(y + screen_y, screen_x, "%s", unstyled.c_str());
  322. screen_x += unstyled.length();
  323. pos = start;
  324. }
  325. // Render styled text
  326. if (pos < end && pos < line_text.size()) {
  327. size_t len = std::min(end - pos, line_text.size() - pos);
  328. std::string styled_text = line_text.substr(pos, len);
  329. // Apply color
  330. int attrs = get_attributes_for_face(styled.attr.face_name);
  331. attron(attrs);
  332. mvprintw(y + screen_y, screen_x, "%s", styled_text.c_str());
  333. attroff(attrs);
  334. screen_x += styled_text.length();
  335. pos = end;
  336. }
  337. }
  338. // Render remaining unstyled text
  339. if (pos < line_text.size()) {
  340. std::string remaining = line_text.substr(pos);
  341. if ((int)(screen_x - x - line_number_width + remaining.length()) > max_content_width) {
  342. remaining = remaining.substr(0, max_content_width - (screen_x - x - line_number_width) - 3) + "...";
  343. }
  344. mvprintw(y + screen_y, screen_x, "%s", remaining.c_str());
  345. }
  346. }
  347. }
  348. // Highlight ISearch match - now handled by MinibufferManager
  349. // if (mode_ == Mode::ISearch && isearch_match_ && isearch_match_->start.line == buffer_line_idx) {
  350. // size_t match_start = isearch_match_->start.column;
  351. // size_t match_len = isearch_match_->end.column - match_start;
  352. // if (match_start < line_text.size()) {
  353. // size_t display_len = std::min(match_len, line_text.size() - match_start);
  354. // std::string matched_text = line_text.substr(match_start, display_len);
  355. // int match_x = x + line_number_width + match_start;
  356. // // Simple clipping check
  357. // if (match_x < x + width) {
  358. // int attrs = get_attributes_for_face(isearch_failed_ ? "isearch-fail" : "isearch");
  359. // if (attrs == 0) attrs = A_REVERSE; // Fallback
  360. // attron(attrs);
  361. // mvprintw(y + screen_y, match_x, "%s", matched_text.c_str());
  362. // attroff(attrs);
  363. // }
  364. // }
  365. // }
  366. // Show cursor if this is the cursor line and this is the active window
  367. if (buffer_line_idx == cursor.line && is_active) {
  368. int cursor_screen_x = x + line_number_width + cursor.column;
  369. if (cursor_screen_x < x + width) {
  370. char cursor_char = ' ';
  371. if (cursor.column < line_text.size()) {
  372. cursor_char = line_text[cursor.column];
  373. }
  374. int attrs = get_attributes_for_face("cursor");
  375. if (attrs == 0) attrs = A_REVERSE;
  376. attron(attrs);
  377. mvaddch(y + screen_y, cursor_screen_x, cursor_char);
  378. attroff(attrs);
  379. }
  380. }
  381. }
  382. // Fill remaining lines (for empty lines below buffer) - no tildes
  383. size_t displayed_lines = std::min((size_t)content_height, end_line - start_line);
  384. for (int screen_y = displayed_lines; screen_y < content_height; ++screen_y) {
  385. move(y + screen_y, x);
  386. for (int i = 0; i < width; ++i) addch(' ');
  387. }
  388. // Render modeline for this window
  389. if (show_modeline) {
  390. render_window_modeline(window, x, y + content_height, width, is_active);
  391. }
  392. }
  393. void TuiEditor::render_window_modeline(std::shared_ptr<Window> window, int x, int y, int width, bool is_active) {
  394. const auto& buffer = window->buffer();
  395. const auto cursor = window->cursor();
  396. // Choose modeline colors
  397. std::string face_name = is_active ? "mode-line" : "mode-line-inactive";
  398. int attrs = get_attributes_for_face(face_name);
  399. if (attrs == 0) attrs = is_active ? A_REVERSE : A_DIM;
  400. attron(attrs);
  401. // Clear the modeline
  402. move(y, x);
  403. for (int i = 0; i < width; ++i) addch(' ');
  404. // Create modeline content
  405. std::string modeline;
  406. // Buffer name and modification status
  407. modeline += buffer.name();
  408. if (buffer.is_modified()) modeline += " [+] ";
  409. // Cursor position
  410. modeline += " | " + std::to_string(cursor.line + 1) + ":" + std::to_string(cursor.column + 1);
  411. // Major mode (if available)
  412. // TODO: Add major mode support when available
  413. // Right-aligned content (percentage through file)
  414. std::string right_side;
  415. if (buffer.line_count() > 0) {
  416. int percentage = (cursor.line * 100) / (buffer.line_count() - 1);
  417. right_side = " " + std::to_string(percentage) + "%";
  418. }
  419. // Truncate modeline if too long
  420. int available_width = width - right_side.length();
  421. if ((int)modeline.length() > available_width) {
  422. modeline = modeline.substr(0, available_width - 3) + "...";
  423. }
  424. // Render left side
  425. mvprintw(y, x, "%s", modeline.c_str());
  426. // Render right side
  427. if (!right_side.empty()) {
  428. mvprintw(y, x + width - right_side.length(), "%s", right_side.c_str());
  429. }
  430. // Turn off modeline attributes
  431. attroff(attrs);
  432. }
  433. void TuiEditor::render_message_line() {
  434. int msg_y = height_ - 1;
  435. int attrs = get_attributes_for_face("minibuffer-prompt");
  436. attron(attrs);
  437. move(msg_y, 0);
  438. clrtoeol();
  439. if (core_->minibuffer_manager().is_active()) {
  440. std::string prompt_part = core_->minibuffer_manager().get_prompt();
  441. std::string input_part = core_->minibuffer_manager().get_input_buffer();
  442. std::string display_text = prompt_part + input_part;
  443. mvprintw(msg_y, 0, "%s", display_text.c_str());
  444. // Display completion candidates below the input line
  445. auto candidates = core_->minibuffer_manager().get_completion_candidates();
  446. if (!candidates.empty()) {
  447. std::string completion_display;
  448. for (size_t i = 0; i < candidates.size() && completion_display.length() < width_ - 5; ++i) {
  449. if (!completion_display.empty()) completion_display += " ";
  450. completion_display += candidates[i].display_text; // Use display_text
  451. }
  452. if (completion_display.length() >= width_ - 5) {
  453. completion_display = completion_display.substr(0, width_ - 8) + "...";
  454. }
  455. // Move up one line to display completions above the current minibuffer line
  456. mvprintw(msg_y - 1, 0, "%s", completion_display.c_str());
  457. }
  458. } else if (!message_line_.empty()) {
  459. // Display transient message
  460. mvprintw(msg_y, 0, "%s", message_line_.c_str());
  461. }
  462. attroff(attrs);
  463. // After rendering, check if it's time to clear the message.
  464. core_->check_and_clear_message();
  465. }
  466. namespace lumacs {
  467. std::unique_ptr<IEditorView> create_tui_editor() {
  468. return std::make_unique<TuiEditor>();
  469. }
  470. }