Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
workflow_dispatch:
pull_request:
push:
branches: [main]
tags: ['v*']

concurrency:
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ ifeq ($(TRIPLE),)
endif
endif

TESTS = amalg app array asset cli etc env format fmon glob ht io math process ps rb str sys thread time mem prompt leak qsort term
TESTS = amalg app array asset cli etc env format fmon glob ht io math process ps rb str sys thread time mem leak qsort term
BENCHES = glob heap
EXAMPLES = app array cargo cli format hash_table io zero_copy ls palette post prompt prompt_fancy serve signal tls wc
EXAMPLES = app array cli format hash_table io zero_copy ls palette post serve signal tls wc
EXAMPLES += fs/config fs/iterator fs/ls fs/roots fs/workspace
ifneq (,$(findstring wasm32,$(TRIPLE)))
EXAMPLES += fs/preopen
Expand Down
163 changes: 87 additions & 76 deletions sp/sp_prompt.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,16 @@
To use the library, you need (a) a context, and (b) to set up the terminal for drawing. In the common
case, you can do both with one call:

sp_prompt_ctx_t* ctx = sp_prompt_begin();
sp_prompt_ctx_t* ctx = sp_prompt_begin(mem);

This detects the size of the terminal, saves the current terminal state, and enters raw mode. If you
need more control over the order of these operations, or want a custom output size, do this:
This detects the size of the terminal, saves the current terminal state, and enters raw mode. Input is
read from stdin and the prompt is drawn to stderr, which leaves stdout free for your program's output.
If either one is not a terminal, you get SP_NULLPTR. To use a different terminal, pass its file
descriptors:

sp_prompt_ctx_t ctx = sp_zero; // Zero initialization is required
sp_prompt_init(&ctx, 69, 420);
sp_prompt_begin_ex(&ctx, 69, 420);
sp_prompt_ctx_t* ctx = sp_prompt_begin_ex(mem, (sp_prompt_config_t) {
.fds = { .in = in, .out = out },
});


### RUNNING A WIDGET
Expand Down Expand Up @@ -692,11 +694,21 @@ typedef struct {
} ex;
} sp_prompt_knight_rider_t;

typedef struct {
sp_sys_fd_t in;
sp_sys_fd_t out;
} sp_prompt_fds_t;

typedef struct {
sp_prompt_fds_t fds;
} sp_prompt_config_t;

/////////
// API //
/////////
// @lifecycle
sp_prompt_ctx_t* sp_prompt_begin(sp_mem_t mem);
sp_prompt_ctx_t* sp_prompt_begin_ex(sp_mem_t mem, sp_prompt_config_t config);
void sp_prompt_end(sp_prompt_ctx_t* ctx);

// @widgets
Expand Down Expand Up @@ -758,16 +770,16 @@ u32 sp_prompt_text_width(sp_str_t text);
sp_str_t sp_prompt_repeat(sp_prompt_ctx_t* ctx, u32 codepoint, u32 count);

// @advanced
sp_prompt_ctx_t* sp_prompt_new(sp_mem_t mem);
s32 sp_prompt_begin_ex(sp_prompt_ctx_t* ctx);
void sp_prompt_ctx_init(sp_prompt_ctx_t* ctx, sp_mem_t mem, u32 cols, u32 rows);
void sp_prompt_prime_events(sp_prompt_ctx_t* ctx, sp_prompt_event_t events[SP_PROMPT_PRIMED_EVENT_CAP]);
bool sp_prompt_run(sp_prompt_ctx_t* ctx, sp_prompt_widget_t widget);
sp_app_config_t sp_prompt_app(sp_prompt_ctx_t* ctx, sp_prompt_widget_t widget);
#endif


#if defined(SP_PROMPT_IMPLEMENTATION)
#if !defined(SP_PROMPT_IMPL_H)
#if defined(SP_PROMPT_PRIVATE_HEADER) || defined(SP_PROMPT_IMPLEMENTATION)
#define SP_PROMPT_IMPL_H

typedef struct {
u32 cursor;
Expand Down Expand Up @@ -866,7 +878,7 @@ struct sp_prompt_ctx_t {
sp_prompt_cell_t* framebuffer;
sp_da(sp_prompt_frame_t) frames;
struct {
struct { sp_sys_fd_t in; sp_sys_fd_t out; } fds;
sp_prompt_fds_t fds;
sp_sys_tty_state_t cache;
bool raw;
} terminal;
Expand Down Expand Up @@ -901,13 +913,15 @@ struct sp_prompt_ctx_t {
SP_PRIVATE void sp_prompt_wake(sp_prompt_ctx_t* ctx);
SP_PRIVATE sp_prompt_event_t sp_prompt_drain_stdin(sp_prompt_ctx_t* ctx);
SP_PRIVATE void sp_prompt_dispatch_event(sp_prompt_ctx_t* ctx, sp_prompt_widget_t widget, sp_prompt_event_t event);
SP_PRIVATE void sp_prompt_flush_log(sp_prompt_ctx_t* ctx);
SP_PRIVATE sp_prompt_config_t sp_prompt_config_resolve(sp_prompt_config_t config);

#endif
#endif

static s32 sp_prompt_enable_raw_mode(sp_prompt_ctx_t* ctx) {
if (sp_tty_set_mode(ctx->terminal.fds.in, ctx->terminal.fds.out, SP_SYS_TTY_MODE_RAW, &ctx->terminal.cache)) return -1;
ctx->terminal.raw = true;
return 0;
}
#ifndef SP_PROMPT_C
#if defined(SP_PROMPT_IMPLEMENTATION)
#define SP_PROMPT_C

static void sp_prompt_emit_bytes(sp_prompt_ctx_t* ctx, const void* ptr, u64 size) {
SP_ASSERT(ctx->tty.io);
Expand Down Expand Up @@ -971,37 +985,72 @@ static void sp_prompt_framebuffer_clear(sp_prompt_ctx_t* ctx) {
ctx->cursor_col = 0;
}

sp_prompt_ctx_t* sp_prompt_new(sp_mem_t mem) {
sp_prompt_ctx_t* ctx = sp_alloc_type(mem, sp_prompt_ctx_t);
sp_prompt_config_t sp_prompt_config_resolve(sp_prompt_config_t config) {
if (!config.fds.in) config.fds.in = sp_sys_stdin;
if (!config.fds.out) config.fds.out = sp_sys_stderr;
return config;
}

sp_prompt_ctx_t* sp_prompt_begin(sp_mem_t mem) {
return sp_prompt_begin_ex(mem, sp_zero_s(sp_prompt_config_t));
}

sp_prompt_ctx_t* sp_prompt_begin_ex(sp_mem_t mem, sp_prompt_config_t config) {
sp_prompt_fds_t fds = sp_prompt_config_resolve(config).fds;
if (!sp_sys_is_tty(fds.out)) return SP_NULLPTR;

sp_tty_color_t color = sp_tty_color_detect(fds.out);
sp_sys_tty_state_t cache = sp_zero;
if (sp_tty_set_mode(fds.in, fds.out, SP_SYS_TTY_MODE_RAW, &cache)) return SP_NULLPTR;

u32 cols = 0;
u32 rows = 0;
sp_sys_tty_size(sp_sys_stdout, &cols, &rows);
sp_sys_tty_size(fds.out, &cols, &rows);
if (cols == 0) cols = SP_PROMPT_DEFAULT_COLS;
if (rows == 0) rows = SP_PROMPT_DEFAULT_ROWS;

sp_prompt_ctx_t* ctx = sp_alloc_type(mem, sp_prompt_ctx_t);
sp_prompt_ctx_init(ctx, mem, cols, rows);
return ctx;
}
ctx->terminal.fds = fds;
ctx->terminal.cache = cache;
ctx->terminal.raw = true;

sp_prompt_ctx_t* sp_prompt_begin(sp_mem_t mem) {
sp_prompt_ctx_t* ctx = sp_prompt_new(mem);
if (sp_prompt_begin_ex(ctx)) {
sp_free(mem, ctx, sizeof(sp_prompt_ctx_t));
return SP_NULLPTR;
}
return ctx;
}
// Write buffering is really important, because our rendering algorithm is extremely
// naive. It's not much more than this:
//
// for row:
// for column:
// cell := cells[row][column]
// emit ANSI style for cell if it changed
// emit cell
//
// In other words, we call write(), one byte at a time. The simplicity of this approach
// is excellent, but you don't want the terminal emulator to try to re-render (M x N)
// times every single frame.
//
// Empirically, you get pretty bad tearing on Windows without buffering.
sp_io_stream_writer_t* fw = sp_mem_arena_alloc_type(ctx->arena, sp_io_stream_writer_t);
sp_io_stream_writer_from_fd(fw, fds.out, SP_IO_CLOSE_MODE_NONE);
ctx->tty = (sp_tty_t) {
.io = &fw->base,
.color = color,
};

s32 sp_prompt_begin_ex(sp_prompt_ctx_t* ctx) {
ctx->terminal.raw = false;
u64 buffer_size = ctx->cols * ctx->rows * SP_PROMPT_CELL_BUFFER_BYTES + SP_PROMPT_BUFFER_EXTRA_BYTES;
u8* buffer = sp_mem_arena_alloc_n(ctx->arena, u8, buffer_size);
sp_io_writer_set_buffer(ctx->tty.io, buffer, buffer_size);

if (sp_prompt_enable_raw_mode(ctx) == -1) return -1;
sp_sys_event_open(&ctx->wake.event);
sp_prompt_emit(ctx, SP_ANSI_HIDE_CURSOR);
sp_io_flush(ctx->tty.io);
return 0;
return ctx;
}

void sp_prompt_end(sp_prompt_ctx_t* ctx) {
sp_prompt_emit(ctx, "\r\n");
sp_prompt_flush_log(ctx);
sp_io_flush(ctx->tty.io);

if (ctx->terminal.raw) {
sp_prompt_emit(ctx, SP_ANSI_SHOW_CURSOR);
sp_io_flush(ctx->tty.io);
Expand All @@ -1014,22 +1063,6 @@ void sp_prompt_end(sp_prompt_ctx_t* ctx) {
ctx->wake.event.fd = SP_SYS_INVALID_FD;
}

if (ctx->terminal.fds.out != SP_SYS_INVALID_FD && ctx->terminal.fds.out != 0) {
sp_sys_write(ctx->terminal.fds.out, "\n", 1, SP_NULLPTR);
sp_mem_arena_marker_t s = sp_mem_begin_scratch();
sp_da(sp_str_t) pending = ctx->channel.log.pending[ctx->channel.log.active];
sp_da_for(pending, it) {
sp_da(sp_str_t) rows = sp_prompt_log_rows(s.mem, pending[it]);
sp_da_for(rows, row_it) {
sp_str_t row = rows[row_it];
if (row.len) {
sp_sys_write(ctx->terminal.fds.out, row.data, row.len, SP_NULLPTR);
}
sp_sys_write(ctx->terminal.fds.out, "\n", 1, SP_NULLPTR);
}
}
sp_mem_end_scratch(s);
}
sp_mutex_destroy(&ctx->channel.lock);
sp_mem_arena_destroy(ctx->channel.log.arenas[0]);
sp_mem_arena_destroy(ctx->channel.log.arenas[1]);
Expand All @@ -1043,8 +1076,10 @@ void sp_prompt_ctx_init(sp_prompt_ctx_t* ctx, sp_mem_t mem, u32 cols, u32 rows)
ctx->rows = rows;
ctx->state = SP_PROMPT_STATE_ACTIVE;
ctx->wake.event.fd = SP_SYS_INVALID_FD;
ctx->terminal.fds.in = sp_sys_stdin;
ctx->terminal.fds.out = sp_sys_stdout;
ctx->terminal.fds = (sp_prompt_fds_t) {
.in = SP_SYS_INVALID_FD,
.out = SP_SYS_INVALID_FD,
};
ctx->arena = sp_mem_arena_new(mem);
ctx->mem = sp_mem_arena_as_allocator(ctx->arena);
sp_da_init(ctx->mem, ctx->frames);
Expand All @@ -1056,31 +1091,6 @@ void sp_prompt_ctx_init(sp_prompt_ctx_t* ctx, sp_mem_t mem, u32 cols, u32 rows)
sp_da_init(sp_mem_arena_as_allocator(ctx->channel.arena), ctx->channel.log.pending[0]);
sp_da_init(sp_mem_arena_as_allocator(ctx->channel.arena), ctx->channel.log.pending[1]);

// Write buffering is really important, because our rendering algorithm is extremely
// naive. It's not much more than this:
//
// for row:
// for column:
// cell := cells[row][column]
// emit ANSI style for cell if it changed
// emit cell
//
// In other words, we call write(), one byte at a time. The simplicity of this approach
// is excellent, but you don't want the terminal emulator to try to re-render (M x N)
// times every single frame.
//
// Empirically, you get pretty bad tearing on Windows without buffering.
sp_io_stream_writer_t* fw = sp_mem_arena_alloc_type(ctx->arena, sp_io_stream_writer_t);
sp_io_stream_writer_from_fd(fw, ctx->terminal.fds.out, SP_IO_CLOSE_MODE_NONE);
ctx->tty = (sp_tty_t) {
.io = &fw->base,
.color = sp_tty_color_detect(ctx->terminal.fds.out),
};

u64 buffer_size = ctx->cols * ctx->rows * SP_PROMPT_CELL_BUFFER_BYTES + SP_PROMPT_BUFFER_EXTRA_BYTES;
u8* buffer = sp_mem_arena_alloc_n(ctx->arena, u8, buffer_size);
sp_io_writer_set_buffer(ctx->tty.io, buffer, buffer_size);

u32 cell_count = ctx->cols * ctx->rows;
if (ctx->framebuffer == SP_NULLPTR) {
ctx->framebuffer = sp_mem_arena_alloc_n(ctx->arena, sp_prompt_cell_t, cell_count);
Expand Down Expand Up @@ -1444,7 +1454,7 @@ static bool sp_prompt_has_pending_log(sp_prompt_ctx_t* ctx) {
return any;
}

static void sp_prompt_flush_log(sp_prompt_ctx_t* ctx) {
void sp_prompt_flush_log(sp_prompt_ctx_t* ctx) {
sp_mutex_lock(&ctx->channel.lock);
u32 retired = ctx->channel.log.active;
bool any = !sp_da_empty(ctx->channel.log.pending[retired]);
Expand Down Expand Up @@ -3457,3 +3467,4 @@ void sp_prompt_knight_rider(sp_prompt_ctx_t* ctx, sp_prompt_knight_rider_t confi
sp_prompt_run(ctx, sp_prompt_knight_rider_widget(ctx, config));
}
#endif
#endif
19 changes: 19 additions & 0 deletions spn.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ source = [
"test/http/*.c",
]

[[test]]
name = "promptt"
source = [
"test/main.c",
"test/prompt/*.c",
]

[[test]]
name = "tls"
source = [
Expand Down Expand Up @@ -105,6 +112,18 @@ source = ["example/msvc.c"]
name = "iterator"
source = ["example/fs/iterator.c"]

[[example]]
name = "prompt"
source = ["example/prompt.c"]

[[example]]
name = "prompt_fancy"
source = ["example/prompt_fancy.c"]

[[example]]
name = "cargo"
source = ["example/cargo.c"]

[profile.sanitizer]
linkage = "shared"
sanitize = ["address", "undefined"]
Expand Down
1 change: 0 additions & 1 deletion test/amalg.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
#include "io.c"
#include "math.c"
#include "mem.c"
#include "prompt.c"
#include "ps.c"
#include "rb.c"
#include "str.c"
Expand Down
Loading
Loading