Skip to content
Open
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
5 changes: 5 additions & 0 deletions include/slash/slash.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ struct slash {
* for instance, typing: "w<TAB>g<TAB>s<TAB>" 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 */
};

/**
Expand All @@ -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)
Expand Down
56 changes: 56 additions & 0 deletions include/slash/statusline.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#pragma once

#include <slash/slash.h>

#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.
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doxygen comment has a formatting typo (* * @note) and also references slash_statusline_set_impl, but the exposed symbol is _slash_statusline_set_impl. Fixing this will keep generated docs accurate and avoid confusion for API consumers.

Suggested change
* * @note This is a macro that wraps slash_statusline_set_impl to allow inline struct initialization.
* @note This is a macro that wraps _slash_statusline_set_impl to allow inline struct initialization.

Copilot uses AI. Check for mistakes.
*/
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);
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ slash_sources = files([
'src/completer.c',
'src/optparse.c',
'src/slash_list.c',
'src/statusline.c',
])

if get_option('builtins')
Expand Down
114 changes: 110 additions & 4 deletions src/slash.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include <slash/slash.h>
#include <slash/optparse.h>
#include <slash/completer.h>
#include <slash/statusline.h>

#include <dlfcn.h>
#include <stdio.h>
Expand All @@ -48,6 +49,10 @@
#include <sys/select.h>
#endif

#ifdef SLASH_HAVE_TERMIOS_H
#include <sys/ioctl.h>
#endif

#include "builtins.h"

/* Terminal codes */
Expand Down Expand Up @@ -137,31 +142,92 @@ 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));
Comment on lines +152 to +162
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rows - 1 is used to build the scroll region and prompt row escape sequences. If ws.ws_row is 0/1, this generates an invalid/negative row and can corrupt terminal state. Add a guard (e.g., require rows >= 2) and avoid enabling the statusline when the terminal is too small or row count is unavailable.

Copilot uses AI. Check for mistakes.

// 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;

return 0;
}

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;
Expand All @@ -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)
Expand Down Expand Up @@ -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) {
Comment on lines +886 to +889
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the last statusline item is removed (count becomes 0), slash_refresh() stops rendering the statusline but never resets the scroll region / clears the reserved bottom row. This can leave the terminal permanently scrolled to rows-1 until exit. Consider deactivating (reset scroll region + clear status row) when slash_statusline_count() == 0 and statusline_enabled is still true.

Copilot uses AI. Check for mistakes.
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 */
Comment on lines +891 to +905
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slash_refresh() recurses unconditionally after calling slash_statusline_activate(). If slash_statusline_activate() returns early (e.g., ioctl(TIOCGWINSZ) fails), statusline_enabled stays false and this becomes infinite recursion / stack overflow. Consider making slash_statusline_activate() report success (e.g., return bool/int) and only re-run refresh when activation actually succeeded; otherwise continue without statusline.

Suggested change
/* 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 */
/* Activation moved cursor; re-run refresh only if it succeeded */
if (slash->statusline_enabled)
return slash_refresh(slash, printtime);
}
if (slash->statusline_enabled) {
struct winsize ws;
if (ioctl(slash->fd_write, TIOCGWINSZ, &ws) == 0 &&
ws.ws_row != slash->statusline_rows) {
slash_statusline_activate(slash);
/* Re-run refresh only if activation updated the tracked row */
if (slash->statusline_enabled &&
ws.ws_row == slash->statusline_rows)
return slash_refresh(slash, printtime);
}
if (slash->statusline_enabled) {
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 */
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +891 to +905
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resize-handling branch also calls slash_statusline_activate() and immediately recurses into slash_refresh(). If activation fails (or terminal reports 0 rows), this can recurse indefinitely. Gate the recursive call on successful activation, or fall back to disabling the statusline for that refresh cycle.

Suggested change
/* 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 */
if (slash->statusline_enabled && slash->statusline_rows > 0) {
/* Activation moved cursor; re-run refresh at new position */
return slash_refresh(slash, printtime);
}
/* Activation failed or produced no usable row; skip statusline for this cycle */
slash->statusline_enabled = false;
} else {
struct winsize ws;
if (ioctl(slash->fd_write, TIOCGWINSZ, &ws) == 0 &&
ws.ws_row != slash->statusline_rows) {
slash_statusline_activate(slash);
if (slash->statusline_enabled && slash->statusline_rows > 0) {
return slash_refresh(slash, printtime);
}
/* Re-activation failed or terminal reported no usable rows */
slash->statusline_enabled = false;
}
}
if (slash->statusline_enabled && slash->statusline_rows > 0) {
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 */
}

Copilot uses AI. Check for mistakes.
}
#endif

return 0;
}

Expand Down Expand Up @@ -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 <stdlib.h>
char *slash_readline(struct slash *slash)
{
Expand Down Expand Up @@ -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);
Comment on lines +1257 to +1258
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sets an ERROR statusline for any non-SLASH_SUCCESS return, including SLASH_EXIT (and potentially other non-error control codes). That can incorrectly display "FAILED" for a normal exit. Consider only treating negative return codes as failures, or explicitly excluding SLASH_EXIT (and any other non-error codes) from this branch.

Suggested change
if(ret != SLASH_SUCCESS) {
slash_statusline_set("slash", {.type = SLASH_STATUS_ERROR},"FAILED: %s", line);
if (ret < 0) {
slash_statusline_set("slash", {.type = SLASH_STATUS_ERROR}, "FAILED: %s", line);

Copilot uses AI. Check for mistakes.
}
#endif
Comment on lines +1256 to +1260
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The #ifdef SLASH_HAVE_TERMIOS_H block is indented inside the function body, while the rest of the file consistently places preprocessor directives at column 0 (e.g., src/slash.c:44, :48). Aligning directives to column 0 improves readability and matches the existing style.

Suggested change
#ifdef SLASH_HAVE_TERMIOS_H
if(ret != SLASH_SUCCESS) {
slash_statusline_set("slash", {.type = SLASH_STATUS_ERROR},"FAILED: %s", line);
}
#endif
#ifdef SLASH_HAVE_TERMIOS_H
if(ret != SLASH_SUCCESS) {
slash_statusline_set("slash", {.type = SLASH_STATUS_ERROR},"FAILED: %s", line);
}
#endif

Copilot uses AI. Check for mistakes.
if (ret == SLASH_EXIT)
break;
}
Expand Down Expand Up @@ -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);
}
Expand Down
Loading
Loading