diff --git a/example/c-vt-render/README.md b/example/c-vt-render/README.md index deb4db388..3725ed46f 100644 --- a/example/c-vt-render/README.md +++ b/example/c-vt-render/README.md @@ -1,7 +1,9 @@ # Example: `ghostty-vt` Render State -This contains a simple example of how to use the `ghostty-vt` render-state API -to create a render state, update it from terminal content, and clean it up. +This contains an example of how to use the `ghostty-vt` render-state API +to create a render state, update it from terminal content, iterate rows +and cells, read styles and colors, inspect cursor state, and manage dirty +tracking. This uses a `build.zig` and `Zig` to build the C program so that we can reuse a lot of our build logic and depend directly on our source diff --git a/example/c-vt-render/src/main.c b/example/c-vt-render/src/main.c index b5c22edf5..0714d4160 100644 --- a/example/c-vt-render/src/main.c +++ b/example/c-vt-render/src/main.c @@ -1,16 +1,34 @@ #include +#include #include #include #include -//! [render-state-update] +/// Helper: resolve a style color to an RGB value using the palette. +static GhosttyColorRgb resolve_color(GhosttyStyleColor color, + const GhosttyRenderStateColors* colors, + GhosttyColorRgb fallback) { + switch (color.tag) { + case GHOSTTY_STYLE_COLOR_RGB: + return color.value.rgb; + case GHOSTTY_STYLE_COLOR_PALETTE: + return colors->palette[color.value.palette]; + default: + return fallback; + } +} + int main(void) { GhosttyResult result; + //! [render-state-update] + // Create a terminal and render state, then update the render state + // from the terminal. The render state captures a snapshot of everything + // needed to draw a frame. GhosttyTerminal terminal = NULL; GhosttyTerminalOptions terminal_opts = { - .cols = 80, - .rows = 24, + .cols = 40, + .rows = 5, .max_scrollback = 10000, }; result = ghostty_terminal_new(NULL, &terminal, terminal_opts); @@ -20,26 +38,197 @@ int main(void) { result = ghostty_render_state_new(NULL, &render_state); assert(result == GHOSTTY_SUCCESS); - const char* first_frame = "first frame\r\n"; + // Feed some styled content into the terminal. + const char* content = + "Hello, \033[1;32mworld\033[0m!\r\n" // bold green "world" + "\033[4munderlined\033[0m text\r\n" // underlined text + "\033[38;2;255;128;0morange\033[0m\r\n"; // 24-bit orange fg ghostty_terminal_vt_write( - terminal, - (const uint8_t*)first_frame, - strlen(first_frame)); + terminal, (const uint8_t*)content, strlen(content)); + result = ghostty_render_state_update(render_state, terminal); assert(result == GHOSTTY_SUCCESS); + //! [render-state-update] - const char* second_frame = "second frame\r\n"; - ghostty_terminal_vt_write( - terminal, - (const uint8_t*)second_frame, - strlen(second_frame)); - result = ghostty_render_state_update(render_state, terminal); + //! [render-dirty-check] + // Check the global dirty state to decide how much work the renderer + // needs to do. After rendering, reset it to false. + GhosttyRenderStateDirty dirty; + result = ghostty_render_state_get( + render_state, GHOSTTY_RENDER_STATE_DATA_DIRTY, &dirty); assert(result == GHOSTTY_SUCCESS); - printf("Render state was updated successfully.\n"); + switch (dirty) { + case GHOSTTY_RENDER_STATE_DIRTY_FALSE: + printf("Frame is clean, nothing to draw.\n"); + break; + case GHOSTTY_RENDER_STATE_DIRTY_PARTIAL: + printf("Partial redraw needed.\n"); + break; + case GHOSTTY_RENDER_STATE_DIRTY_FULL: + printf("Full redraw needed.\n"); + break; + } + //! [render-dirty-check] + //! [render-colors] + // Retrieve colors (background, foreground, palette) from the render + // state. These are needed to resolve palette-indexed cell colors. + GhosttyRenderStateColors colors = + GHOSTTY_INIT_SIZED(GhosttyRenderStateColors); + result = ghostty_render_state_colors_get(render_state, &colors); + assert(result == GHOSTTY_SUCCESS); + + printf("Background: #%02x%02x%02x\n", + colors.background.r, colors.background.g, colors.background.b); + printf("Foreground: #%02x%02x%02x\n", + colors.foreground.r, colors.foreground.g, colors.foreground.b); + //! [render-colors] + + //! [render-cursor] + // Read cursor position and visual style from the render state. + bool cursor_visible = false; + ghostty_render_state_get( + render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE, + &cursor_visible); + + bool cursor_in_viewport = false; + ghostty_render_state_get( + render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE, + &cursor_in_viewport); + + if (cursor_visible && cursor_in_viewport) { + uint16_t cx, cy; + ghostty_render_state_get( + render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X, &cx); + ghostty_render_state_get( + render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y, &cy); + + GhosttyRenderStateCursorVisualStyle style; + ghostty_render_state_get( + render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE, + &style); + + const char* style_name = "unknown"; + switch (style) { + case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BAR: + style_name = "bar"; + break; + case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK: + style_name = "block"; + break; + case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_UNDERLINE: + style_name = "underline"; + break; + case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK_HOLLOW: + style_name = "hollow"; + break; + } + printf("Cursor at (%u, %u), style: %s\n", cx, cy, style_name); + } + //! [render-cursor] + + //! [render-row-iterate] + // Iterate rows via the row iterator. For each dirty row, iterate its + // cells, read codepoints/graphemes and styles, and emit ANSI-colored + // output as a simple "renderer". + GhosttyRenderStateRowIterator row_iter = NULL; + result = ghostty_render_state_row_iterator_new(NULL, &row_iter); + assert(result == GHOSTTY_SUCCESS); + + result = ghostty_render_state_get( + render_state, GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR, &row_iter); + assert(result == GHOSTTY_SUCCESS); + + GhosttyRenderStateRowCells cells = NULL; + result = ghostty_render_state_row_cells_new(NULL, &cells); + assert(result == GHOSTTY_SUCCESS); + + int row_index = 0; + while (ghostty_render_state_row_iterator_next(row_iter)) { + // Check per-row dirty state; a real renderer would skip clean rows. + bool row_dirty = false; + ghostty_render_state_row_get( + row_iter, GHOSTTY_RENDER_STATE_ROW_DATA_DIRTY, &row_dirty); + + printf("Row %2d [%s]: ", row_index, + row_dirty ? "dirty" : "clean"); + + // Get cells for this row (reuses the same cells handle). + result = ghostty_render_state_row_get( + row_iter, GHOSTTY_RENDER_STATE_ROW_DATA_CELLS, &cells); + assert(result == GHOSTTY_SUCCESS); + + while (ghostty_render_state_row_cells_next(cells)) { + // Get the grapheme length; 0 means the cell is empty. + uint32_t grapheme_len = 0; + ghostty_render_state_row_cells_get( + cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN, + &grapheme_len); + + if (grapheme_len == 0) { + putchar(' '); + continue; + } + + // Read the style for this cell. Returns the default style for + // cells that have no explicit styling. + GhosttyStyle style = GHOSTTY_INIT_SIZED(GhosttyStyle); + ghostty_render_state_row_cells_get( + cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE, &style); + + // Resolve foreground color for this cell. + GhosttyColorRgb fg = + resolve_color(style.fg_color, &colors, colors.foreground); + + // Emit ANSI true-color escape for the foreground. + printf("\033[38;2;%u;%u;%um", fg.r, fg.g, fg.b); + if (style.bold) printf("\033[1m"); + if (style.underline) printf("\033[4m"); + + // Read grapheme codepoints into a buffer and print them. + // The buffer must be at least grapheme_len elements. + uint32_t codepoints[16]; + uint32_t len = grapheme_len < 16 ? grapheme_len : 16; + ghostty_render_state_row_cells_get( + cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF, + codepoints); + + for (uint32_t i = 0; i < len; i++) { + // Simple ASCII print; a real renderer would handle UTF-8. + if (codepoints[i] < 128) + putchar((char)codepoints[i]); + else + printf("U+%04X", codepoints[i]); + } + + printf("\033[0m"); // Reset style after each cell. + } + + printf("\n"); + + // Clear per-row dirty flag after "rendering" it. + bool clean = false; + ghostty_render_state_row_set( + row_iter, GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY, &clean); + + row_index++; + } + //! [render-row-iterate] + + //! [render-dirty-reset] + // After finishing the frame, reset the global dirty state so the next + // update can report changes accurately. + GhosttyRenderStateDirty clean_state = GHOSTTY_RENDER_STATE_DIRTY_FALSE; + result = ghostty_render_state_set( + render_state, GHOSTTY_RENDER_STATE_OPTION_DIRTY, &clean_state); + assert(result == GHOSTTY_SUCCESS); + //! [render-dirty-reset] + + // Cleanup + ghostty_render_state_row_cells_free(cells); + ghostty_render_state_row_iterator_free(row_iter); ghostty_render_state_free(render_state); ghostty_terminal_free(terminal); return 0; } -//! [render-state-update] diff --git a/include/ghostty/vt/render.h b/include/ghostty/vt/render.h index bf79cdd7e..d8f6400a9 100644 --- a/include/ghostty/vt/render.h +++ b/include/ghostty/vt/render.h @@ -58,10 +58,26 @@ extern "C" { * reset the row-level dirty flags. So, the caller of the render state API must * be careful to manage both layers of dirty state correctly. * - * ## Example + * ## Examples * + * ### Creating and updating render state * @snippet c-vt-render/src/main.c render-state-update * + * ### Checking dirty state + * @snippet c-vt-render/src/main.c render-dirty-check + * + * ### Reading colors + * @snippet c-vt-render/src/main.c render-colors + * + * ### Reading cursor state + * @snippet c-vt-render/src/main.c render-cursor + * + * ### Iterating rows and cells + * @snippet c-vt-render/src/main.c render-row-iterate + * + * ### Resetting dirty state after rendering + * @snippet c-vt-render/src/main.c render-dirty-reset + * * @{ */ diff --git a/src/terminal/c/render.zig b/src/terminal/c/render.zig index 3feb4c9fb..b7f2fca0a 100644 --- a/src/terminal/c/render.zig +++ b/src/terminal/c/render.zig @@ -435,7 +435,7 @@ pub const RowCellsData = enum(c_int) { .raw => page.Cell.C, .style => style_c.Style, .graphemes_len => u32, - .graphemes_buf => [*]u32, + .graphemes_buf => u32, }; } }; @@ -472,7 +472,10 @@ fn rowCellsGetTyped( switch (data) { .invalid => return .invalid_value, .raw => out.* = cell.cval(), - .style => out.* = style_c.Style.fromStyle(cells.styles[x]), + .style => out.* = if (cell.hasStyling()) + style_c.Style.fromStyle(cells.styles[x]) + else + style_c.Style.fromStyle(.{}), .graphemes_len => { if (!cell.hasText()) { out.* = 0; @@ -484,11 +487,10 @@ fn rowCellsGetTyped( .graphemes_buf => { if (!cell.hasText()) return .success; const extra = if (cell.hasGrapheme()) cells.graphemes[x] else &[_]u21{}; - const total = 1 + extra.len; - const out_slice = out.*[0..total]; - out_slice[0] = cell.codepoint(); + const buf: [*]u32 = @ptrCast(out); + buf[0] = cell.codepoint(); for (extra, 1..) |cp, i| { - out_slice[i] = cp; + buf[i] = cp; } }, }