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
2 changes: 1 addition & 1 deletion .claude
25 changes: 24 additions & 1 deletion USER_DOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ What is live at every render, and what is not:
| `$((…))` | yes |
| `$(…)` and backquotes | **no** — the text stays on screen as written; it would fork on every redraw |

**Preview caveat:** `print -P` formats its expanded input again using zsh's
prompt escapes. If a variable contains `%` sequences, `print -rP "[${V}]"`
can interpret them as prompt escapes even though the live prompt leaves the
expanded value literal. Do not treat `print -P` output as an exact preview of
variable-backed prompt segments; this mismatch is tracked in [issue #134](https://github.com/Univers42/hellish/issues/134).

For a computed segment, set a variable from a hook and put the variable in
the prompt. `HELLISH_PRECMD_FUNCS` runs once before each prompt
(`hellishrc.example`, section 5b):
Expand Down Expand Up @@ -201,7 +207,24 @@ newest history line that starts with what you have typed appears dim after
the cursor. It is a hint, not part of the line: **Right**, **End** or
**Ctrl-F** at the end of the line insert it; anywhere else, and when there
is no hint, those keys do what they always did. Enter runs only what you
typed, and the hint is wiped from the screen. `unset HELLISH_AUTOSUGGEST`
typed, and the hint is wiped from the screen.

| key | with a hint | without one |
|---|---|---|
| Right, End, Ctrl-F | insert all of it | forward-char / end-of-line |
| Alt-F, Ctrl-Right | insert its next word; the rest stays as the hint | forward-word |
| Alt-N | show the next older, different matching line | non-incremental forward search |
| Alt-P | go back to the newer one shown before | non-incremental reverse search |

A word is the blanks at the front of the hint plus the run of characters up
to the next space or tab (so `-m`, `a/b`, `x=1` are one word each). Alt-N
and Alt-P step through distinct lines (same full text offered once), look
at no more than `HELLISH_SUGGEST_MAX` entries, ring the bell at the oldest
and the newest end instead of wrapping, and start over from the newest
match as soon as the line is edited. Right after Alt-N inserts the line
shown, not the newest. A hint a zle widget set as `POSTDISPLAY` is not
cycled or taken by words. In vi insert mode Alt-F / Alt-N / Alt-P work the
same (no Ctrl-Right). `unset HELLISH_AUTOSUGGEST`
(or `=0`) turns it off at the next prompt. A key you bound yourself
(`bind` / `bindkey` / `.inputrc`) is never taken over.

Expand Down
12 changes: 11 additions & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ FROM ${BASE}
# in was a red rung on a commit the rung had built fine an hour earlier.
# A mirror that keeps failing four times still fails the build -- it is
# the transient, not the broken, that is retried.
# One repo is the exception: when openSUSE-Tumbleweed-Update still will not
# refresh after the retries, it is disabled. A plain install can live with a
# stale repo, but `zypper dup` refuses to run while ANY enabled repo failed
# to refresh, so one unreachable mirror for that repo (which only carries
# rare urgent patches; Tumbleweed itself ships through Oss) turned the
# fallback into a guaranteed red rung after #169 merged, 30 min of
# "Curl error (28)" with the code never compiled. modifyrepo takes the
# alias (repo-update), not the display name; no `|| true`, so an image that
# renames the alias fails loudly instead of silently disabling nothing.
# apt gets the same loop, update included: an Ubuntu mirror mid-sync serves
# an index that disagrees with its files ("File has unexpected size ...
# Mirror sync in progress?", or a 404 on a .deb the index already moved
Expand Down Expand Up @@ -74,7 +83,8 @@ RUN set -eux; \
elif command -v zypper >/dev/null 2>&1; then \
zretry() { n=0; until "$@"; do n=$((n + 1)); \
[ "$n" -ge 4 ] && return 1; sleep 20; done; }; \
zretry zypper --non-interactive --gpg-auto-import-keys refresh || true; \
zretry zypper --non-interactive --gpg-auto-import-keys refresh \
|| zypper --non-interactive modifyrepo --disable repo-update; \
pkgs="gcc clang make readline-devel ncurses-devel git-core bash \
python3 tar gzip which findutils diffutils"; \
if ! zretry zypper --non-interactive install $pkgs; then \
Expand Down
4 changes: 4 additions & 0 deletions incs/prompt.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,14 @@ typedef struct s_rl
char *sg_ent; /* history entry the suggestion comes from */
char *sg_prev; /* the line sg_ent was searched for */
bool sg_known; /* sg_prev / sg_ent describe a finished scan */
size_t sg_hn; /* history length that scan (and any cycle) saw */
char *sg_txt; /* ghost for the line on screen, or NULL */
char *sg_post; /* POSTDISPLAY a widget set (zle_rl5.c) */
char *sg_post_at; /* the line it was set for */
char *sg_pub; /* what a widget was handed as POSTDISPLAY */
t_vec sg_cyc; /* history indices offered (size_t) */
size_t sg_cyc_k; /* which of them is on screen */
char *sg_cyc_line; /* line the cycle is for; NULL: none */
bool sg_painted; /* a ghost is on screen right now */
int sg_col; /* column it starts at */
/* --- RPROMPT: rendered here, painted by the editor (rl_rprompt.c) --- */
Expand Down
18 changes: 16 additions & 2 deletions src/infrastructure/rl_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ void rl_preinit(t_rl *l);
(rl_slot.c). A slot is one key sequence in one keymap; `fn` names the
stock function it holds and the one that replaces it. */
# define RL_NFEAT 2
# define RL_NSLOTS 17
# define RL_NSLOTS 25

enum e_feat
{
Expand All @@ -60,7 +60,10 @@ enum e_slot_fn
SLOT_PREV,
SLOT_NEXT,
SLOT_FWD,
SLOT_END
SLOT_END,
SLOT_WORD,
SLOT_OLDER,
SLOT_NEWER
};

typedef struct s_slot
Expand All @@ -72,6 +75,9 @@ typedef struct s_slot
} t_slot;

t_slot rl_slot_at(int i);
t_slot rl_slot_extra(int j);
void rl_slot_funcs(const t_slot *s, rl_command_func_t **stock,
rl_command_func_t **repl);
void rl_slot_sync(t_shell *state);
void rl_slot_pre(t_rl *l);
void rl_slot_post(t_rl *l);
Expand All @@ -93,6 +99,14 @@ char *rl_suggest_update(t_shell *state, int cols);
char *rl_suggest_live(t_rl *l);
char *rl_suggest_find(t_shell *state, t_rl *l);
int rl_suggest_accept(int count, int key);
t_shell *rl_suggest_ghost_state(void);
int rl_suggest_bell(void);
int rl_suggest_word(int count, int key);
int rl_suggest_older(int count, int key);
int rl_suggest_newer(int count, int key);
ssize_t rl_suggest_scan(t_shell *state, t_rl *l, size_t from);
void rl_suggest_cyc_clear(t_rl *l);
bool rl_suggest_cyc_sync(t_shell *state, t_rl *l);
void rl_suggest_end(t_shell *state);
void rl_suggest_stale(void);
void rl_suggest_unpaint(void);
Expand Down
49 changes: 0 additions & 49 deletions src/platform/posix/rl_slot.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,55 +20,6 @@
** again (slot_user). The arrows come in two encodings (CSI and the
** application-cursor SS3 form) and two keymaps, hence the rows. */

/* The whole table, by index: 0-7 are the history keys (Up / Down, in the
CSI and SS3 encodings, emacs then vi), 8-12 the emacs accept keys and
13-16 the vi ones (which have no ^F). `vi` picks vi_insertion_keymap
over the emacs one. */
t_slot rl_slot_at(int i)
{
static const char *keys[] = {"\033[A", "\033[B", "\033OA", "\033OB",
"\033[C", "\033OC", "\006", "\033[F", "\033OF"};
t_slot s;
int k;
int j;

s.feat = FEAT_HIST;
s.vi = i / 4;
s.key = keys[i % 4];
s.fn = SLOT_PREV + i % 2;
if (i < 8)
return (s);
j = i - 8;
k = 4 + j;
s.feat = FEAT_ACCEPT;
s.vi = j > 4;
if (s.vi)
k = j - 1 + (j > 6);
s.key = keys[k];
s.fn = SLOT_FWD + (k > 6);
return (s);
}

/* The stock function of a slot, and what replaces it while its feature
is on. */
static void rl_slot_funcs(const t_slot *s, rl_command_func_t **stock,
rl_command_func_t **repl)
{
*stock = rl_get_previous_history;
*repl = rl_history_search_backward;
if (s->fn == SLOT_NEXT)
{
*stock = rl_get_next_history;
*repl = rl_history_search_forward;
}
if (s->fn == SLOT_FWD || s->fn == SLOT_END)
*repl = rl_suggest_accept;
if (s->fn == SLOT_FWD)
*stock = rl_forward_char;
if (s->fn == SLOT_END)
*stock = rl_end_of_line;
}

/* A marker no user can bind: a slot still holding it after the user's
bind lines ran was not touched by them. */
static int rl_slot_mark(int count, int key)
Expand Down
89 changes: 89 additions & 0 deletions src/platform/posix/rl_slot_tab.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* rl_slot_tab.c :+: :+: :+: */
/* +:+ +:+ +:+ */
/* By: dlesieur <dlesieur@student.42.fr> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/10/17 12:00:00 by dlesieur #+# #+# */
/* Updated: 2026/10/17 12:00:00 by dlesieur ### ########.fr */
/* */
/* ************************************************************************** */

#include "rl_private.h"

/* The table of slots (rl_slot.c holds the logic that binds them). */

/* The whole table, by index: 0-7 are the history keys (Up / Down, in the
CSI and SS3 encodings, emacs then vi), 8-12 the emacs accept keys and
13-16 the vi ones (which have no ^F); 17-24 are the word and cycle keys
(rl_slot_extra). `vi` picks vi_insertion_keymap over the emacs one. */
t_slot rl_slot_at(int i)
{
static const char *keys[] = {"\033[A", "\033[B", "\033OA", "\033OB",
"\033[C", "\033OC", "\006", "\033[F", "\033OF"};
t_slot s;
int k;
int j;

if (i >= 17)
return (rl_slot_extra(i - 17));
s.feat = FEAT_HIST;
s.vi = i / 4;
s.key = keys[i % 4];
s.fn = SLOT_PREV + i % 2;
if (i < 8)
return (s);
j = i - 8;
k = 4 + j;
s.feat = FEAT_ACCEPT;
s.vi = j > 4;
if (s.vi)
k = j - 1 + (j > 6);
s.key = keys[k];
s.fn = SLOT_FWD + (k > 6);
return (s);
}

/* Slots 17-24: Alt-F, Ctrl-Right (two encodings) take a word of the
ghost, Alt-N / Alt-P cycle it -- 0-4 in the emacs keymap, 5-7 (Alt-F,
Alt-N, Alt-P) in vi insert mode. `\033O5C` is left alone: readline has
nothing on it, so there is no stock function to give back. */
t_slot rl_slot_extra(int j)
{
static const char *keys[] = {"\033f", "\033[1;5C", "\033[5C",
"\033n", "\033p"};
t_slot s;
int k;

k = j;
if (j == 5)
k = 0;
if (j > 5)
k = j - 3;
s.feat = FEAT_ACCEPT;
s.vi = j > 4;
s.key = keys[k];
s.fn = SLOT_WORD + (k > 2) + (k > 3);
return (s);
}

/* The stock function of a slot, and what replaces it while its feature
is on. In vi insert mode the Alt keys have no stock function (ESC
leaves insert mode), so there the slot is unbound while off. */
void rl_slot_funcs(const t_slot *s, rl_command_func_t **stock,
rl_command_func_t **repl)
{
static rl_command_func_t *const stk[] = {rl_get_previous_history,
rl_get_next_history, rl_forward_char, rl_end_of_line,
rl_forward_word, rl_noninc_forward_search,
rl_noninc_reverse_search};
static rl_command_func_t *const rep[] = {rl_history_search_backward,
rl_history_search_forward, rl_suggest_accept, rl_suggest_accept,
rl_suggest_word, rl_suggest_older, rl_suggest_newer};

*stock = stk[s->fn];
*repl = rep[s->fn];
if (s->vi && s->fn >= SLOT_WORD)
*stock = NULL;
}
65 changes: 22 additions & 43 deletions src/platform/posix/rl_suggest.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,45 +23,30 @@
** is a C1 control in its UTF-8 form: running `printf '\e[31m'` once must
** not repaint every later prompt. */

/* True when `s` holds nothing a terminal would interpret. 0x7f is DEL; the
pair 0xC2 0x80..0x9F is U+0080..U+009F, which some terminals read as a
control sequence introducer. */
static bool sg_clean(const unsigned char *s)
/* The newest of the last sg_max entries that starts with the first `n`
bytes of the line, is longer than it and is clean (rl_suggest_scan). */
static char *sg_scan(t_shell *state, t_rl *l)
{
size_t i;
ssize_t i;

i = 0;
while (s[i])
{
if (s[i] < 0x20 || s[i] == 0x7f)
return (false);
if (s[i] == 0xc2 && s[i + 1] >= 0x80 && s[i + 1] < 0xa0)
return (false);
i++;
}
return (true);
i = rl_suggest_scan(state, l, state->hist.hist_cmds.len);
if (i < 0)
return (NULL);
return (((char **)state->hist.hist_cmds.ctx)[i]);
}

/* The newest of the last sg_max entries that starts with the first `n`
bytes of the line, is longer than it and is clean. A bounded look keeps
a huge history from slowing a keystroke down. */
static char *sg_scan(t_shell *state, t_rl *l, size_t n)
/* True when the last scan still answers for the line: the line extends the
one it was for and the entry still matches (or there was none). */
static bool sg_cached(t_rl *l, size_t n)
{
char **a;
size_t i;
size_t seen;
size_t p;

a = (char **)state->hist.hist_cmds.ctx;
i = state->hist.hist_cmds.len;
seen = 0;
while (i > 0 && seen++ < (size_t)l->sg_max)
{
i--;
if (a[i] && !ft_strncmp(a[i], rl_line_buffer, n)
&& ft_strlen(a[i]) > n && sg_clean((unsigned char *)a[i]))
return (a[i]);
}
return (NULL);
if (!l->sg_known)
return (false);
p = ft_strlen(l->sg_prev);
return (p <= n && !ft_strncmp(rl_line_buffer, l->sg_prev, p)
&& (!l->sg_ent || (ft_strlen(l->sg_ent) > n
&& !ft_strncmp(l->sg_ent, rl_line_buffer, n))));
}

/* The entry to suggest for the line, or NULL.
Expand All @@ -75,26 +60,20 @@ static char *sg_scan(t_shell *state, t_rl *l, size_t n)
** it: it only grows between reads, and the cache is dropped per read. */
char *rl_suggest_find(t_shell *state, t_rl *l)
{
size_t n;
size_t p;
char *e;

n = (size_t)rl_end;
p = 0;
if (l->sg_known)
p = ft_strlen(l->sg_prev);
if (l->sg_known && p <= n && !ft_strncmp(rl_line_buffer, l->sg_prev, p)
&& (!l->sg_ent || (ft_strlen(l->sg_ent) > n
&& !ft_strncmp(l->sg_ent, rl_line_buffer, n))))
rl_suggest_cyc_sync(state, l);
if (sg_cached(l, (size_t)rl_end))
return (l->sg_ent);
xfree(l->sg_prev);
xfree(l->sg_ent);
e = sg_scan(state, l, n);
e = sg_scan(state, l);
l->sg_ent = NULL;
if (e)
l->sg_ent = ft_strdup(e);
l->sg_prev = ft_strdup(rl_line_buffer);
l->sg_known = true;
l->sg_hn = state->hist.hist_cmds.len;
return (l->sg_ent);
}

Expand Down
6 changes: 5 additions & 1 deletion src/platform/posix/rl_suggest2.c
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,18 @@ char *rl_suggest_update(t_shell *state, int cols)

/* What the accept keys may insert right now: the ghost of the last frame,
if the line still is what it was drawn for. Keys can arrive faster than
frames (a paste), so the entry is matched against the line again. */
frames (a paste), so the entry is matched against the line again -- and
an alternative shown by a cycle belongs to the line it was cycled for,
not to a longer one typed since. */
char *rl_suggest_live(t_rl *l)
{
size_t n;

n = (size_t)rl_end;
if (!l->sg_txt || rl_point != rl_end)
return (NULL);
if (l->sg_cyc_line && ft_strcmp(l->sg_cyc_line, rl_line_buffer))
return (NULL);
if (l->sg_post && !ft_strcmp(l->sg_post_at, rl_line_buffer))
return (l->sg_post);
if (!l->sg_post && l->sg_ent && ft_strlen(l->sg_ent) > n
Expand Down
1 change: 1 addition & 0 deletions src/platform/posix/rl_suggest4.c
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ void rl_suggest_end(t_shell *state)
xfree(l->sg_post);
xfree(l->sg_post_at);
xfree(l->sg_pub);
rl_suggest_cyc_clear(l);
l->sg_ent = NULL;
l->sg_prev = NULL;
l->sg_post = NULL;
Expand Down
Loading
Loading