/** * @file output.c * @brief Screen rendering and output module for the Beluga text editor * @details Handles all screen updates, cursor positioning, status bar * rendering, and display synchronization using ANSI escape sequences */ #include "../include/output.h" #include "../include/append_buffer.h" #include "../include/buffer.h" #include "../include/editor_op.h" #include "../include/data.h" #include "../include/define.h" #include "../include/row_op.h" #include "../include/split_screen.h" #include "../include/syntax_highlighter.h" #include "../include/terminal.h" #include "../include/lsp_ui.h" #include #include #include #include #include "include/completion.h" #include "include/utils.h" /** * @brief Renders a single pane with its buffer content */ static void editorDrawPane(struct abuf* ab, EditorPane* pane) { int file_row; char pos_buf[32]; int pos_len; int byte_len_to_print; int bytes_to_print; char* highlighted; if (pane == NULL || pane->buffer_id < 0) return; const struct buffer_t* buf = bufferFindById(pane->buffer_id); if (buf == NULL) return; // Draw all pane for (int y = 0; y < pane->height; y++) { file_row = y + pane->y_offset; // Set cursor at start of pane row pos_len = snprintf(pos_buf, sizeof(pos_buf), "\x1b[%d;%dH", pane->origin_y + y + 1, pane->origin_x + 1); abAppend(ab, pos_buf, pos_len); // Apply background color (6 bytes for RGB format) abAppend(ab, E.theme.BACKGROUND_COLOR, (int)strlen(E.theme.BACKGROUND_COLOR)); // pane line is out of buffer if (file_row >= buf->numrows) { // Empty line - show tilde abAppend(ab, "~", 1); for (int i = 0; i < pane->width - 1; ++i) { abAppend(ab, " ", 1); } } else { if (E.constantes.LSP) { lspUiDrawGutter(ab, &E.lsp_diagnostics, pane->buffer_id, file_row); } if (buf->filename[strlen(buf->filename) - 1] == 'c' || buf->filename[strlen(buf->filename) - 1] == 'h') { // Render line with syntax highlighting, constrain to pane width highlighted = highlight_line( &buf->row[file_row].chars[pane->x_offset], &byte_len_to_print); // Print only up to pane width abAppend(ab, highlighted, byte_len_to_print); bFree(highlighted); } else { // Render basic line bytes_to_print = buf->row[file_row].size - pane->x_offset < pane->width ? buf->row[file_row].size - pane->x_offset : pane->width; abAppend(ab, &buf->row[file_row].chars[pane->x_offset], bytes_to_print); } // Fill remaining space with background color to pane width for (int i = 0; i < pane->width - editorRowCharCount(&buf->row[file_row], pane->width) + pane->x_offset; ++i) { abAppend(ab, " ", 1); } } } } /** * @brief Renders all panes based on current split configuration */ static void editorDrawAllPanes(struct abuf* ab) { const ScreenLayout* layout = splitScreenGetLayout(); if (layout->num_panes == 1) { // Single pane fullscreen editorDrawPane(ab, &layout->panes[0]); } else if (layout->num_panes == 2) { // Draw both panes for (int i = 0; i < 2; i++) { editorDrawPane(ab, &layout->panes[i]); // Draw pane border/divider if not the last pane if (layout->mode == SPLIT_VERTICAL && i == 0) { // Draw vertical divider int divider_col = layout->panes[0].width; for (int y = 0; y < layout->panes[0].height; y++) { char pos_buf[32]; snprintf(pos_buf, sizeof(pos_buf), "\x1b[%d;%dH", y + 1, divider_col); abAppend(ab, pos_buf, (int)strlen(pos_buf)); abAppend(ab, "\x1b[1m|\x1b[0m", 9); // Bold pipe divider } } else if (layout->mode == SPLIT_HORIZONTAL && i == 0) { // Draw horizontal divider int divider_row = layout->panes[0].height; char pos_buf[32]; snprintf(pos_buf, sizeof(pos_buf), "\x1b[%d;%dH", divider_row + 1, 1); abAppend(ab, pos_buf, (int)strlen(pos_buf)); for (int x = 0; x < E.screencols; x++) { abAppend(ab, "\x1b[1m-\x1b[0m", 9); // Bold dash divider } } } } } /** * @brief Updates scroll offsets to keep cursor visible on screen * @details Adjusts E.row_offset and E.col_offset to ensure the cursor remains * within the visible viewport. Also updates E.rx (rendered x-coordinate). * @note Updates global editor state E * @see editorRowCxToRx() */ void editorScroll() { EditorPane* active = splitScreenGetActivePane(); struct buffer_t* buf = bufferFindById(active->buffer_id); int rel_x, rel_y; // compute relative coordinates rel_x = editorRowCharCount(&buf->row[buf->y], buf->x); appDebug("counting %d\n", rel_x); rel_y = buf->y; appDebug("%d %d / %d %d\n", active->cursor_x, active->x_offset, active->cursor_y, active->y_offset); while (rel_x != active->cursor_x + active->x_offset || rel_y != active->cursor_y + active->y_offset) { if (rel_x < active->cursor_x + active->x_offset) { // LEFT if (active->cursor_x == 0 && active->x_offset) { active->x_offset--; } else { active->cursor_x--; } } else { // RIGHT if (rel_x > active->cursor_x + active->x_offset) { if (active->cursor_x == active->width - 1) { active->x_offset++; } else { active->cursor_x++; } } } if (rel_y < active->cursor_y + active->y_offset) { if (active->cursor_y == 0 && active->y_offset) { active->y_offset--; } else { active->cursor_y--; } } if (rel_y > active->cursor_y + active->y_offset) { if (active->cursor_y == active->height - 1) { active->y_offset++; } else { active->cursor_y++; } } } } char* basename(char* path) { int len = (int)strlen(path); for (int i = len - 1; i > 0; i--) { if (path[i] == '/') { path = path + i + 1; break; } } return path; } /** * @brief Renders the status bar at the bottom of the screen * @details Displays filename, line count, dirty flag, and current cursor * position in an inverted color bar. Right-aligns the cursor position * indicator. * @param ab Pointer to append buffer structure for accumulating output * @note Uses ANSI escape codes for color inversion */ void editorDrawStatusBar(struct abuf* ab) { // TO MODIFY int len, render_len; char status[E.screencols], render_status[E.screencols * 4]; EditorPane* active = splitScreenGetActivePane(); struct buffer_t* buf = bufferFindById(active->buffer_id); abAppend(ab, "\x1b[7m", 4); // inverting colors const char* mode_str = ""; ScreenLayout* layout = splitScreenGetLayout(); if (layout->mode == SPLIT_VERTICAL) mode_str = " [V-SPLIT]"; else if (layout->mode == SPLIT_HORIZONTAL) mode_str = " [H-SPLIT]"; // Build buffer status showing all buffers with dirty indicators char buf_status[1024] = ""; int offset = 0; for (int i = 0; i < E.number_of_buffer; i++) { struct buffer_t* b = &E.buffers[i]; char marker = (b->buffer_id == active->buffer_id) ? '>' : ' '; char dirty_marker = b->dirty ? '*' : ' '; offset += snprintf(&buf_status[offset], sizeof(buf_status) - offset, "%c%d:%s%c ", marker, b->buffer_id, b->filename ? basename(b->filename) : "[No Name]", dirty_marker); } len = snprintf(status, sizeof(status), "%s%s", buf_status, mode_str); render_len = snprintf(render_status, sizeof(render_status), "%d/%d", active->cursor_y + 1, buf->numrows); if (len > E.screencols) { len = E.screencols; } abAppend(ab, status, len); while (len < E.screencols) { if (E.screencols - len == render_len + 1) { abAppend(ab, render_status, render_len); break; } abAppend(ab, " ", 1); ++len; } abAppend(ab, "\x1b[m", 3); // normal text mode abAppend(ab, "\r\n", 2); } /** * @brief Renders the message bar below the status bar * @details Displays temporary status messages for a limited time (5 seconds). * Only displays message if within time window and within screen width. * @param ab Pointer to append buffer structure for accumulating output * @note Messages are set by editorSetStatusMessage() */ void editorDrawMessageBar(struct abuf* ab) { int msg_len = (int)strlen(E.status_msg); abAppend(ab, ERASE_END_LINE, 3); if (msg_len > E.screencols) { msg_len = E.screencols; } if (msg_len && time(NULL) - E.status_msg_time < 5) { abAppend(ab, E.status_msg, msg_len); } } /** * @brief Performs complete screen refresh and buffer synchronization * @details Clears screen, redraws all visible content (rows, status bar, * message bar), positions cursor, and writes accumulated buffer to stdout. This * is the main rendering function called each frame. * @note Updates global editor state E (via editorScroll()) * @see editorDrawRows() * @see editorDrawStatusBar() * @see editorDrawMessageBar() */ void editorRefreshScreen() { struct abuf ab = ABUF_INIT; char buf[32]; abAppend(&ab, HIDE_CURSOR, 6); abAppend(&ab, CURSOR_TOP_LEFT, 3); abAppend(&ab, E.theme.BACKGROUND_COLOR, (int)strlen(E.theme.BACKGROUND_COLOR)); editorScroll(); EditorPane* active = splitScreenGetActivePane(); struct buffer_t* buffer = bufferFindById(active->buffer_id); editorDrawAllPanes(&ab); if (E.constantes.LSP) { // ── LSP: draw completion popup every frame while visible ────────────────── appDebug("[REFRESH] lsp_completion.visible=%d\n", E.lsp_completion.visible); while (E.lsp_client->completion_requested && !E.lsp_client->completion_just_arrived); // reset flags E.lsp_client->completion_just_arrived = 0; E.lsp_client->completion_requested = 0; // ── LSP: diagnostic for current line in status bar ──────────────────────── const char* diag = lspUiDiagnosticAtCursor( &E.lsp_diagnostics, active->buffer_id, buffer->y); if (diag) { char single_line[512]; int i = 0; // Copy until newline, \0, or screen width while (diag[i] && diag[i] != '\n' && i < E.screencols - 4) { single_line[i] = diag[i]; i++; } // If message was truncated, add ellipsis if (diag[i] != '\0' && diag[i] != '\n') { single_line[i++] = '.'; single_line[i++] = '.'; single_line[i++] = '.'; } single_line[i] = '\0'; editorSetStatusMessage("● %s", single_line); } } editorDrawStatusBar(&ab); editorDrawMessageBar(&ab); if (E.constantes.LSP && (E.lsp_client && E.lsp_client->state == LSP_READY)) { lspUiDrawCompletion(&ab, &E.lsp_completion); } // ── Position cursor (account for gutter width) ──────────────────────────── snprintf(buf, sizeof(buf), "\x1b[%d;%dH", active->cursor_y + active->origin_y + 1, active->cursor_x + active->origin_x + 1 + (E.constantes.LSP ? GUTTER_WIDTH : 0)); abAppend(&ab, buf, (int)strlen(buf)); abAppend(&ab, SHOW_CURSOR, 6); write(STDOUT_FILENO, ab.b, ab.len); abFree(&ab); }