tui_editor.cpp 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. #include "lumacs/tui_editor.hpp"
  2. #include "lumacs/editor_core.hpp"
  3. #include "lumacs/lua_api.hpp"
  4. #include "lumacs/mode_activator.hpp"
  5. #include "lumacs/keybinding.hpp"
  6. #include "lumacs/cursor_blink.hpp"
  7. #include <ncurses.h>
  8. #include <memory>
  9. #include <chrono>
  10. #include <string>
  11. #include <sstream>
  12. #include <algorithm>
  13. #include <spdlog/spdlog.h>
  14. using namespace lumacs;
  15. /// ncurses-based TUI frontend for Lumacs
  16. class TuiEditor : public IEditorView {
  17. public:
  18. TuiEditor() : core_(nullptr) {}
  19. ~TuiEditor() override {
  20. endwin(); // Cleanup ncurses
  21. }
  22. // IEditorView methods
  23. void init() override;
  24. void run() override;
  25. void handle_editor_event(EditorEvent event) override;
  26. void set_core(EditorCore* core) override;
  27. private:
  28. EditorCore* core_ = nullptr; // Raw pointer to EditorCore, not owned
  29. std::unique_ptr<ModeActivator> mode_activator_;
  30. bool should_quit_ = false;
  31. std::string message_line_;
  32. int height_ = 0, width_ = 0;
  33. // Cursor blinking using shared controller
  34. CursorBlinkController cursor_blink_;
  35. // Viewport bounds calculation (avoids code duplication)
  36. struct ViewportBounds {
  37. int content_width;
  38. int content_height;
  39. int line_number_width;
  40. int minibuffer_lines;
  41. };
  42. ViewportBounds calculate_viewport_bounds() const {
  43. ViewportBounds bounds;
  44. bounds.minibuffer_lines = 1; // Always reserve 1 line for minibuffer/message
  45. bounds.content_height = height_ - bounds.minibuffer_lines;
  46. bool show_line_numbers = core_->config().get<bool>("show_line_numbers", true);
  47. bounds.line_number_width = show_line_numbers ? core_->config().get<int>("line_number_width", 6) : 0;
  48. bounds.content_width = width_ - bounds.line_number_width;
  49. return bounds;
  50. }
  51. // Private helper method declarations
  52. std::string resolve_key(int ch);
  53. bool handle_input(int ch);
  54. void render();
  55. void render_layout_node(std::shared_ptr<LayoutNode> node, int x, int y, int width, int height);
  56. void render_window(std::shared_ptr<Window> window, int x, int y, int width, int height);
  57. void render_window_modeline(std::shared_ptr<Window> window, int x, int y, int width, bool is_active);
  58. void render_message_line();
  59. int get_attributes_for_face(const std::string& face_name);
  60. // Hardware cursor position tracking
  61. int hw_cursor_x_ = -1;
  62. int hw_cursor_y_ = -1;
  63. };
  64. // --- TuiEditor Public Method Definitions ---
  65. void TuiEditor::init() {
  66. // Initialize ncurses
  67. initscr();
  68. cbreak(); // Disable line buffering
  69. noecho(); // Don't echo pressed keys
  70. keypad(stdscr, TRUE); // Enable special keys
  71. raw(); // Enable all control characters
  72. timeout(50); // Set 50ms timeout for getch() to avoid blocking forever
  73. // Color support
  74. if (has_colors()) {
  75. start_color();
  76. use_default_colors();
  77. }
  78. // Get screen dimensions
  79. getmaxyx(stdscr, height_, width_);
  80. // Initialize theme colors for ncurses
  81. if (has_colors() && core_->theme_manager().active_theme()) {
  82. core_->theme_manager().active_theme()->initialize_ncurses_colors();
  83. }
  84. // Set initial viewport size
  85. auto bounds = calculate_viewport_bounds();
  86. core_->set_viewport_size(bounds.content_width, bounds.content_height);
  87. spdlog::debug("ncurses editor initialized: {}x{}", width_, height_);
  88. }
  89. void TuiEditor::run() {
  90. should_quit_ = false;
  91. // Initial render
  92. render();
  93. while (!should_quit_) {
  94. // Handle screen resize
  95. int new_height, new_width;
  96. getmaxyx(stdscr, new_height, new_width);
  97. if (new_height != height_ || new_width != width_) {
  98. height_ = new_height;
  99. width_ = new_width;
  100. auto bounds = calculate_viewport_bounds();
  101. core_->set_viewport_size(bounds.content_width, bounds.content_height);
  102. spdlog::debug("Screen resized to: {}x{}", width_, height_);
  103. spdlog::debug("Content area: {}x{}", bounds.content_width, bounds.content_height);
  104. cursor_blink_.notify_cursor_moved(); // Resize implies movement
  105. render();
  106. }
  107. // Get input (with timeout)
  108. int ch = getch();
  109. // Only process input and render if we got actual input (not timeout)
  110. if (ch != ERR) {
  111. cursor_blink_.notify_cursor_moved();
  112. handle_input(ch);
  113. render();
  114. } else {
  115. // No input (timeout) - update blink state
  116. if (cursor_blink_.update()) {
  117. core_->check_and_clear_message();
  118. render();
  119. }
  120. }
  121. }
  122. }
  123. void TuiEditor::handle_editor_event(EditorEvent event) {
  124. // Handle UI-specific events first
  125. if (event == EditorEvent::Quit) {
  126. should_quit_ = true;
  127. return;
  128. } else if (event == EditorEvent::Message) {
  129. message_line_ = core_->last_message();
  130. return;
  131. } else if (event == EditorEvent::CursorMoved) {
  132. cursor_blink_.notify_cursor_moved();
  133. render();
  134. return;
  135. } else if (event == EditorEvent::TransientMessageCleared) {
  136. render();
  137. return;
  138. }
  139. // Delegate mode activation events to ModeActivator
  140. if (mode_activator_) {
  141. auto quit_callback = [this]() { should_quit_ = true; };
  142. mode_activator_->handle_mode_event(event, quit_callback);
  143. }
  144. }
  145. void TuiEditor::set_core(EditorCore* core) {
  146. core_ = core;
  147. if (core_) {
  148. mode_activator_ = std::make_unique<ModeActivator>(*core_);
  149. }
  150. }
  151. // --- TuiEditor Private Helper Method Definitions ---
  152. int TuiEditor::get_attributes_for_face(const std::string& face_name) {
  153. auto theme = core_->theme_manager().active_theme();
  154. if (theme) {
  155. return theme->get_face_attributes_ncurses(face_name);
  156. }
  157. return 0; // A_NORMAL
  158. }
  159. /// Convert ncurses key code to our key name format
  160. std::string TuiEditor::resolve_key(int ch) {
  161. spdlog::trace("NCURSES INPUT: Raw key code: {} (0x{:x})", ch, ch);
  162. if (ch == 27) return "Escape";
  163. if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) return "Return";
  164. if (ch == '\t') return "Tab";
  165. if (ch == KEY_BACKSPACE || ch == 127 || ch == '\b') return "Backspace";
  166. if (ch == KEY_UP) return "ArrowUp";
  167. if (ch == KEY_DOWN) return "ArrowDown";
  168. if (ch == KEY_LEFT) return "ArrowLeft";
  169. if (ch == KEY_RIGHT) return "ArrowRight";
  170. if (ch == KEY_HOME) return "Home";
  171. if (ch == KEY_END) return "End";
  172. if (ch == KEY_PPAGE) return "PageUp";
  173. if (ch == KEY_NPAGE) return "PageDown";
  174. if (ch == ' ') return "Space";
  175. // Control keys
  176. if (ch > 0 && ch < 32) {
  177. return "C-" + std::string(1, (char)('a' + ch - 1));
  178. }
  179. // Regular characters
  180. if (ch >= 32 && ch < 127) {
  181. return std::string(1, (char)ch);
  182. }
  183. return "";
  184. }
  185. bool TuiEditor::handle_input(int ch) {
  186. std::string key_name;
  187. // Check for Meta key sequence (Escape + Key)
  188. if (ch == 27) {
  189. // Set non-blocking read to check for immediate next key
  190. timeout(0);
  191. int next_ch = getch();
  192. timeout(50); // Restore timeout
  193. if (next_ch != ERR) {
  194. // Ensure next_ch is a valid printable char or special key we can map
  195. // For now, handle simple M-char sequences
  196. std::string next_key = resolve_key(next_ch);
  197. if (!next_key.empty() && next_key.length() == 1) { // Simple char
  198. key_name = "M-" + next_key;
  199. } else if (!next_key.empty()) {
  200. // Special key with Meta, e.g. M-ArrowUp?
  201. // resolve_key returns "ArrowUp", so we get "M-ArrowUp". This is valid.
  202. key_name = "M-" + next_key;
  203. } else {
  204. // Couldn't resolve next key, just treat as Escape then ignore next?
  205. // Or treat as Escape sequence.
  206. key_name = "Escape";
  207. // We effectively consumed next_ch and ignored it.
  208. // Better might be to ungetch, but ncurses ungetch is tricky.
  209. // Let's assume if resolve_key fails it was garbage.
  210. }
  211. } else {
  212. key_name = "Escape";
  213. }
  214. } else {
  215. key_name = resolve_key(ch);
  216. }
  217. if (key_name.empty()) {
  218. spdlog::trace("Empty key name, ignoring input");
  219. return false;
  220. }
  221. spdlog::trace("Resolved key: {}", key_name);
  222. // Handle Minibuffer Input Logic first
  223. if (core_->minibuffer_manager().is_active()) {
  224. return core_->minibuffer_manager().handle_key_event(key_name);
  225. }
  226. // Normal mode processing (pass to keybinding system)
  227. KeyProcessingResult result = core_->keybinding_manager().process_key(Key::parse(key_name));
  228. if (result.command_result.has_value()) {
  229. core_->set_message(result.command_result->message);
  230. }
  231. if (result.type == KeyResult::Unbound) {
  232. // Fallback: Self-insert for printable characters
  233. // Check if key is a single character and not a control sequence
  234. // The resolve_key function returns "C-x", "M-x", "Esc", "Return", etc.
  235. // Printable characters are returned as single chars "a", "1", etc.
  236. bool has_ctrl = key_name.find("C-") != std::string::npos;
  237. bool has_meta = key_name.find("M-") != std::string::npos;
  238. // Special case: "Space" should be treated as a printable character
  239. bool is_space = (key_name == "Space");
  240. std::string char_to_insert = is_space ? " " : key_name;
  241. if (!has_ctrl && !has_meta && (key_name.length() == 1 || is_space)) {
  242. // We can assume it's printable if length is 1 and it's not a special key (which resolve_key handles)
  243. core_->command_system().execute("self-insert-command", {char_to_insert});
  244. // --- Macro Recording Logic for Self-Insert ---
  245. if (core_->is_recording_macro()) {
  246. core_->record_key_sequence(char_to_insert);
  247. }
  248. // --------------------------------------------
  249. return true;
  250. }
  251. }
  252. return result.type != KeyResult::Unbound;
  253. }
  254. // process_key is removed as keybinding_manager handles it
  255. void TuiEditor::render() {
  256. // Reset hardware cursor position
  257. hw_cursor_x_ = -1;
  258. hw_cursor_y_ = -1;
  259. // Clear and update screen info
  260. getmaxyx(stdscr, height_, width_);
  261. // Set background color from theme
  262. auto theme = core_->theme_manager().active_theme();
  263. if (theme) {
  264. int bg_color_pair = theme->get_color_pair(ThemeElement::Background);
  265. bkgd(bg_color_pair);
  266. }
  267. clear();
  268. // Calculate content area
  269. auto bounds = calculate_viewport_bounds();
  270. // Render the layout tree recursively (now includes per-window modelines)
  271. render_layout_node(core_->root_layout(), 0, 0, width_, bounds.content_height);
  272. // Global message/command line (last line)
  273. render_message_line();
  274. // Set hardware cursor
  275. if (core_->minibuffer_manager().is_active()) {
  276. // Minibuffer active: render_message_line already moved cursor to input pos
  277. curs_set(2); // Ensure visible (high visibility)
  278. } else {
  279. // Minibuffer inactive: place at buffer cursor
  280. if (hw_cursor_x_ != -1 && hw_cursor_y_ != -1 && cursor_blink_.is_visible()) {
  281. move(hw_cursor_y_, hw_cursor_x_);
  282. curs_set(2); // High visibility
  283. } else {
  284. curs_set(0); // Hide if off-screen or blinking off
  285. }
  286. }
  287. // Refresh screen
  288. refresh();
  289. }
  290. void TuiEditor::render_layout_node(std::shared_ptr<LayoutNode> node, int x, int y, int width, int height) {
  291. if (!node) return;
  292. if (node->type == LayoutNode::Type::Leaf) {
  293. // Render a single window
  294. render_window(node->window, x, y, width, height);
  295. } else if (node->type == LayoutNode::Type::HorizontalSplit) {
  296. // Split horizontally: top and bottom windows
  297. int top_height = height / 2;
  298. int bottom_height = height - top_height;
  299. render_layout_node(node->child1, x, y, width, top_height);
  300. render_layout_node(node->child2, x, y + top_height, width, bottom_height);
  301. } else if (node->type == LayoutNode::Type::VerticalSplit) {
  302. // Split vertically: left and right windows
  303. int separator_width = (width > 2) ? 1 : 0;
  304. int available_width = width - separator_width;
  305. int left_width = available_width / 2;
  306. int right_width = available_width - left_width;
  307. render_layout_node(node->child1, x, y, left_width, height);
  308. // Draw separator if enabled
  309. if (separator_width > 0) {
  310. int attrs = get_attributes_for_face("window-divider");
  311. attron(attrs);
  312. int sep_x = x + left_width;
  313. for (int i = 0; i < height; ++i) {
  314. mvaddch(y + i, sep_x, ACS_VLINE);
  315. }
  316. attroff(attrs);
  317. }
  318. render_layout_node(node->child2, x + left_width + separator_width, y, right_width, height);
  319. }
  320. }
  321. void TuiEditor::render_window(std::shared_ptr<Window> window, int x, int y, int width, int height) {
  322. if (!window) return;
  323. // Check configuration for line numbers and modeline
  324. bool show_line_numbers = core_->config().get<bool>("show_line_numbers", true);
  325. bool show_modeline = core_->config().get<bool>("show_modeline", true);
  326. int line_number_width = show_line_numbers ? core_->config().get<int>("line_number_width", 6) : 0;
  327. int modeline_height = show_modeline ? 1 : 0;
  328. // Update window viewport size (reserve space for modeline)
  329. int content_width = width - line_number_width;
  330. int content_height = height - modeline_height;
  331. window->set_viewport_size(content_width, content_height);
  332. // Get window data
  333. const auto& buffer = window->buffer();
  334. const auto cursor = window->cursor();
  335. auto [start_line, end_line] = window->visible_line_range();
  336. bool is_active = (window == core_->active_window());
  337. spdlog::trace("Render window at {},{} size {}x{} viewport={}-{} cursor=({},{}) active={}",
  338. x, y, width, height, start_line, end_line, cursor.line, cursor.column, is_active);
  339. // Render buffer lines
  340. for (int screen_y = 0; screen_y < content_height && start_line + screen_y < end_line; ++screen_y) {
  341. size_t buffer_line_idx = start_line + screen_y;
  342. const auto& line_text = buffer.line(buffer_line_idx);
  343. // Clear this line
  344. move(y + screen_y, x);
  345. for (int i = 0; i < width; ++i) addch(' ');
  346. // Line number (if enabled)
  347. if (show_line_numbers) {
  348. mvprintw(y + screen_y, x, "%3zu ", buffer_line_idx + 1);
  349. }
  350. // Line content with syntax highlighting
  351. if (!line_text.empty()) {
  352. int max_content_width = content_width - 1;
  353. const auto& styles = buffer.get_line_styles(buffer_line_idx);
  354. if (styles.empty()) {
  355. // No styles, render as plain text
  356. std::string display_text = line_text;
  357. if ((int)display_text.length() > max_content_width) {
  358. display_text = display_text.substr(0, max_content_width - 3) + "...";
  359. }
  360. mvprintw(y + screen_y, x + line_number_width, "%s", display_text.c_str());
  361. } else {
  362. // Render with syntax highlighting
  363. size_t pos = 0;
  364. int screen_x = x + line_number_width;
  365. for (const auto& styled : styles) {
  366. size_t start = styled.range.start.column;
  367. size_t end = std::min(styled.range.end.column, line_text.size());
  368. // Render unstyled text before this styled range
  369. if (pos < start && pos < line_text.size()) {
  370. size_t len = std::min(start - pos, line_text.size() - pos);
  371. std::string unstyled = line_text.substr(pos, len);
  372. mvprintw(y + screen_y, screen_x, "%s", unstyled.c_str());
  373. screen_x += unstyled.length();
  374. pos = start;
  375. }
  376. // Render styled text
  377. if (pos < end && pos < line_text.size()) {
  378. size_t len = std::min(end - pos, line_text.size() - pos);
  379. std::string styled_text = line_text.substr(pos, len);
  380. // Apply color
  381. int attrs = get_attributes_for_face(styled.attr.face_name);
  382. attron(attrs);
  383. mvprintw(y + screen_y, screen_x, "%s", styled_text.c_str());
  384. attroff(attrs);
  385. screen_x += styled_text.length();
  386. pos = end;
  387. }
  388. }
  389. // Render remaining unstyled text
  390. if (pos < line_text.size()) {
  391. std::string remaining = line_text.substr(pos);
  392. if ((int)(screen_x - x - line_number_width + remaining.length()) > max_content_width) {
  393. remaining = remaining.substr(0, max_content_width - (screen_x - x - line_number_width) - 3) + "...";
  394. }
  395. mvprintw(y + screen_y, screen_x, "%s", remaining.c_str());
  396. }
  397. }
  398. }
  399. // Highlight ISearch match
  400. if (core_->minibuffer_manager().is_isearch_active() && core_->minibuffer_manager().get_isearch_match_range().has_value()) {
  401. auto match = core_->minibuffer_manager().get_isearch_match_range().value();
  402. // Check if current line is part of the match
  403. if (buffer_line_idx >= match.start.line && buffer_line_idx <= match.end.line) {
  404. int horizontal_offset = window->viewport().horizontal_offset;
  405. size_t hl_start = (buffer_line_idx == match.start.line) ? match.start.column : 0;
  406. size_t hl_end = (buffer_line_idx == match.end.line) ? match.end.column : line_text.length();
  407. // Handle case where match extends past end of line (e.g. capturing newline)
  408. // Ensure we don't go past line text length for string extraction
  409. size_t text_len = line_text.length();
  410. size_t safe_hl_end = std::min(hl_end, text_len);
  411. // Adjust for horizontal scroll
  412. int draw_start = static_cast<int>(hl_start) - horizontal_offset;
  413. int draw_end = static_cast<int>(safe_hl_end) - horizontal_offset;
  414. // Clip to viewable area
  415. draw_start = std::max(0, draw_start);
  416. // Clip draw_end to window width
  417. if (draw_end > content_width) draw_end = content_width;
  418. if (draw_start < draw_end) {
  419. // Extract text to highlight
  420. std::string text_to_hl = line_text.substr(draw_start + horizontal_offset, draw_end - draw_start);
  421. int screen_x = x + line_number_width + draw_start;
  422. int attrs = get_attributes_for_face(core_->minibuffer_manager().is_isearch_failed() ? "isearch-fail" : "isearch");
  423. if (attrs == 0) attrs = A_REVERSE;
  424. attron(attrs);
  425. mvprintw(y + screen_y, screen_x, "%s", text_to_hl.c_str());
  426. attroff(attrs);
  427. }
  428. }
  429. }
  430. // Show cursor if this is the cursor line and this is the active window
  431. if (buffer_line_idx == cursor.line && is_active && cursor_blink_.is_visible()) {
  432. int cursor_screen_x = x + line_number_width + cursor.column;
  433. if (cursor_screen_x < x + width) {
  434. char cursor_char = ' ';
  435. if (cursor.column < line_text.size()) {
  436. cursor_char = line_text[cursor.column];
  437. }
  438. // Force A_REVERSE to ensure visible block cursor
  439. int attrs = A_REVERSE;
  440. attron(attrs);
  441. mvaddch(y + screen_y, cursor_screen_x, cursor_char);
  442. attroff(attrs);
  443. }
  444. }
  445. // Track hardware cursor position for active window
  446. if (buffer_line_idx == cursor.line && is_active) {
  447. int cursor_screen_x = x + line_number_width + (int)cursor.column;
  448. if (cursor_screen_x < x + width) {
  449. hw_cursor_x_ = cursor_screen_x;
  450. hw_cursor_y_ = y + screen_y;
  451. }
  452. }
  453. }
  454. // Fill remaining lines (for empty lines below buffer) - no tildes
  455. size_t displayed_lines = std::min((size_t)content_height, end_line - start_line);
  456. for (int screen_y = displayed_lines; screen_y < content_height; ++screen_y) {
  457. move(y + screen_y, x);
  458. for (int i = 0; i < width; ++i) addch(' ');
  459. }
  460. // Render modeline for this window
  461. if (show_modeline) {
  462. render_window_modeline(window, x, y + content_height, width, is_active);
  463. }
  464. }
  465. void TuiEditor::render_window_modeline(std::shared_ptr<Window> window, int x, int y, int width, bool is_active) {
  466. const auto& buffer = window->buffer();
  467. const auto cursor = window->cursor();
  468. // Choose modeline colors
  469. std::string face_name = is_active ? "mode-line" : "mode-line-inactive";
  470. int attrs = get_attributes_for_face(face_name);
  471. if (attrs == 0) attrs = is_active ? A_REVERSE : A_DIM;
  472. attron(attrs);
  473. // Clear the modeline
  474. move(y, x);
  475. for (int i = 0; i < width; ++i) addch(' ');
  476. // Create modeline content
  477. std::string modeline;
  478. // Buffer name and modification status
  479. modeline += buffer.name();
  480. if (buffer.is_modified()) modeline += " [+] ";
  481. // Cursor position
  482. modeline += " | " + std::to_string(cursor.line + 1) + ":" + std::to_string(cursor.column + 1);
  483. // Major mode (if available)
  484. // TODO: Add major mode support when available
  485. // Right-aligned content (percentage through file)
  486. std::string right_side;
  487. if (buffer.line_count() > 0) {
  488. int percentage = (cursor.line * 100) / (buffer.line_count() - 1);
  489. right_side = " " + std::to_string(percentage) + "%";
  490. }
  491. // Truncate modeline if too long
  492. int available_width = width - right_side.length();
  493. if ((int)modeline.length() > available_width) {
  494. modeline = modeline.substr(0, available_width - 3) + "...";
  495. }
  496. // Render left side
  497. mvprintw(y, x, "%s", modeline.c_str());
  498. // Render right side
  499. if (!right_side.empty()) {
  500. mvprintw(y, x + width - right_side.length(), "%s", right_side.c_str());
  501. }
  502. // Turn off modeline attributes
  503. attroff(attrs);
  504. }
  505. void TuiEditor::render_message_line() {
  506. int msg_y = height_ - 1;
  507. int attrs = get_attributes_for_face("minibuffer-prompt");
  508. attron(attrs);
  509. move(msg_y, 0);
  510. clrtoeol();
  511. if (core_->minibuffer_manager().is_active()) {
  512. std::string prompt_part = core_->minibuffer_manager().get_prompt();
  513. std::string input_part = core_->minibuffer_manager().get_input_buffer();
  514. std::string display_text = prompt_part + input_part;
  515. mvprintw(msg_y, 0, "%s", display_text.c_str());
  516. // Display completion candidates below the input line (conceptually, but physically above if at bottom)
  517. auto candidates = core_->minibuffer_manager().get_completion_candidates();
  518. if (!candidates.empty() && msg_y > 0) {
  519. // Move up one line to display completions above the current minibuffer line
  520. move(msg_y - 1, 0);
  521. clrtoeol(); // Clear the line first to remove artifacts from underlying window
  522. std::string completion_display;
  523. for (size_t i = 0; i < candidates.size() && static_cast<int>(completion_display.length()) < width_ - 5; ++i) {
  524. if (!completion_display.empty()) completion_display += " ";
  525. completion_display += candidates[i].display_text;
  526. }
  527. if (static_cast<int>(completion_display.length()) >= width_ - 5) {
  528. completion_display = completion_display.substr(0, width_ - 8) + "...";
  529. }
  530. // Render with a different color/attribute if possible
  531. mvprintw(msg_y - 1, 0, "%s", completion_display.c_str());
  532. }
  533. // Explicitly place cursor at the correct position in minibuffer
  534. // prompt_part is handled by get_prompt(), cursor_position is relative to input_buffer
  535. move(msg_y, prompt_part.length() + core_->minibuffer_manager().get_cursor_position());
  536. } else if (core_->keybinding_manager().is_building_sequence()) {
  537. // Display partial key sequence (like Emacs "C-x-")
  538. std::string key_seq = core_->keybinding_manager().current_sequence_display() + "-";
  539. mvprintw(msg_y, 0, "%s", key_seq.c_str());
  540. } else if (!message_line_.empty()) {
  541. // Display transient message with truncation if needed
  542. std::string display_msg = message_line_;
  543. // Handle multi-line messages: show first line + indicator
  544. size_t newline_pos = display_msg.find('\n');
  545. if (newline_pos != std::string::npos) {
  546. // Count lines
  547. size_t line_count = 1;
  548. size_t pos = 0;
  549. while ((pos = display_msg.find('\n', pos)) != std::string::npos) {
  550. ++line_count;
  551. ++pos;
  552. }
  553. // Show first line with line count indicator
  554. display_msg = display_msg.substr(0, newline_pos) +
  555. " [+" + std::to_string(line_count - 1) + " lines, C-h e]";
  556. }
  557. const std::string suffix = "...[C-h e]";
  558. int available_width = width_ - 1;
  559. if (static_cast<int>(display_msg.length()) > available_width) {
  560. size_t max_chars = available_width - suffix.length();
  561. display_msg = display_msg.substr(0, max_chars) + suffix;
  562. }
  563. // Apply color based on severity
  564. int msg_attrs = attrs;
  565. switch (core_->last_message_severity()) {
  566. case MessageSeverity::Warning:
  567. msg_attrs |= COLOR_PAIR(3) | A_BOLD; // Yellow
  568. break;
  569. case MessageSeverity::Error:
  570. msg_attrs |= COLOR_PAIR(1) | A_BOLD; // Red
  571. break;
  572. default:
  573. break;
  574. }
  575. attron(msg_attrs);
  576. mvprintw(msg_y, 0, "%s", display_msg.c_str());
  577. attroff(msg_attrs);
  578. }
  579. attroff(attrs);
  580. // After rendering, check if it's time to clear the message.
  581. core_->check_and_clear_message();
  582. }
  583. namespace lumacs {
  584. std::unique_ptr<IEditorView> create_tui_editor() {
  585. return std::make_unique<TuiEditor>();
  586. }
  587. }