| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715 |
- #include "lumacs/tui_editor.hpp"
- #include "lumacs/editor_core.hpp"
- #include "lumacs/lua_api.hpp"
- #include "lumacs/mode_activator.hpp"
- #include "lumacs/keybinding.hpp"
- #include "lumacs/cursor_blink.hpp"
- #include <ncurses.h>
- #include <memory>
- #include <chrono>
- #include <string>
- #include <sstream>
- #include <algorithm>
- #include <spdlog/spdlog.h>
- using namespace lumacs;
- /// ncurses-based TUI frontend for Lumacs
- class TuiEditor : public IEditorView {
- public:
- TuiEditor() : core_(nullptr) {}
- ~TuiEditor() override {
- endwin(); // Cleanup ncurses
- }
- // IEditorView methods
- void init() override;
- void run() override;
- void handle_editor_event(EditorEvent event) override;
- void set_core(EditorCore* core) override;
- private:
- EditorCore* core_ = nullptr; // Raw pointer to EditorCore, not owned
- std::unique_ptr<ModeActivator> mode_activator_;
- bool should_quit_ = false;
- std::string message_line_;
- int height_ = 0, width_ = 0;
- // Cursor blinking using shared controller
- CursorBlinkController cursor_blink_;
-
- // Viewport bounds calculation (avoids code duplication)
- struct ViewportBounds {
- int content_width;
- int content_height;
- int line_number_width;
- int minibuffer_lines;
- };
- ViewportBounds calculate_viewport_bounds() const {
- ViewportBounds bounds;
- bounds.minibuffer_lines = 1; // Always reserve 1 line for minibuffer/message
- bounds.content_height = height_ - bounds.minibuffer_lines;
- bool show_line_numbers = core_->config().get<bool>("show_line_numbers", true);
- bounds.line_number_width = show_line_numbers ? core_->config().get<int>("line_number_width", 6) : 0;
- bounds.content_width = width_ - bounds.line_number_width;
- return bounds;
- }
- // Private helper method declarations
- std::string resolve_key(int ch);
- bool handle_input(int ch);
- void render();
- void render_layout_node(std::shared_ptr<LayoutNode> node, int x, int y, int width, int height);
- void render_window(std::shared_ptr<Window> window, int x, int y, int width, int height);
- void render_window_modeline(std::shared_ptr<Window> window, int x, int y, int width, bool is_active);
- void render_message_line();
- int get_attributes_for_face(const std::string& face_name);
- // Hardware cursor position tracking
- int hw_cursor_x_ = -1;
- int hw_cursor_y_ = -1;
- };
- // --- TuiEditor Public Method Definitions ---
- void TuiEditor::init() {
- // Initialize ncurses
- initscr();
- cbreak(); // Disable line buffering
- noecho(); // Don't echo pressed keys
- keypad(stdscr, TRUE); // Enable special keys
- raw(); // Enable all control characters
- timeout(50); // Set 50ms timeout for getch() to avoid blocking forever
-
- // Color support
- if (has_colors()) {
- start_color();
- use_default_colors();
- }
-
- // Get screen dimensions
- getmaxyx(stdscr, height_, width_);
-
- // Initialize theme colors for ncurses
- if (has_colors() && core_->theme_manager().active_theme()) {
- core_->theme_manager().active_theme()->initialize_ncurses_colors();
- }
-
- // Set initial viewport size
- auto bounds = calculate_viewport_bounds();
- core_->set_viewport_size(bounds.content_width, bounds.content_height);
-
- spdlog::debug("ncurses editor initialized: {}x{}", width_, height_);
- }
- void TuiEditor::run() {
- should_quit_ = false;
-
- // Initial render
- render();
- while (!should_quit_) {
- // Handle screen resize
- int new_height, new_width;
- getmaxyx(stdscr, new_height, new_width);
- if (new_height != height_ || new_width != width_) {
- height_ = new_height;
- width_ = new_width;
- auto bounds = calculate_viewport_bounds();
- core_->set_viewport_size(bounds.content_width, bounds.content_height);
- spdlog::debug("Screen resized to: {}x{}", width_, height_);
- spdlog::debug("Content area: {}x{}", bounds.content_width, bounds.content_height);
- cursor_blink_.notify_cursor_moved(); // Resize implies movement
- render();
- }
- // Get input (with timeout)
- int ch = getch();
- // Only process input and render if we got actual input (not timeout)
- if (ch != ERR) {
- cursor_blink_.notify_cursor_moved();
- handle_input(ch);
- render();
- } else {
- // No input (timeout) - update blink state
- if (cursor_blink_.update()) {
- core_->check_and_clear_message();
- render();
- }
- }
- }
- }
- void TuiEditor::handle_editor_event(EditorEvent event) {
- // Handle UI-specific events first
- if (event == EditorEvent::Quit) {
- should_quit_ = true;
- return;
- } else if (event == EditorEvent::Message) {
- message_line_ = core_->last_message();
- return;
- } else if (event == EditorEvent::CursorMoved) {
- cursor_blink_.notify_cursor_moved();
- render();
- return;
- } else if (event == EditorEvent::TransientMessageCleared) {
- render();
- return;
- }
- // Delegate mode activation events to ModeActivator
- if (mode_activator_) {
- auto quit_callback = [this]() { should_quit_ = true; };
- mode_activator_->handle_mode_event(event, quit_callback);
- }
- }
- void TuiEditor::set_core(EditorCore* core) {
- core_ = core;
- if (core_) {
- mode_activator_ = std::make_unique<ModeActivator>(*core_);
- }
- }
- // --- TuiEditor Private Helper Method Definitions ---
- int TuiEditor::get_attributes_for_face(const std::string& face_name) {
- auto theme = core_->theme_manager().active_theme();
- if (theme) {
- return theme->get_face_attributes_ncurses(face_name);
- }
- return 0; // A_NORMAL
- }
- /// Convert ncurses key code to our key name format
- std::string TuiEditor::resolve_key(int ch) {
- spdlog::trace("NCURSES INPUT: Raw key code: {} (0x{:x})", ch, ch);
-
- if (ch == 27) return "Escape";
- if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) return "Return";
- if (ch == '\t') return "Tab";
- if (ch == KEY_BACKSPACE || ch == 127 || ch == '\b') return "Backspace";
- if (ch == KEY_UP) return "ArrowUp";
- if (ch == KEY_DOWN) return "ArrowDown";
- if (ch == KEY_LEFT) return "ArrowLeft";
- if (ch == KEY_RIGHT) return "ArrowRight";
- if (ch == KEY_HOME) return "Home";
- if (ch == KEY_END) return "End";
- if (ch == KEY_PPAGE) return "PageUp";
- if (ch == KEY_NPAGE) return "PageDown";
- if (ch == ' ') return "Space";
-
- // Control keys
- if (ch > 0 && ch < 32) {
- return "C-" + std::string(1, (char)('a' + ch - 1));
- }
- // Regular characters
- if (ch >= 32 && ch < 127) {
- return std::string(1, (char)ch);
- }
- return "";
- }
- bool TuiEditor::handle_input(int ch) {
- std::string key_name;
- // Check for Meta key sequence (Escape + Key)
- if (ch == 27) {
- // Set non-blocking read to check for immediate next key
- timeout(0);
- int next_ch = getch();
- timeout(50); // Restore timeout
- if (next_ch != ERR) {
- // Ensure next_ch is a valid printable char or special key we can map
- // For now, handle simple M-char sequences
- std::string next_key = resolve_key(next_ch);
- if (!next_key.empty() && next_key.length() == 1) { // Simple char
- key_name = "M-" + next_key;
- } else if (!next_key.empty()) {
- // Special key with Meta, e.g. M-ArrowUp?
- // resolve_key returns "ArrowUp", so we get "M-ArrowUp". This is valid.
- key_name = "M-" + next_key;
- } else {
- // Couldn't resolve next key, just treat as Escape then ignore next?
- // Or treat as Escape sequence.
- key_name = "Escape";
- // We effectively consumed next_ch and ignored it.
- // Better might be to ungetch, but ncurses ungetch is tricky.
- // Let's assume if resolve_key fails it was garbage.
- }
- } else {
- key_name = "Escape";
- }
- } else {
- key_name = resolve_key(ch);
- }
-
- if (key_name.empty()) {
- spdlog::trace("Empty key name, ignoring input");
- return false;
- }
- spdlog::trace("Resolved key: {}", key_name);
- // Handle Minibuffer Input Logic first
- if (core_->minibuffer_manager().is_active()) {
- return core_->minibuffer_manager().handle_key_event(key_name);
- }
- // Normal mode processing (pass to keybinding system)
- KeyProcessingResult result = core_->keybinding_manager().process_key(Key::parse(key_name));
- if (result.command_result.has_value()) {
- core_->set_message(result.command_result->message);
- }
-
- if (result.type == KeyResult::Unbound) {
- // Fallback: Self-insert for printable characters
- // Check if key is a single character and not a control sequence
- // The resolve_key function returns "C-x", "M-x", "Esc", "Return", etc.
- // Printable characters are returned as single chars "a", "1", etc.
- bool has_ctrl = key_name.find("C-") != std::string::npos;
- bool has_meta = key_name.find("M-") != std::string::npos;
- // Special case: "Space" should be treated as a printable character
- bool is_space = (key_name == "Space");
- std::string char_to_insert = is_space ? " " : key_name;
- if (!has_ctrl && !has_meta && (key_name.length() == 1 || is_space)) {
- // We can assume it's printable if length is 1 and it's not a special key (which resolve_key handles)
- core_->command_system().execute("self-insert-command", {char_to_insert});
- // --- Macro Recording Logic for Self-Insert ---
- if (core_->is_recording_macro()) {
- core_->record_key_sequence(char_to_insert);
- }
- // --------------------------------------------
- return true;
- }
- }
- return result.type != KeyResult::Unbound;
- }
- // process_key is removed as keybinding_manager handles it
- void TuiEditor::render() {
- // Reset hardware cursor position
- hw_cursor_x_ = -1;
- hw_cursor_y_ = -1;
- // Clear and update screen info
- getmaxyx(stdscr, height_, width_);
-
- // Set background color from theme
- auto theme = core_->theme_manager().active_theme();
- if (theme) {
- int bg_color_pair = theme->get_color_pair(ThemeElement::Background);
- bkgd(bg_color_pair);
- }
- clear();
-
- // Calculate content area
- auto bounds = calculate_viewport_bounds();
- // Render the layout tree recursively (now includes per-window modelines)
- render_layout_node(core_->root_layout(), 0, 0, width_, bounds.content_height);
-
- // Global message/command line (last line)
- render_message_line();
-
- // Set hardware cursor
- if (core_->minibuffer_manager().is_active()) {
- // Minibuffer active: render_message_line already moved cursor to input pos
- curs_set(2); // Ensure visible (high visibility)
- } else {
- // Minibuffer inactive: place at buffer cursor
- if (hw_cursor_x_ != -1 && hw_cursor_y_ != -1 && cursor_blink_.is_visible()) {
- move(hw_cursor_y_, hw_cursor_x_);
- curs_set(2); // High visibility
- } else {
- curs_set(0); // Hide if off-screen or blinking off
- }
- }
- // Refresh screen
- refresh();
- }
- void TuiEditor::render_layout_node(std::shared_ptr<LayoutNode> node, int x, int y, int width, int height) {
- if (!node) return;
-
- if (node->type == LayoutNode::Type::Leaf) {
- // Render a single window
- render_window(node->window, x, y, width, height);
- } else if (node->type == LayoutNode::Type::HorizontalSplit) {
- // Split horizontally: top and bottom windows
- int top_height = height / 2;
- int bottom_height = height - top_height;
-
- render_layout_node(node->child1, x, y, width, top_height);
- render_layout_node(node->child2, x, y + top_height, width, bottom_height);
- } else if (node->type == LayoutNode::Type::VerticalSplit) {
- // Split vertically: left and right windows
- int separator_width = (width > 2) ? 1 : 0;
- int available_width = width - separator_width;
- int left_width = available_width / 2;
- int right_width = available_width - left_width;
-
- render_layout_node(node->child1, x, y, left_width, height);
- // Draw separator if enabled
- if (separator_width > 0) {
- int attrs = get_attributes_for_face("window-divider");
- attron(attrs);
-
- int sep_x = x + left_width;
- for (int i = 0; i < height; ++i) {
- mvaddch(y + i, sep_x, ACS_VLINE);
- }
-
- attroff(attrs);
- }
- render_layout_node(node->child2, x + left_width + separator_width, y, right_width, height);
- }
- }
- void TuiEditor::render_window(std::shared_ptr<Window> window, int x, int y, int width, int height) {
- if (!window) return;
-
- // Check configuration for line numbers and modeline
- bool show_line_numbers = core_->config().get<bool>("show_line_numbers", true);
- bool show_modeline = core_->config().get<bool>("show_modeline", true);
- int line_number_width = show_line_numbers ? core_->config().get<int>("line_number_width", 6) : 0;
- int modeline_height = show_modeline ? 1 : 0;
-
- // Update window viewport size (reserve space for modeline)
- int content_width = width - line_number_width;
- int content_height = height - modeline_height;
- window->set_viewport_size(content_width, content_height);
-
- // Get window data
- const auto& buffer = window->buffer();
- const auto cursor = window->cursor();
- auto [start_line, end_line] = window->visible_line_range();
- bool is_active = (window == core_->active_window());
-
- spdlog::trace("Render window at {},{} size {}x{} viewport={}-{} cursor=({},{}) active={}",
- x, y, width, height, start_line, end_line, cursor.line, cursor.column, is_active);
-
- // Render buffer lines
- for (int screen_y = 0; screen_y < content_height && start_line + screen_y < end_line; ++screen_y) {
- size_t buffer_line_idx = start_line + screen_y;
- const auto& line_text = buffer.line(buffer_line_idx);
-
- // Clear this line
- move(y + screen_y, x);
- for (int i = 0; i < width; ++i) addch(' ');
-
- // Line number (if enabled)
- if (show_line_numbers) {
- mvprintw(y + screen_y, x, "%3zu ", buffer_line_idx + 1);
- }
- // Line content with syntax highlighting
- if (!line_text.empty()) {
- int max_content_width = content_width - 1;
- const auto& styles = buffer.get_line_styles(buffer_line_idx);
- if (styles.empty()) {
- // No styles, render as plain text
- std::string display_text = line_text;
- if ((int)display_text.length() > max_content_width) {
- display_text = display_text.substr(0, max_content_width - 3) + "...";
- }
- mvprintw(y + screen_y, x + line_number_width, "%s", display_text.c_str());
- } else {
- // Render with syntax highlighting
- size_t pos = 0;
- int screen_x = x + line_number_width;
- for (const auto& styled : styles) {
- size_t start = styled.range.start.column;
- size_t end = std::min(styled.range.end.column, line_text.size());
- // Render unstyled text before this styled range
- if (pos < start && pos < line_text.size()) {
- size_t len = std::min(start - pos, line_text.size() - pos);
- std::string unstyled = line_text.substr(pos, len);
- mvprintw(y + screen_y, screen_x, "%s", unstyled.c_str());
- screen_x += unstyled.length();
- pos = start;
- }
- // Render styled text
- if (pos < end && pos < line_text.size()) {
- size_t len = std::min(end - pos, line_text.size() - pos);
- std::string styled_text = line_text.substr(pos, len);
- // Apply color
- int attrs = get_attributes_for_face(styled.attr.face_name);
- attron(attrs);
- mvprintw(y + screen_y, screen_x, "%s", styled_text.c_str());
- attroff(attrs);
- screen_x += styled_text.length();
- pos = end;
- }
- }
- // Render remaining unstyled text
- if (pos < line_text.size()) {
- std::string remaining = line_text.substr(pos);
- if ((int)(screen_x - x - line_number_width + remaining.length()) > max_content_width) {
- remaining = remaining.substr(0, max_content_width - (screen_x - x - line_number_width) - 3) + "...";
- }
- mvprintw(y + screen_y, screen_x, "%s", remaining.c_str());
- }
- }
- }
- // Highlight ISearch match
- if (core_->minibuffer_manager().is_isearch_active() && core_->minibuffer_manager().get_isearch_match_range().has_value()) {
- auto match = core_->minibuffer_manager().get_isearch_match_range().value();
-
- // Check if current line is part of the match
- if (buffer_line_idx >= match.start.line && buffer_line_idx <= match.end.line) {
- int horizontal_offset = window->viewport().horizontal_offset;
-
- size_t hl_start = (buffer_line_idx == match.start.line) ? match.start.column : 0;
- size_t hl_end = (buffer_line_idx == match.end.line) ? match.end.column : line_text.length();
-
- // Handle case where match extends past end of line (e.g. capturing newline)
- // Ensure we don't go past line text length for string extraction
- size_t text_len = line_text.length();
- size_t safe_hl_end = std::min(hl_end, text_len);
- // Adjust for horizontal scroll
- int draw_start = static_cast<int>(hl_start) - horizontal_offset;
- int draw_end = static_cast<int>(safe_hl_end) - horizontal_offset;
-
- // Clip to viewable area
- draw_start = std::max(0, draw_start);
- // Clip draw_end to window width
- if (draw_end > content_width) draw_end = content_width;
- if (draw_start < draw_end) {
- // Extract text to highlight
- std::string text_to_hl = line_text.substr(draw_start + horizontal_offset, draw_end - draw_start);
-
- int screen_x = x + line_number_width + draw_start;
- int attrs = get_attributes_for_face(core_->minibuffer_manager().is_isearch_failed() ? "isearch-fail" : "isearch");
- if (attrs == 0) attrs = A_REVERSE;
-
- attron(attrs);
- mvprintw(y + screen_y, screen_x, "%s", text_to_hl.c_str());
- attroff(attrs);
- }
- }
- }
-
- // Show cursor if this is the cursor line and this is the active window
- if (buffer_line_idx == cursor.line && is_active && cursor_blink_.is_visible()) {
- int cursor_screen_x = x + line_number_width + cursor.column;
- if (cursor_screen_x < x + width) {
- char cursor_char = ' ';
- if (cursor.column < line_text.size()) {
- cursor_char = line_text[cursor.column];
- }
-
- // Force A_REVERSE to ensure visible block cursor
- int attrs = A_REVERSE;
-
- attron(attrs);
- mvaddch(y + screen_y, cursor_screen_x, cursor_char);
- attroff(attrs);
- }
- }
- // Track hardware cursor position for active window
- if (buffer_line_idx == cursor.line && is_active) {
- int cursor_screen_x = x + line_number_width + (int)cursor.column;
- if (cursor_screen_x < x + width) {
- hw_cursor_x_ = cursor_screen_x;
- hw_cursor_y_ = y + screen_y;
- }
- }
- }
-
- // Fill remaining lines (for empty lines below buffer) - no tildes
- size_t displayed_lines = std::min((size_t)content_height, end_line - start_line);
- for (int screen_y = displayed_lines; screen_y < content_height; ++screen_y) {
- move(y + screen_y, x);
- for (int i = 0; i < width; ++i) addch(' ');
- }
-
- // Render modeline for this window
- if (show_modeline) {
- render_window_modeline(window, x, y + content_height, width, is_active);
- }
- }
- void TuiEditor::render_window_modeline(std::shared_ptr<Window> window, int x, int y, int width, bool is_active) {
- const auto& buffer = window->buffer();
- const auto cursor = window->cursor();
-
- // Choose modeline colors
- std::string face_name = is_active ? "mode-line" : "mode-line-inactive";
- int attrs = get_attributes_for_face(face_name);
- if (attrs == 0) attrs = is_active ? A_REVERSE : A_DIM;
-
- attron(attrs);
-
- // Clear the modeline
- move(y, x);
- for (int i = 0; i < width; ++i) addch(' ');
-
- // Create modeline content
- std::string modeline;
-
- // Buffer name and modification status
- modeline += buffer.name();
- if (buffer.is_modified()) modeline += " [+] ";
-
- // Cursor position
- modeline += " | " + std::to_string(cursor.line + 1) + ":" + std::to_string(cursor.column + 1);
-
- // Major mode (if available)
- // TODO: Add major mode support when available
-
- // Right-aligned content (percentage through file)
- std::string right_side;
- if (buffer.line_count() > 0) {
- int percentage = (cursor.line * 100) / (buffer.line_count() - 1);
- right_side = " " + std::to_string(percentage) + "%";
- }
-
- // Truncate modeline if too long
- int available_width = width - right_side.length();
- if ((int)modeline.length() > available_width) {
- modeline = modeline.substr(0, available_width - 3) + "...";
- }
-
- // Render left side
- mvprintw(y, x, "%s", modeline.c_str());
-
- // Render right side
- if (!right_side.empty()) {
- mvprintw(y, x + width - right_side.length(), "%s", right_side.c_str());
- }
-
- // Turn off modeline attributes
- attroff(attrs);
- }
- void TuiEditor::render_message_line() {
- int msg_y = height_ - 1;
- int attrs = get_attributes_for_face("minibuffer-prompt");
-
- attron(attrs);
- move(msg_y, 0);
- clrtoeol();
- if (core_->minibuffer_manager().is_active()) {
- std::string prompt_part = core_->minibuffer_manager().get_prompt();
- std::string input_part = core_->minibuffer_manager().get_input_buffer();
- std::string display_text = prompt_part + input_part;
-
- mvprintw(msg_y, 0, "%s", display_text.c_str());
- // Display completion candidates below the input line (conceptually, but physically above if at bottom)
- auto candidates = core_->minibuffer_manager().get_completion_candidates();
- if (!candidates.empty() && msg_y > 0) {
- // Move up one line to display completions above the current minibuffer line
- move(msg_y - 1, 0);
- clrtoeol(); // Clear the line first to remove artifacts from underlying window
-
- std::string completion_display;
- for (size_t i = 0; i < candidates.size() && static_cast<int>(completion_display.length()) < width_ - 5; ++i) {
- if (!completion_display.empty()) completion_display += " ";
- completion_display += candidates[i].display_text;
- }
- if (static_cast<int>(completion_display.length()) >= width_ - 5) {
- completion_display = completion_display.substr(0, width_ - 8) + "...";
- }
-
- // Render with a different color/attribute if possible
- mvprintw(msg_y - 1, 0, "%s", completion_display.c_str());
- }
- // Explicitly place cursor at the correct position in minibuffer
- // prompt_part is handled by get_prompt(), cursor_position is relative to input_buffer
- move(msg_y, prompt_part.length() + core_->minibuffer_manager().get_cursor_position());
- } else if (core_->keybinding_manager().is_building_sequence()) {
- // Display partial key sequence (like Emacs "C-x-")
- std::string key_seq = core_->keybinding_manager().current_sequence_display() + "-";
- mvprintw(msg_y, 0, "%s", key_seq.c_str());
- } else if (!message_line_.empty()) {
- // Display transient message with truncation if needed
- std::string display_msg = message_line_;
- // Handle multi-line messages: show first line + indicator
- size_t newline_pos = display_msg.find('\n');
- if (newline_pos != std::string::npos) {
- // Count lines
- size_t line_count = 1;
- size_t pos = 0;
- while ((pos = display_msg.find('\n', pos)) != std::string::npos) {
- ++line_count;
- ++pos;
- }
- // Show first line with line count indicator
- display_msg = display_msg.substr(0, newline_pos) +
- " [+" + std::to_string(line_count - 1) + " lines, C-h e]";
- }
- const std::string suffix = "...[C-h e]";
- int available_width = width_ - 1;
- if (static_cast<int>(display_msg.length()) > available_width) {
- size_t max_chars = available_width - suffix.length();
- display_msg = display_msg.substr(0, max_chars) + suffix;
- }
- // Apply color based on severity
- int msg_attrs = attrs;
- switch (core_->last_message_severity()) {
- case MessageSeverity::Warning:
- msg_attrs |= COLOR_PAIR(3) | A_BOLD; // Yellow
- break;
- case MessageSeverity::Error:
- msg_attrs |= COLOR_PAIR(1) | A_BOLD; // Red
- break;
- default:
- break;
- }
- attron(msg_attrs);
- mvprintw(msg_y, 0, "%s", display_msg.c_str());
- attroff(msg_attrs);
- }
- attroff(attrs);
- // After rendering, check if it's time to clear the message.
- core_->check_and_clear_message();
- }
- namespace lumacs {
- std::unique_ptr<IEditorView> create_tui_editor() {
- return std::make_unique<TuiEditor>();
- }
- }
|