Skip to content

Commit 1dd5ae5

Browse files
authored
Merge pull request #177 from Univers42/develop
Develop
2 parents eb62861 + db8f4e3 commit 1dd5ae5

16 files changed

Lines changed: 825 additions & 102 deletions

File tree

‎.claude‎

Submodule .claude updated 56 files

‎USER_DOC.md‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ What is live at every render, and what is not:
114114
| `$((…))` | yes |
115115
| `$(…)` and backquotes | **no** — the text stays on screen as written; it would fork on every redraw |
116116

117+
**Preview caveat:** `print -P` formats its expanded input again using zsh's
118+
prompt escapes. If a variable contains `%` sequences, `print -rP "[${V}]"`
119+
can interpret them as prompt escapes even though the live prompt leaves the
120+
expanded value literal. Do not treat `print -P` output as an exact preview of
121+
variable-backed prompt segments; this mismatch is tracked in [issue #134](https://github.com/Univers42/hellish/issues/134).
122+
117123
For a computed segment, set a variable from a hook and put the variable in
118124
the prompt. `HELLISH_PRECMD_FUNCS` runs once before each prompt
119125
(`hellishrc.example`, section 5b):
@@ -201,7 +207,24 @@ newest history line that starts with what you have typed appears dim after
201207
the cursor. It is a hint, not part of the line: **Right**, **End** or
202208
**Ctrl-F** at the end of the line insert it; anywhere else, and when there
203209
is no hint, those keys do what they always did. Enter runs only what you
204-
typed, and the hint is wiped from the screen. `unset HELLISH_AUTOSUGGEST`
210+
typed, and the hint is wiped from the screen.
211+
212+
| key | with a hint | without one |
213+
|---|---|---|
214+
| Right, End, Ctrl-F | insert all of it | forward-char / end-of-line |
215+
| Alt-F, Ctrl-Right | insert its next word; the rest stays as the hint | forward-word |
216+
| Alt-N | show the next older, different matching line | non-incremental forward search |
217+
| Alt-P | go back to the newer one shown before | non-incremental reverse search |
218+
219+
A word is the blanks at the front of the hint plus the run of characters up
220+
to the next space or tab (so `-m`, `a/b`, `x=1` are one word each). Alt-N
221+
and Alt-P step through distinct lines (same full text offered once), look
222+
at no more than `HELLISH_SUGGEST_MAX` entries, ring the bell at the oldest
223+
and the newest end instead of wrapping, and start over from the newest
224+
match as soon as the line is edited. Right after Alt-N inserts the line
225+
shown, not the newest. A hint a zle widget set as `POSTDISPLAY` is not
226+
cycled or taken by words. In vi insert mode Alt-F / Alt-N / Alt-P work the
227+
same (no Ctrl-Right). `unset HELLISH_AUTOSUGGEST`
205228
(or `=0`) turns it off at the next prompt. A key you bound yourself
206229
(`bind` / `bindkey` / `.inputrc`) is never taken over.
207230

‎docker/Dockerfile‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ FROM ${BASE}
4444
# in was a red rung on a commit the rung had built fine an hour earlier.
4545
# A mirror that keeps failing four times still fails the build -- it is
4646
# the transient, not the broken, that is retried.
47+
# One repo is the exception: when openSUSE-Tumbleweed-Update still will not
48+
# refresh after the retries, it is disabled. A plain install can live with a
49+
# stale repo, but `zypper dup` refuses to run while ANY enabled repo failed
50+
# to refresh, so one unreachable mirror for that repo (which only carries
51+
# rare urgent patches; Tumbleweed itself ships through Oss) turned the
52+
# fallback into a guaranteed red rung after #169 merged, 30 min of
53+
# "Curl error (28)" with the code never compiled. modifyrepo takes the
54+
# alias (repo-update), not the display name; no `|| true`, so an image that
55+
# renames the alias fails loudly instead of silently disabling nothing.
4756
# apt gets the same loop, update included: an Ubuntu mirror mid-sync serves
4857
# an index that disagrees with its files ("File has unexpected size ...
4958
# Mirror sync in progress?", or a 404 on a .deb the index already moved
@@ -74,7 +83,8 @@ RUN set -eux; \
7483
elif command -v zypper >/dev/null 2>&1; then \
7584
zretry() { n=0; until "$@"; do n=$((n + 1)); \
7685
[ "$n" -ge 4 ] && return 1; sleep 20; done; }; \
77-
zretry zypper --non-interactive --gpg-auto-import-keys refresh || true; \
86+
zretry zypper --non-interactive --gpg-auto-import-keys refresh \
87+
|| zypper --non-interactive modifyrepo --disable repo-update; \
7888
pkgs="gcc clang make readline-devel ncurses-devel git-core bash \
7989
python3 tar gzip which findutils diffutils"; \
8090
if ! zretry zypper --non-interactive install $pkgs; then \

‎incs/prompt.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,14 @@ typedef struct s_rl
103103
char *sg_ent; /* history entry the suggestion comes from */
104104
char *sg_prev; /* the line sg_ent was searched for */
105105
bool sg_known; /* sg_prev / sg_ent describe a finished scan */
106+
size_t sg_hn; /* history length that scan (and any cycle) saw */
106107
char *sg_txt; /* ghost for the line on screen, or NULL */
107108
char *sg_post; /* POSTDISPLAY a widget set (zle_rl5.c) */
108109
char *sg_post_at; /* the line it was set for */
109110
char *sg_pub; /* what a widget was handed as POSTDISPLAY */
111+
t_vec sg_cyc; /* history indices offered (size_t) */
112+
size_t sg_cyc_k; /* which of them is on screen */
113+
char *sg_cyc_line; /* line the cycle is for; NULL: none */
110114
bool sg_painted; /* a ghost is on screen right now */
111115
int sg_col; /* column it starts at */
112116
/* --- RPROMPT: rendered here, painted by the editor (rl_rprompt.c) --- */

‎src/infrastructure/rl_private.h‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ void rl_preinit(t_rl *l);
4747
(rl_slot.c). A slot is one key sequence in one keymap; `fn` names the
4848
stock function it holds and the one that replaces it. */
4949
# define RL_NFEAT 2
50-
# define RL_NSLOTS 17
50+
# define RL_NSLOTS 25
5151

5252
enum e_feat
5353
{
@@ -60,7 +60,10 @@ enum e_slot_fn
6060
SLOT_PREV,
6161
SLOT_NEXT,
6262
SLOT_FWD,
63-
SLOT_END
63+
SLOT_END,
64+
SLOT_WORD,
65+
SLOT_OLDER,
66+
SLOT_NEWER
6467
};
6568

6669
typedef struct s_slot
@@ -72,6 +75,9 @@ typedef struct s_slot
7275
} t_slot;
7376

7477
t_slot rl_slot_at(int i);
78+
t_slot rl_slot_extra(int j);
79+
void rl_slot_funcs(const t_slot *s, rl_command_func_t **stock,
80+
rl_command_func_t **repl);
7581
void rl_slot_sync(t_shell *state);
7682
void rl_slot_pre(t_rl *l);
7783
void rl_slot_post(t_rl *l);
@@ -93,6 +99,14 @@ char *rl_suggest_update(t_shell *state, int cols);
9399
char *rl_suggest_live(t_rl *l);
94100
char *rl_suggest_find(t_shell *state, t_rl *l);
95101
int rl_suggest_accept(int count, int key);
102+
t_shell *rl_suggest_ghost_state(void);
103+
int rl_suggest_bell(void);
104+
int rl_suggest_word(int count, int key);
105+
int rl_suggest_older(int count, int key);
106+
int rl_suggest_newer(int count, int key);
107+
ssize_t rl_suggest_scan(t_shell *state, t_rl *l, size_t from);
108+
void rl_suggest_cyc_clear(t_rl *l);
109+
bool rl_suggest_cyc_sync(t_shell *state, t_rl *l);
96110
void rl_suggest_end(t_shell *state);
97111
void rl_suggest_stale(void);
98112
void rl_suggest_unpaint(void);

‎src/platform/posix/rl_slot.c‎

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -20,55 +20,6 @@
2020
** again (slot_user). The arrows come in two encodings (CSI and the
2121
** application-cursor SS3 form) and two keymaps, hence the rows. */
2222

23-
/* The whole table, by index: 0-7 are the history keys (Up / Down, in the
24-
CSI and SS3 encodings, emacs then vi), 8-12 the emacs accept keys and
25-
13-16 the vi ones (which have no ^F). `vi` picks vi_insertion_keymap
26-
over the emacs one. */
27-
t_slot rl_slot_at(int i)
28-
{
29-
static const char *keys[] = {"\033[A", "\033[B", "\033OA", "\033OB",
30-
"\033[C", "\033OC", "\006", "\033[F", "\033OF"};
31-
t_slot s;
32-
int k;
33-
int j;
34-
35-
s.feat = FEAT_HIST;
36-
s.vi = i / 4;
37-
s.key = keys[i % 4];
38-
s.fn = SLOT_PREV + i % 2;
39-
if (i < 8)
40-
return (s);
41-
j = i - 8;
42-
k = 4 + j;
43-
s.feat = FEAT_ACCEPT;
44-
s.vi = j > 4;
45-
if (s.vi)
46-
k = j - 1 + (j > 6);
47-
s.key = keys[k];
48-
s.fn = SLOT_FWD + (k > 6);
49-
return (s);
50-
}
51-
52-
/* The stock function of a slot, and what replaces it while its feature
53-
is on. */
54-
static void rl_slot_funcs(const t_slot *s, rl_command_func_t **stock,
55-
rl_command_func_t **repl)
56-
{
57-
*stock = rl_get_previous_history;
58-
*repl = rl_history_search_backward;
59-
if (s->fn == SLOT_NEXT)
60-
{
61-
*stock = rl_get_next_history;
62-
*repl = rl_history_search_forward;
63-
}
64-
if (s->fn == SLOT_FWD || s->fn == SLOT_END)
65-
*repl = rl_suggest_accept;
66-
if (s->fn == SLOT_FWD)
67-
*stock = rl_forward_char;
68-
if (s->fn == SLOT_END)
69-
*stock = rl_end_of_line;
70-
}
71-
7223
/* A marker no user can bind: a slot still holding it after the user's
7324
bind lines ran was not touched by them. */
7425
static int rl_slot_mark(int count, int key)

‎src/platform/posix/rl_slot_tab.c‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/* ************************************************************************** */
2+
/* */
3+
/* ::: :::::::: */
4+
/* rl_slot_tab.c :+: :+: :+: */
5+
/* +:+ +:+ +:+ */
6+
/* By: dlesieur <dlesieur@student.42.fr> +#+ +:+ +#+ */
7+
/* +#+#+#+#+#+ +#+ */
8+
/* Created: 2026/10/17 12:00:00 by dlesieur #+# #+# */
9+
/* Updated: 2026/10/17 12:00:00 by dlesieur ### ########.fr */
10+
/* */
11+
/* ************************************************************************** */
12+
13+
#include "rl_private.h"
14+
15+
/* The table of slots (rl_slot.c holds the logic that binds them). */
16+
17+
/* The whole table, by index: 0-7 are the history keys (Up / Down, in the
18+
CSI and SS3 encodings, emacs then vi), 8-12 the emacs accept keys and
19+
13-16 the vi ones (which have no ^F); 17-24 are the word and cycle keys
20+
(rl_slot_extra). `vi` picks vi_insertion_keymap over the emacs one. */
21+
t_slot rl_slot_at(int i)
22+
{
23+
static const char *keys[] = {"\033[A", "\033[B", "\033OA", "\033OB",
24+
"\033[C", "\033OC", "\006", "\033[F", "\033OF"};
25+
t_slot s;
26+
int k;
27+
int j;
28+
29+
if (i >= 17)
30+
return (rl_slot_extra(i - 17));
31+
s.feat = FEAT_HIST;
32+
s.vi = i / 4;
33+
s.key = keys[i % 4];
34+
s.fn = SLOT_PREV + i % 2;
35+
if (i < 8)
36+
return (s);
37+
j = i - 8;
38+
k = 4 + j;
39+
s.feat = FEAT_ACCEPT;
40+
s.vi = j > 4;
41+
if (s.vi)
42+
k = j - 1 + (j > 6);
43+
s.key = keys[k];
44+
s.fn = SLOT_FWD + (k > 6);
45+
return (s);
46+
}
47+
48+
/* Slots 17-24: Alt-F, Ctrl-Right (two encodings) take a word of the
49+
ghost, Alt-N / Alt-P cycle it -- 0-4 in the emacs keymap, 5-7 (Alt-F,
50+
Alt-N, Alt-P) in vi insert mode. `\033O5C` is left alone: readline has
51+
nothing on it, so there is no stock function to give back. */
52+
t_slot rl_slot_extra(int j)
53+
{
54+
static const char *keys[] = {"\033f", "\033[1;5C", "\033[5C",
55+
"\033n", "\033p"};
56+
t_slot s;
57+
int k;
58+
59+
k = j;
60+
if (j == 5)
61+
k = 0;
62+
if (j > 5)
63+
k = j - 3;
64+
s.feat = FEAT_ACCEPT;
65+
s.vi = j > 4;
66+
s.key = keys[k];
67+
s.fn = SLOT_WORD + (k > 2) + (k > 3);
68+
return (s);
69+
}
70+
71+
/* The stock function of a slot, and what replaces it while its feature
72+
is on. In vi insert mode the Alt keys have no stock function (ESC
73+
leaves insert mode), so there the slot is unbound while off. */
74+
void rl_slot_funcs(const t_slot *s, rl_command_func_t **stock,
75+
rl_command_func_t **repl)
76+
{
77+
static rl_command_func_t *const stk[] = {rl_get_previous_history,
78+
rl_get_next_history, rl_forward_char, rl_end_of_line,
79+
rl_forward_word, rl_noninc_forward_search,
80+
rl_noninc_reverse_search};
81+
static rl_command_func_t *const rep[] = {rl_history_search_backward,
82+
rl_history_search_forward, rl_suggest_accept, rl_suggest_accept,
83+
rl_suggest_word, rl_suggest_older, rl_suggest_newer};
84+
85+
*stock = stk[s->fn];
86+
*repl = rep[s->fn];
87+
if (s->vi && s->fn >= SLOT_WORD)
88+
*stock = NULL;
89+
}

‎src/platform/posix/rl_suggest.c‎

Lines changed: 22 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -23,45 +23,30 @@
2323
** is a C1 control in its UTF-8 form: running `printf '\e[31m'` once must
2424
** not repaint every later prompt. */
2525

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

33-
i = 0;
34-
while (s[i])
35-
{
36-
if (s[i] < 0x20 || s[i] == 0x7f)
37-
return (false);
38-
if (s[i] == 0xc2 && s[i + 1] >= 0x80 && s[i + 1] < 0xa0)
39-
return (false);
40-
i++;
41-
}
42-
return (true);
32+
i = rl_suggest_scan(state, l, state->hist.hist_cmds.len);
33+
if (i < 0)
34+
return (NULL);
35+
return (((char **)state->hist.hist_cmds.ctx)[i]);
4336
}
4437

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

54-
a = (char **)state->hist.hist_cmds.ctx;
55-
i = state->hist.hist_cmds.len;
56-
seen = 0;
57-
while (i > 0 && seen++ < (size_t)l->sg_max)
58-
{
59-
i--;
60-
if (a[i] && !ft_strncmp(a[i], rl_line_buffer, n)
61-
&& ft_strlen(a[i]) > n && sg_clean((unsigned char *)a[i]))
62-
return (a[i]);
63-
}
64-
return (NULL);
44+
if (!l->sg_known)
45+
return (false);
46+
p = ft_strlen(l->sg_prev);
47+
return (p <= n && !ft_strncmp(rl_line_buffer, l->sg_prev, p)
48+
&& (!l->sg_ent || (ft_strlen(l->sg_ent) > n
49+
&& !ft_strncmp(l->sg_ent, rl_line_buffer, n))));
6550
}
6651

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

82-
n = (size_t)rl_end;
83-
p = 0;
84-
if (l->sg_known)
85-
p = ft_strlen(l->sg_prev);
86-
if (l->sg_known && p <= n && !ft_strncmp(rl_line_buffer, l->sg_prev, p)
87-
&& (!l->sg_ent || (ft_strlen(l->sg_ent) > n
88-
&& !ft_strncmp(l->sg_ent, rl_line_buffer, n))))
65+
rl_suggest_cyc_sync(state, l);
66+
if (sg_cached(l, (size_t)rl_end))
8967
return (l->sg_ent);
9068
xfree(l->sg_prev);
9169
xfree(l->sg_ent);
92-
e = sg_scan(state, l, n);
70+
e = sg_scan(state, l);
9371
l->sg_ent = NULL;
9472
if (e)
9573
l->sg_ent = ft_strdup(e);
9674
l->sg_prev = ft_strdup(rl_line_buffer);
9775
l->sg_known = true;
76+
l->sg_hn = state->hist.hist_cmds.len;
9877
return (l->sg_ent);
9978
}
10079

‎src/platform/posix/rl_suggest2.c‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,18 @@ char *rl_suggest_update(t_shell *state, int cols)
5757

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

6567
n = (size_t)rl_end;
6668
if (!l->sg_txt || rl_point != rl_end)
6769
return (NULL);
70+
if (l->sg_cyc_line && ft_strcmp(l->sg_cyc_line, rl_line_buffer))
71+
return (NULL);
6872
if (l->sg_post && !ft_strcmp(l->sg_post_at, rl_line_buffer))
6973
return (l->sg_post);
7074
if (!l->sg_post && l->sg_ent && ft_strlen(l->sg_ent) > n

‎src/platform/posix/rl_suggest4.c‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ void rl_suggest_end(t_shell *state)
6464
xfree(l->sg_post);
6565
xfree(l->sg_post_at);
6666
xfree(l->sg_pub);
67+
rl_suggest_cyc_clear(l);
6768
l->sg_ent = NULL;
6869
l->sg_prev = NULL;
6970
l->sg_post = NULL;

0 commit comments

Comments
 (0)