From e8197546802f75df7cac72418edbd24ae3531b86 Mon Sep 17 00:00:00 2001 From: edvard Date: Fri, 27 Mar 2026 10:22:44 +0100 Subject: [PATCH] statusline api implemented --- include/slash/slash.h | 5 ++ include/slash/statusline.h | 56 ++++++++++++++++++ meson.build | 1 + src/slash.c | 114 ++++++++++++++++++++++++++++++++++-- src/statusline.c | 117 +++++++++++++++++++++++++++++++++++++ 5 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 include/slash/statusline.h create mode 100644 src/statusline.c diff --git a/include/slash/slash.h b/include/slash/slash.h index b2002a0..6938376 100644 --- a/include/slash/slash.h +++ b/include/slash/slash.h @@ -189,6 +189,10 @@ struct slash { * for instance, typing: "wgs" would result in the completed command line "watch get serial0" in 6 keystrokes */ bool complete_in_completion; + + /* Statusline */ + bool statusline_enabled; /* scroll region is active for statusline */ + int statusline_rows; /* terminal rows when scroll region was last set */ }; /** @@ -207,6 +211,7 @@ void slash_destroy(struct slash *slash); char *slash_readline(struct slash *slash); void slash_sigint(struct slash *slash, int signum); +void slash_sigwinch(struct slash *slash); /** * @brief Implement this function to do something with the current line (logging, etc) diff --git a/include/slash/statusline.h b/include/slash/statusline.h new file mode 100644 index 0000000..2ebbbd5 --- /dev/null +++ b/include/slash/statusline.h @@ -0,0 +1,56 @@ +#pragma once + +#include + +#define SLASH_STATUSLINE_MAX_ITEMS 16 +#define SLASH_STATUSLINE_KEY_SIZE 32 +#define SLASH_STATUSLINE_TEXT_SIZE 128 + +typedef enum { + SLASH_STATUS_NORMAL = 0, + SLASH_STATUS_WARNING, + SLASH_STATUS_ERROR +} slash_status_type_t; + +typedef struct { + slash_status_type_t type; + //int timeout_sec; + //bool blink; +} slash_status_opts_t; + +/** + * @brief Set or update a statusline item by key with formatting and options. + * If key already exists, update its text and options. Otherwise add a new item. + * + * @param key Identifier string (max 31 chars). + * @param opts_brace Configuration options wrapped in curly braces (e.g., { .type = SLASH_STATUS_ERROR }). + * Pass {0} to use default options (NORMAL type, no timeout). + * @param format Printf-style format string for the display text (rendered max 127 chars). + * @param ... Variadic arguments matching the format string. + * @return 0 on success, -1 if no space available. + * * @note This is a macro that wraps slash_statusline_set_impl to allow inline struct initialization. + */ +int _slash_statusline_set_impl(const char *key, const slash_status_opts_t *opts, const char *format, ...) __attribute__((format(printf, 3, 4))); + +#define slash_statusline_set(key, opts_brace, ...) \ + _slash_statusline_set_impl(key, &(slash_status_opts_t)opts_brace, __VA_ARGS__) + +/** + * @brief Remove a statusline item by key. + * @param key Identifier string to remove + * @return 0 on success, -1 if key not found + */ +int slash_statusline_remove(const char *key); + +/** + * @brief Get the number of active statusline items. + * @return count of active items + */ +int slash_statusline_count(void); + +/** + * @brief Render the statusline content to the terminal. + * Called internally by slash_refresh(). Not normally called by users. + * @param slash Slash context (for slash_write) + */ +void slash_statusline_render(struct slash *slash); diff --git a/meson.build b/meson.build index d8966ef..ad4dc80 100644 --- a/meson.build +++ b/meson.build @@ -6,6 +6,7 @@ slash_sources = files([ 'src/completer.c', 'src/optparse.c', 'src/slash_list.c', + 'src/statusline.c', ]) if get_option('builtins') diff --git a/src/slash.c b/src/slash.c index 15d4e60..6677a00 100644 --- a/src/slash.c +++ b/src/slash.c @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -48,6 +49,10 @@ #include #endif +#ifdef SLASH_HAVE_TERMIOS_H +#include +#endif + #include "builtins.h" /* Terminal codes */ @@ -137,16 +142,77 @@ static int slash_rawmode_disable(struct slash *slash) return 0; } +static void slash_statusline_activate(struct slash *slash) +{ +#ifdef SLASH_HAVE_TERMIOS_H + struct winsize ws; + if (ioctl(slash->fd_write, TIOCGWINSZ, &ws) == -1) + return; + + int rows = ws.ws_row; + char esc[32]; + + //Set scroll region to all rows except the last. + //DECSTBM resets cursor to (1,1) per VT100 spec. + snprintf(esc, sizeof(esc), "\033[1;%dr", rows - 1); + slash_write(slash, esc, strlen(esc)); + + // Move cursor to bottom of scroll region (prompt row) + snprintf(esc, sizeof(esc), "\033[%d;1H", rows - 1); + slash_write(slash, esc, strlen(esc)); + + // Draw statusline on the reserved bottom row + slash_write(slash, "\0337", 2); // DEC save cursor + snprintf(esc, sizeof(esc), "\033[%d;1H", rows); + slash_write(slash, esc, strlen(esc)); + slash_statusline_render(slash); + slash_write(slash, "\0338", 2); // DEC restore cursor + + slash->statusline_enabled = true; + slash->statusline_rows = rows; +#endif +} + +static void slash_statusline_deactivate(struct slash *slash) +{ +#ifdef SLASH_HAVE_TERMIOS_H + if (!slash->statusline_enabled) + return; + + char esc[32]; + + // Save cursor so we can restore after scroll region reset + slash_write(slash, "\0337", 2); + + // Clear the statusline row + snprintf(esc, sizeof(esc), "\033[%d;1H\033[K", slash->statusline_rows); + slash_write(slash, esc, strlen(esc)); + + // Reset scroll region to full terminal (moves cursor to 1,1) + slash_write(slash, "\033[r", 3); + + // Restore cursor to where it was + slash_write(slash, "\0338", 2); + + slash->statusline_enabled = false; +#endif +} + static int slash_configure_term(struct slash *slash) { if (slash_rawmode_enable(slash) < 0) return -ENOTTY; + if (slash_statusline_count() > 0) + slash_statusline_activate(slash); + return 0; } static int slash_restore_term(struct slash *slash) { + slash_statusline_deactivate(slash); + if (slash_rawmode_disable(slash) < 0) return -ENOTTY; @@ -154,14 +220,14 @@ static int slash_restore_term(struct slash *slash) } static int slash_wait_select(void *slashp, unsigned int ms); -void slash_acquire_std_in_out(struct slash *slash) { +void slash_acquire_std_in_out(struct slash *slash) { slash_configure_term(slash); #ifdef SLASH_HAVE_SELECT slash->waitfunc = slash_wait_select; #endif } -void slash_release_std_in_out(struct slash *slash) { +void slash_release_std_in_out(struct slash *slash) { slash_restore_term(slash); #ifdef SLASH_HAVE_SELECT slash->waitfunc = NULL; @@ -175,7 +241,11 @@ int slash_write(struct slash *slash, const char *buf, size_t count) static int slash_read(struct slash *slash, void *buf, size_t count) { - return read(slash->fd_read, buf, count); + int ret; + do { + ret = read(slash->fd_read, buf, count); + } while (ret < 0 && errno == EINTR); + return ret; } int slash_putchar(struct slash *slash, char c) @@ -813,6 +883,29 @@ int slash_refresh(struct slash *slash, int printtime) if (slash_write(slash, esc, strlen(esc)) < 0) return -1; + /* Update statusline on the fixed bottom row */ +#ifdef SLASH_HAVE_TERMIOS_H + if (slash_statusline_count() > 0) { + if (!slash->statusline_enabled) { + slash_statusline_activate(slash); + /* Activation moved cursor; re-run refresh at new position */ + return slash_refresh(slash, printtime); + } + struct winsize ws; + if (ioctl(slash->fd_write, TIOCGWINSZ, &ws) == 0 && + ws.ws_row != slash->statusline_rows) { + slash_statusline_activate(slash); + return slash_refresh(slash, printtime); + } + char pos[16]; + slash_write(slash, "\0337", 2); /* DEC save cursor */ + snprintf(pos, sizeof(pos), "\033[%d;1H", slash->statusline_rows); + slash_write(slash, pos, strlen(pos)); + slash_statusline_render(slash); + slash_write(slash, "\0338", 2); /* DEC restore cursor */ + } +#endif + return 0; } @@ -918,17 +1011,24 @@ static void slash_swap(struct slash *slash) void slash_clear_screen(struct slash *slash) { const char *esc = ESCAPE("H") ESCAPE("2J"); slash_write(slash, esc, strlen(esc)); + /* Force statusline scroll region re-setup on next refresh */ + slash->statusline_enabled = false; } void slash_sigint(struct slash *slash, int signum) { if (slash->busy) { slash->signal = signum; } else { - slash_reset(slash); + slash_reset(slash); slash_refresh(slash, 0); } } +void slash_sigwinch(struct slash *slash) { + slash->statusline_enabled = false; /* force scroll region re-setup */ + slash_refresh(slash, 0); +} + #include char *slash_readline(struct slash *slash) { @@ -1153,6 +1253,11 @@ int slash_loop(struct slash *slash) if (!slash_line_empty(line, strlen(line))) { /* Run command */ ret = slash_execute(slash, line); + #ifdef SLASH_HAVE_TERMIOS_H + if(ret != SLASH_SUCCESS) { + slash_statusline_set("slash", {.type = SLASH_STATUS_ERROR},"FAILED: %s", line); + } + #endif if (ret == SLASH_EXIT) break; } @@ -1239,6 +1344,7 @@ void slash_create_static(struct slash *slash, char * line_buf, size_t line_size, slash->cmd_list = 0; slash->complete_in_completion = true; + slash->statusline_enabled = false; tcgetattr(slash->fd_read, &slash->original); } diff --git a/src/statusline.c b/src/statusline.c new file mode 100644 index 0000000..c8a3f95 --- /dev/null +++ b/src/statusline.c @@ -0,0 +1,117 @@ +#include + +#include +#include +#include +#include + +// Base theme: White text on Orange background +#define STATUS_COLOR_NORMAL "\033[0;38;5;255;48;5;202m" + +// Warning: Bold Yellow text on Orange background +#define STATUS_COLOR_WARNING "\033[1;38;5;11;48;5;202m" + +// Error: Bold White text on Red background +#define STATUS_COLOR_ERROR "\033[1;38;5;15;48;5;196m" + +struct slash_statusline_item { + char key[SLASH_STATUSLINE_KEY_SIZE]; + char text[SLASH_STATUSLINE_TEXT_SIZE]; + bool active; + slash_status_type_t type; +}; + +static struct slash_statusline_item items[SLASH_STATUSLINE_MAX_ITEMS]; +static int item_count = 0; + +int _slash_statusline_set_impl(const char * key, const slash_status_opts_t * opts, const char * format, ...) { + va_list args; + + // Search for existing key + for (int i = 0; i < SLASH_STATUSLINE_MAX_ITEMS; i++) { + if (items[i].active && strncmp(items[i].key, key, SLASH_STATUSLINE_KEY_SIZE) == 0) { + + items[i].type = opts->type; + + va_start(args, format); + vsnprintf(items[i].text, SLASH_STATUSLINE_TEXT_SIZE, format, args); + va_end(args); + return 0; + } + } + + // Find empty slot + for (int i = 0; i < SLASH_STATUSLINE_MAX_ITEMS; i++) { + if (!items[i].active) { + strncpy(items[i].key, key, SLASH_STATUSLINE_KEY_SIZE - 1); + items[i].key[SLASH_STATUSLINE_KEY_SIZE - 1] = '\0'; + + items[i].type = opts->type; + + va_start(args, format); + vsnprintf(items[i].text, SLASH_STATUSLINE_TEXT_SIZE, format, args); + va_end(args); + + items[i].active = true; + item_count++; + return 0; + } + } + + return -1; +} + +int slash_statusline_remove(const char * key) { + for (int i = 0; i < SLASH_STATUSLINE_MAX_ITEMS; i++) { + if (items[i].active && strncmp(items[i].key, key, SLASH_STATUSLINE_KEY_SIZE) == 0) { + items[i].active = false; + items[i].key[0] = '\0'; + items[i].text[0] = '\0'; + item_count--; + return 0; + } + } + return -1; +} + +int slash_statusline_count(void) { + return item_count; +} + +void slash_statusline_render(struct slash * slash) { + if (item_count == 0) { + slash_write(slash, "\033[K", 3); + return; + } + + slash_write(slash, STATUS_COLOR_NORMAL, strlen(STATUS_COLOR_NORMAL)); + + bool first = true; + for (int i = 0; i < SLASH_STATUSLINE_MAX_ITEMS; i++) { + if (!items[i].active) + continue; + + if (!first) { + slash_write(slash, " \xe2\x94\x83 ", 5); + } else { + slash_write(slash, " ", 1); + } + + if (items[i].type == SLASH_STATUS_WARNING) { + slash_write(slash, STATUS_COLOR_WARNING, strlen(STATUS_COLOR_WARNING)); + } else if (items[i].type == SLASH_STATUS_ERROR) { + slash_write(slash, STATUS_COLOR_ERROR, strlen(STATUS_COLOR_ERROR)); + } + + slash_write(slash, items[i].text, strlen(items[i].text)); + + if (items[i].type != SLASH_STATUS_NORMAL) { + slash_write(slash, STATUS_COLOR_NORMAL, strlen(STATUS_COLOR_NORMAL)); + } + + first = false; + } + + // Fill rest of line with background color then hard reset + slash_write(slash, " \033[K\033[0m", 8); +}