Skip to content

Commit fcc231c

Browse files
authored
Merge pull request #182 from Univers42/docs/whats-new-3.2
feat(banner): show what is new in 3.2, readable at 80 columns
2 parents 98de69e + 995a1a5 commit fcc231c

8 files changed

Lines changed: 482 additions & 75 deletions

File tree

‎incs/version.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,6 @@
3030
/* Bumped whenever the welcome header itself changes what it says. The
3131
header is shown once a day, but a new revision is new information, so
3232
it is shown again immediately rather than waiting for tomorrow. */
33-
# define HELLISH_HEADER_REV 3
33+
# define HELLISH_HEADER_REV 4
3434

3535
#endif

‎mkdocs.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ markdown_extensions:
5555

5656
nav:
5757
- Home: index.md
58+
- What's new in 3.2: whats-new.md
5859
- What it is + Install: product.md
5960
- The manual: manual.md
6061
- Builtins reference:

‎src/infrastructure/banner.c‎

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,25 @@ static void build_cwd(char *buf, size_t n)
5555
ft_strlcpy(buf, cwd, n);
5656
}
5757

58-
/* The left column: a greeting, the 42 logo mascot, the hellish identity and the
59-
current directory. HP_LOGO lines are centred and tinted with logo_color. */
58+
/* Where the release's features are explained in full, under the logo. The
59+
left column is a fixed 43 columns wide, so the link fits there at any
60+
terminal width; the right column has 32 at 80. Plain text, not an OSC 8
61+
hyperlink: header_width() skips CSI escapes only, and would count the
62+
hidden target as visible columns. */
63+
static const char *guide_link(void)
64+
{
65+
if (header_use_unicode())
66+
return ("\033[38;5;209m\xe2\x86\x92\033[0m "
67+
"\033[4;38;5;81munivers42.github.io/hellish/whats-new\033[0m");
68+
return ("\033[38;5;209m->\033[0m "
69+
"\033[4;38;5;81munivers42.github.io/hellish/whats-new\033[0m");
70+
}
71+
72+
/* The left column: a greeting, the 42 logo mascot, the hellish identity, the
73+
current directory and the link to the release guide. HP_LOGO lines are
74+
centred and tinted with logo_color. Under an ASCII terminal the identity's
75+
middle dot is a dash: header_width() cannot decode it there and stops
76+
counting, which pushed the frame out of line. */
6077
static void build_left(const char **l)
6178
{
6279
static char welcome[128];
@@ -75,9 +92,15 @@ static void build_left(const char **l)
7592
while (g_logo[++i])
7693
l[i + 2] = g_logo[i];
7794
l[i + 2] = "";
78-
l[i + 3] = "hellish " HELLISH_VERSION " \xc2\xb7 a POSIX shell with bite";
95+
l[i + 3] = "hellish " HELLISH_VERSION " - a POSIX shell with bite";
96+
if (header_use_unicode())
97+
l[i + 3] = "hellish " HELLISH_VERSION
98+
" \xc2\xb7 a POSIX shell with bite";
7999
l[i + 4] = cwd;
80-
l[i + 5] = NULL;
100+
l[i + 5] = "";
101+
l[i + 6] = "\033[38;5;245mthe 3.2 guide, with examples\033[0m";
102+
l[i + 7] = guide_link();
103+
l[i + 8] = NULL;
81104
}
82105

83106
/* Print the welcome panel on interactive (tty) startup, when it has
@@ -92,7 +115,7 @@ static void build_left(const char **l)
92115
HELLISH_BANNER. */
93116
void show_welcome(t_shell *state)
94117
{
95-
const char *l[16];
118+
const char *l[20];
96119
const char *r[24];
97120
t_panel p;
98121

‎src/infrastructure/banner2.c‎

Lines changed: 52 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -13,79 +13,77 @@
1313
#include "banner_private.h"
1414
#include "shell.h"
1515

16-
/* A rotating-feel showcase of real things this shell can do that a plain sh
17-
can't — a brag line plus two copy-pasteable tricks. Each is fronted by a
18-
coloured badge (g_chip) and carries its own inline colours: the command in
19-
warm code-yellow, the gloss in muted grey. All verified to work. */
20-
static const char *g_chip[] = {
21-
"\033[1;48;5;209;38;5;236m HOT \033[0m",
22-
"\033[1;48;5;141;38;5;236m PRO \033[0m",
23-
"\033[1;48;5;73;38;5;236m TIP \033[0m"
24-
};
25-
26-
static const char *g_news[] = {
27-
"\033[38;5;245moutpaces \033[1;38;5;209mbash --posix\033[0m"
28-
"\033[38;5;245m on compute-heavy scripts\033[0m",
29-
"\033[38;5;223mdiff <(a) <(b)\033[0m"
30-
"\033[38;5;245m compare two outputs, no temp files\033[0m",
31-
"\033[38;5;223mcp report{,.bak}\033[0m"
32-
"\033[38;5;245m duplicate a name in one word\033[0m",
33-
NULL
34-
};
35-
36-
/* The block heading — an invitation to learn a trick, in cool cyan. */
37-
static void news_head(char *dst)
16+
/* One row of "What's new in 3.2": the keys that reach a new feature, in
17+
code-yellow, then what it does, in muted grey -- the shape of "Getting
18+
started", so each can be tried before the prompt even appears. Every
19+
row fits the 32 columns the right column has on an 80-column terminal.
20+
Keys are padded to ten columns by hand (ft_snprintf pads bytes; an arrow
21+
is three bytes in one column); the ASCII keys are for terminals where
22+
header_use_unicode() says no. A NULL key ends a group: the always-on
23+
features, then the hint keys, which need HELLISH_AUTOSUGGEST.
24+
wiki/whats-new.md is the long form of this table and must stay in step
25+
with it; next release, replace both and bump HELLISH_HEADER_REV.
26+
Returns 0 at the end of a group. */
27+
static int news_row(char *dst, int n)
3828
{
39-
ft_snprintf(dst, 160, "\033[1;38;5;81mDid you know?\033[0m");
40-
}
29+
static const char *utf8[] = {"\xe2\x86\x91 \xe2\x86\x93 ",
30+
"make <TAB>", "bind -x ", "bindkey ", "${var@P} ", "$SRANDOM ",
31+
NULL, "\xe2\x86\x92 ^F End ", "Alt-F ", "Alt-N/P ", NULL};
32+
static const char *ascii[] = {"Up Down ", "make <TAB>", "bind -x ",
33+
"bindkey ", "${var@P} ", "$SRANDOM ", NULL, "Right ^F ",
34+
"Alt-F ", "Alt-N/P ", NULL};
35+
static const char *what[] = {"history by prefix", "Makefile targets",
36+
"key runs a command", "zsh binds now work", "render as a prompt",
37+
"and $EPOCHREALTIME", NULL, "take the dim hint", "take one word",
38+
"older/newer hint"};
39+
const char **keys;
4140

42-
/* One showcase line: an indented badge, then the self-coloured tip. */
43-
static void news_item(char *dst, const char *chip, const char *text)
44-
{
45-
ft_snprintf(dst, 160, " %s %s\033[0m", chip, text);
41+
keys = ascii;
42+
if (header_use_unicode())
43+
keys = utf8;
44+
if (!keys[n])
45+
return (0);
46+
ft_snprintf(dst, 192, " \033[38;5;223m%s\033[0m "
47+
"\033[38;5;245m%s\033[0m", keys[n], what[n]);
48+
return (1);
4649
}
4750

48-
/* The showcase block: the heading, one badged tip per feature, then a salmon
49-
pointer onward to the docs. Backing strings are caller-owned. */
50-
static void news_block(const char **r, int *i, char nw[][160])
51+
/* The news block: a salmon NEW badge on a cyan title, the always-on rows,
52+
then the switch for hints and the keys that take them. `k` walks the
53+
table across its group break; `m` the caller-owned backing rows. */
54+
static void news_block(const char **r, int *i, char nw[][192])
5155
{
52-
const char *arw;
53-
int n;
56+
int k;
57+
int m;
5458

55-
news_head(nw[0]);
56-
r[(*i)++] = nw[0];
59+
r[(*i)++] = "\033[1;48;5;209;38;5;236m NEW \033[0m "
60+
"\033[1;38;5;81mWhat's new in 3.2\033[0m";
5761
r[(*i)++] = "";
58-
n = -1;
59-
while (g_news[++n])
60-
{
61-
news_item(nw[n + 1], g_chip[n], g_news[n]);
62-
r[(*i)++] = nw[n + 1];
63-
}
64-
arw = "->";
65-
if (header_use_unicode())
66-
arw = "\xe2\x86\x92";
67-
ft_snprintf(nw[4], 160, " \033[38;5;173m%s\033[0m "
68-
"\033[38;5;245mmore in docs/, or run \033[0m\033[38;5;223mhelp\033[0m",
69-
arw);
62+
k = 0;
63+
m = 0;
64+
while (news_row(nw[m], k++))
65+
r[(*i)++] = nw[m++];
7066
r[(*i)++] = "";
71-
r[(*i)++] = nw[4];
67+
r[(*i)++] = " \033[38;5;223mexport HELLISH_AUTOSUGGEST=1\033[0m";
68+
while (news_row(nw[m], k++))
69+
r[(*i)++] = nw[m++];
7270
}
7371

74-
/* The right column: getting-started tips, the "Did you know?" showcase, then a
75-
live version/update status footer. Backing strings are static (shown once).
76-
*/
72+
/* The right column: getting-started tips, what is new in this release,
73+
then a live version/update status footer. Backing strings are static
74+
(shown once). */
7775
void build_right(const char **r)
7876
{
79-
static char nw[5][160];
77+
static char nw[9][192];
8078
static char status[192];
8179
int i;
8280

8381
i = 0;
8482
r[i++] = HP_HEAD "Getting started";
8583
r[i++] = "";
8684
r[i++] = " help commands & options";
87-
r[i++] = " update check for a newer release";
88-
r[i++] = " type NAME how any name resolves";
85+
r[i++] = " update check for updates";
86+
r[i++] = " type NAME what a name runs";
8987
r[i++] = HP_RULE;
9088
news_block(r, &i, nw);
9189
r[i++] = HP_RULE;

‎tests/banner_news_test.py‎

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/usr/bin/env python3
2+
"""The welcome banner's "What's new" block reads whole, and its link lands.
3+
4+
The block is there to be read before the first prompt, so a clipped row is a
5+
defect. At 80 columns, the narrowest terminal the panel is laid out for, the
6+
right column has 32 cells; a row longer than that is cut with an ellipsis
7+
and loses its meaning. The 3.1 rows were written for about 40 and were cut.
8+
Under an ASCII terminal (LANG=C) every framed row must still be exactly the
9+
terminal's width: a UTF-8 byte there cannot be measured, and the identity
10+
line's middle dot pushed the frame out of line.
11+
12+
The banner links to wiki/whats-new.md. That page must exist and be in the
13+
docs site's nav (mkdocs.yml), or the printed URL is a 404, and it must name
14+
every key the banner shows, so the two cannot drift apart.
15+
16+
Usage: python3 banner_news_test.py /path/to/hellish
17+
"""
18+
import fcntl
19+
import os
20+
import pty
21+
import re
22+
import select
23+
import struct
24+
import sys
25+
import tempfile
26+
import termios
27+
import time
28+
29+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
30+
SHELL = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else
31+
os.path.join(ROOT, "build", "bin", "hellish"))
32+
FAILS = []
33+
ESC = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(\x07|\x1b\\)")
34+
LINK = "univers42.github.io/hellish/whats-new"
35+
36+
37+
def check(name, ok, detail=""):
38+
print(("ok " if ok else "FAIL ") + name)
39+
if not ok:
40+
FAILS.append(name)
41+
print(" " + detail)
42+
43+
44+
def banner(cols, lang):
45+
"""The banner as text rows, drawn on a `cols`-wide pty in a fresh HOME
46+
(so the cwd row is a short `~`)."""
47+
home = tempfile.mkdtemp(prefix="banner_news_")
48+
env = {"HOME": home, "PATH": os.environ["PATH"], "TERM": "xterm-256color",
49+
"LANG": lang, "LC_ALL": lang, "XDG_CACHE_HOME": home,
50+
"HELLISH_BANNER": "1", "HELLISH_NO_UPDATE_CHECK": "1",
51+
"ASAN_OPTIONS": "detect_leaks=0"}
52+
pid, fd = pty.fork()
53+
if pid == 0:
54+
os.chdir(home)
55+
os.environ.clear()
56+
os.environ.update(env)
57+
os.execv(SHELL, [SHELL])
58+
os._exit(127)
59+
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", 50, cols, 0, 0))
60+
out, quiet, end = b"", time.time() + 1.0, time.time() + 15
61+
while time.time() < min(quiet, end):
62+
r, _, _ = select.select([fd], [], [], 0.05)
63+
if r:
64+
try:
65+
out += os.read(fd, 65536)
66+
except OSError:
67+
break
68+
quiet = time.time() + 1.0
69+
os.kill(pid, 9)
70+
os.waitpid(pid, 0)
71+
text = ESC.sub("", out.decode("utf-8", errors="replace"))
72+
return [ln.strip("\r") for ln in text.split("\n")]
73+
74+
75+
def news_cells(rows, bar):
76+
"""The right-hand cells from the "What's new" heading to the next rule."""
77+
cells, inside = [], False
78+
for row in rows:
79+
parts = row.split(bar)
80+
if len(parts) != 4:
81+
continue
82+
cell = parts[2]
83+
if "What's new in" in cell:
84+
inside = True
85+
elif inside and set(cell.strip()) <= set("─-"):
86+
if cell.strip():
87+
break
88+
if inside:
89+
cells.append(cell)
90+
return cells
91+
92+
93+
def panel(name, cols, lang, bar, clip):
94+
rows = banner(cols, lang)
95+
framed = [r for r in rows if r.startswith(bar) and r.endswith(bar)]
96+
check(name + "/banner-drawn", len(framed) > 10, repr(rows[:6]))
97+
bad = [r for r in framed if len(r) != cols]
98+
check(name + "/every-row-is-%d-wide" % cols, not bad, repr(bad[:2]))
99+
cells = news_cells(rows, bar)
100+
check(name + "/news-block-found", len(cells) > 5, repr(cells))
101+
cut = [c for c in cells if c.rstrip().endswith(clip)]
102+
check(name + "/no-news-row-clipped", not cut, repr(cut))
103+
check(name + "/guide-link-whole", any(LINK in r for r in framed),
104+
repr([r for r in framed if "univers42" in r]))
105+
return cells
106+
107+
108+
def doc(cells):
109+
path = os.path.join(ROOT, "wiki", "whats-new.md")
110+
check("doc/whats-new-exists", os.path.isfile(path), path)
111+
if not os.path.isfile(path):
112+
return
113+
with open(path, encoding="utf-8") as f:
114+
page = f.read()
115+
with open(os.path.join(ROOT, "mkdocs.yml"), encoding="utf-8") as f:
116+
nav = f.read()
117+
check("doc/in-the-site-nav", re.search(r":\s*whats-new\.md\s*$", nav,
118+
re.M) is not None)
119+
# A key row is " KEY10 what": one cell margin, two of indent, then
120+
# the key padded to ten columns. The switch row is a command instead.
121+
for c in cells:
122+
text = c[3:].rstrip()
123+
if not text or "What's new in" in c:
124+
continue
125+
key = text if text.startswith("export ") else c[3:13].rstrip()
126+
check("doc/names " + key, key in page, "not in " + path)
127+
128+
129+
def main():
130+
cells = panel("utf8-80", 80, "C.UTF-8", "│", "…")
131+
panel("utf8-120", 120, "C.UTF-8", "│", "…")
132+
panel("ascii-80", 80, "C", "|", "~")
133+
doc(cells)
134+
print("%d failed" % len(FAILS))
135+
return 1 if FAILS else 0
136+
137+
138+
sys.exit(main())

0 commit comments

Comments
 (0)