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 incs/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@
/* Bumped whenever the welcome header itself changes what it says. The
header is shown once a day, but a new revision is new information, so
it is shown again immediately rather than waiting for tomorrow. */
# define HELLISH_HEADER_REV 3
# define HELLISH_HEADER_REV 4

#endif
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ markdown_extensions:

nav:
- Home: index.md
- What's new in 3.2: whats-new.md
- What it is + Install: product.md
- The manual: manual.md
- Builtins reference:
Expand Down
33 changes: 28 additions & 5 deletions src/infrastructure/banner.c
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,25 @@ static void build_cwd(char *buf, size_t n)
ft_strlcpy(buf, cwd, n);
}

/* The left column: a greeting, the 42 logo mascot, the hellish identity and the
current directory. HP_LOGO lines are centred and tinted with logo_color. */
/* Where the release's features are explained in full, under the logo. The
left column is a fixed 43 columns wide, so the link fits there at any
terminal width; the right column has 32 at 80. Plain text, not an OSC 8
hyperlink: header_width() skips CSI escapes only, and would count the
hidden target as visible columns. */
static const char *guide_link(void)
{
if (header_use_unicode())
return ("\033[38;5;209m\xe2\x86\x92\033[0m "
"\033[4;38;5;81munivers42.github.io/hellish/whats-new\033[0m");
return ("\033[38;5;209m->\033[0m "
"\033[4;38;5;81munivers42.github.io/hellish/whats-new\033[0m");
}

/* The left column: a greeting, the 42 logo mascot, the hellish identity, the
current directory and the link to the release guide. HP_LOGO lines are
centred and tinted with logo_color. Under an ASCII terminal the identity's
middle dot is a dash: header_width() cannot decode it there and stops
counting, which pushed the frame out of line. */
static void build_left(const char **l)
{
static char welcome[128];
Expand All @@ -75,9 +92,15 @@ static void build_left(const char **l)
while (g_logo[++i])
l[i + 2] = g_logo[i];
l[i + 2] = "";
l[i + 3] = "hellish " HELLISH_VERSION " \xc2\xb7 a POSIX shell with bite";
l[i + 3] = "hellish " HELLISH_VERSION " - a POSIX shell with bite";
if (header_use_unicode())
l[i + 3] = "hellish " HELLISH_VERSION
" \xc2\xb7 a POSIX shell with bite";
l[i + 4] = cwd;
l[i + 5] = NULL;
l[i + 5] = "";
l[i + 6] = "\033[38;5;245mthe 3.2 guide, with examples\033[0m";
l[i + 7] = guide_link();
l[i + 8] = NULL;
}

/* Print the welcome panel on interactive (tty) startup, when it has
Expand All @@ -92,7 +115,7 @@ static void build_left(const char **l)
HELLISH_BANNER. */
void show_welcome(t_shell *state)
{
const char *l[16];
const char *l[20];
const char *r[24];
t_panel p;

Expand Down
106 changes: 52 additions & 54 deletions src/infrastructure/banner2.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,79 +13,77 @@
#include "banner_private.h"
#include "shell.h"

/* A rotating-feel showcase of real things this shell can do that a plain sh
can't — a brag line plus two copy-pasteable tricks. Each is fronted by a
coloured badge (g_chip) and carries its own inline colours: the command in
warm code-yellow, the gloss in muted grey. All verified to work. */
static const char *g_chip[] = {
"\033[1;48;5;209;38;5;236m HOT \033[0m",
"\033[1;48;5;141;38;5;236m PRO \033[0m",
"\033[1;48;5;73;38;5;236m TIP \033[0m"
};

static const char *g_news[] = {
"\033[38;5;245moutpaces \033[1;38;5;209mbash --posix\033[0m"
"\033[38;5;245m on compute-heavy scripts\033[0m",
"\033[38;5;223mdiff <(a) <(b)\033[0m"
"\033[38;5;245m compare two outputs, no temp files\033[0m",
"\033[38;5;223mcp report{,.bak}\033[0m"
"\033[38;5;245m duplicate a name in one word\033[0m",
NULL
};

/* The block heading — an invitation to learn a trick, in cool cyan. */
static void news_head(char *dst)
/* One row of "What's new in 3.2": the keys that reach a new feature, in
code-yellow, then what it does, in muted grey -- the shape of "Getting
started", so each can be tried before the prompt even appears. Every
row fits the 32 columns the right column has on an 80-column terminal.
Keys are padded to ten columns by hand (ft_snprintf pads bytes; an arrow
is three bytes in one column); the ASCII keys are for terminals where
header_use_unicode() says no. A NULL key ends a group: the always-on
features, then the hint keys, which need HELLISH_AUTOSUGGEST.
wiki/whats-new.md is the long form of this table and must stay in step
with it; next release, replace both and bump HELLISH_HEADER_REV.
Returns 0 at the end of a group. */
static int news_row(char *dst, int n)
{
ft_snprintf(dst, 160, "\033[1;38;5;81mDid you know?\033[0m");
}
static const char *utf8[] = {"\xe2\x86\x91 \xe2\x86\x93 ",
"make <TAB>", "bind -x ", "bindkey ", "${var@P} ", "$SRANDOM ",
NULL, "\xe2\x86\x92 ^F End ", "Alt-F ", "Alt-N/P ", NULL};
static const char *ascii[] = {"Up Down ", "make <TAB>", "bind -x ",
"bindkey ", "${var@P} ", "$SRANDOM ", NULL, "Right ^F ",
"Alt-F ", "Alt-N/P ", NULL};
static const char *what[] = {"history by prefix", "Makefile targets",
"key runs a command", "zsh binds now work", "render as a prompt",
"and $EPOCHREALTIME", NULL, "take the dim hint", "take one word",
"older/newer hint"};
const char **keys;

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

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

news_head(nw[0]);
r[(*i)++] = nw[0];
r[(*i)++] = "\033[1;48;5;209;38;5;236m NEW \033[0m "
"\033[1;38;5;81mWhat's new in 3.2\033[0m";
r[(*i)++] = "";
n = -1;
while (g_news[++n])
{
news_item(nw[n + 1], g_chip[n], g_news[n]);
r[(*i)++] = nw[n + 1];
}
arw = "->";
if (header_use_unicode())
arw = "\xe2\x86\x92";
ft_snprintf(nw[4], 160, " \033[38;5;173m%s\033[0m "
"\033[38;5;245mmore in docs/, or run \033[0m\033[38;5;223mhelp\033[0m",
arw);
k = 0;
m = 0;
while (news_row(nw[m], k++))
r[(*i)++] = nw[m++];
r[(*i)++] = "";
r[(*i)++] = nw[4];
r[(*i)++] = " \033[38;5;223mexport HELLISH_AUTOSUGGEST=1\033[0m";
while (news_row(nw[m], k++))
r[(*i)++] = nw[m++];
}

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

i = 0;
r[i++] = HP_HEAD "Getting started";
r[i++] = "";
r[i++] = " help commands & options";
r[i++] = " update check for a newer release";
r[i++] = " type NAME how any name resolves";
r[i++] = " update check for updates";
r[i++] = " type NAME what a name runs";
r[i++] = HP_RULE;
news_block(r, &i, nw);
r[i++] = HP_RULE;
Expand Down
138 changes: 138 additions & 0 deletions tests/banner_news_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""The welcome banner's "What's new" block reads whole, and its link lands.

The block is there to be read before the first prompt, so a clipped row is a
defect. At 80 columns, the narrowest terminal the panel is laid out for, the
right column has 32 cells; a row longer than that is cut with an ellipsis
and loses its meaning. The 3.1 rows were written for about 40 and were cut.
Under an ASCII terminal (LANG=C) every framed row must still be exactly the
terminal's width: a UTF-8 byte there cannot be measured, and the identity
line's middle dot pushed the frame out of line.

The banner links to wiki/whats-new.md. That page must exist and be in the
docs site's nav (mkdocs.yml), or the printed URL is a 404, and it must name
every key the banner shows, so the two cannot drift apart.

Usage: python3 banner_news_test.py /path/to/hellish
"""
import fcntl
import os
import pty
import re
import select
import struct
import sys
import tempfile
import termios
import time

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SHELL = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else
os.path.join(ROOT, "build", "bin", "hellish"))
FAILS = []
ESC = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(\x07|\x1b\\)")
LINK = "univers42.github.io/hellish/whats-new"


def check(name, ok, detail=""):
print(("ok " if ok else "FAIL ") + name)
if not ok:
FAILS.append(name)
print(" " + detail)


def banner(cols, lang):
"""The banner as text rows, drawn on a `cols`-wide pty in a fresh HOME
(so the cwd row is a short `~`)."""
home = tempfile.mkdtemp(prefix="banner_news_")
env = {"HOME": home, "PATH": os.environ["PATH"], "TERM": "xterm-256color",
"LANG": lang, "LC_ALL": lang, "XDG_CACHE_HOME": home,
"HELLISH_BANNER": "1", "HELLISH_NO_UPDATE_CHECK": "1",
"ASAN_OPTIONS": "detect_leaks=0"}
pid, fd = pty.fork()
if pid == 0:
os.chdir(home)
os.environ.clear()
os.environ.update(env)
os.execv(SHELL, [SHELL])
os._exit(127)
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", 50, cols, 0, 0))
out, quiet, end = b"", time.time() + 1.0, time.time() + 15
while time.time() < min(quiet, end):
r, _, _ = select.select([fd], [], [], 0.05)
if r:
try:
out += os.read(fd, 65536)
except OSError:
break
quiet = time.time() + 1.0
os.kill(pid, 9)
os.waitpid(pid, 0)
text = ESC.sub("", out.decode("utf-8", errors="replace"))
return [ln.strip("\r") for ln in text.split("\n")]


def news_cells(rows, bar):
"""The right-hand cells from the "What's new" heading to the next rule."""
cells, inside = [], False
for row in rows:
parts = row.split(bar)
if len(parts) != 4:
continue
cell = parts[2]
if "What's new in" in cell:
inside = True
elif inside and set(cell.strip()) <= set("─-"):
if cell.strip():
break
if inside:
cells.append(cell)
return cells


def panel(name, cols, lang, bar, clip):
rows = banner(cols, lang)
framed = [r for r in rows if r.startswith(bar) and r.endswith(bar)]
check(name + "/banner-drawn", len(framed) > 10, repr(rows[:6]))
bad = [r for r in framed if len(r) != cols]
check(name + "/every-row-is-%d-wide" % cols, not bad, repr(bad[:2]))
cells = news_cells(rows, bar)
check(name + "/news-block-found", len(cells) > 5, repr(cells))
cut = [c for c in cells if c.rstrip().endswith(clip)]
check(name + "/no-news-row-clipped", not cut, repr(cut))
check(name + "/guide-link-whole", any(LINK in r for r in framed),
repr([r for r in framed if "univers42" in r]))
return cells


def doc(cells):
path = os.path.join(ROOT, "wiki", "whats-new.md")
check("doc/whats-new-exists", os.path.isfile(path), path)
if not os.path.isfile(path):
return
with open(path, encoding="utf-8") as f:
page = f.read()
with open(os.path.join(ROOT, "mkdocs.yml"), encoding="utf-8") as f:
nav = f.read()
check("doc/in-the-site-nav", re.search(r":\s*whats-new\.md\s*$", nav,
re.M) is not None)
# A key row is " KEY10 what": one cell margin, two of indent, then
# the key padded to ten columns. The switch row is a command instead.
for c in cells:
text = c[3:].rstrip()
if not text or "What's new in" in c:
continue
key = text if text.startswith("export ") else c[3:13].rstrip()
check("doc/names " + key, key in page, "not in " + path)


def main():
cells = panel("utf8-80", 80, "C.UTF-8", "│", "…")
panel("utf8-120", 120, "C.UTF-8", "│", "…")
panel("ascii-80", 80, "C", "|", "~")
doc(cells)
print("%d failed" % len(FAILS))
return 1 if FAILS else 0


sys.exit(main())
Loading
Loading