tui_editor.cpp 28 KB

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