#!/bin/sh
# alganet · blog — the shell version of the site, as a tui.sh program.
#
# This shell script IS the site. It renders the blog — home, the full index and
# every post, in English and Portuguese — as a full-screen terminal UI. The aim is
# NOT a classical TUI: no sidebar, no boxes, no status bar. It is the website
# itself — a centered column of text with the `alganet` masthead on top and a thin
# scrollbar on the right — just delivered through a terminal.
#
# It runs BOTH ways, off the exact same file:
#   • HOSTED — the browser boot loader mounts tuish at /src and the .tui content at
#     /content and runs us on busybox-wasm, painting to xterm.js.
#   • NATIVE — a real terminal, reached with:
#         bash -c "$(curl -fsSL https://alganet.github.io/terminal/index.sh)"
#     Nothing is mounted; we fetch the toolkit and every page over HTTP as you
#     browse. OSC-2 titles, OSC-52 copy, truecolor and resize all work for free.
# It is GENERIC either way: the content is ".tui" files generated by ../build.sh
# alongside each .html. Page token is home | blog | post:<basename>, lang en | pt.

_BASE="${TUISH_BASE:-https://alganet.github.io}"   # where the site is served
_src="${TUISH_SRC:-/src}"

# curl-or-wget fetcher (used only in native mode).
_fetch () {
	if command -v curl >/dev/null 2>&1; then curl -fsSL "$1"
	elif command -v wget >/dev/null 2>&1; then wget -qO- "$1"
	else return 1; fi
}

# Hosted (tuish mounted at /src) vs native (fetch everything over HTTP).
if test -r "${_src}/compat.sh"
then _NATIVE=0
else
	_NATIVE=1
	# `curl … | sh` puts the SCRIPT on stdin, so the reader could not read the
	# keyboard. Re-exec with the terminal as stdin and a shell tuish supports.
	if ! test -t 0
	then
		if test -e /dev/tty
		then _resh=sh; command -v bash >/dev/null 2>&1 && _resh=bash
		     exec "$_resh" -c "$(_fetch "${_BASE}/terminal/index.sh")" </dev/tty
		else echo "Run in a terminal:  bash -c \"\$(curl -fsSL ${_BASE}/terminal/index.sh)\"" >&2; exit 1
		fi
	fi
	command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 \
		|| { echo "need curl or wget" >&2; exit 1; }
	_src=$(mktemp -d) || { echo "mktemp failed" >&2; exit 1; }
	_TMPF="${_src}/.page"
	for _m in compat ord tui term canvas event hid viewport str draw keybind buf clip
	do _fetch "${_BASE}/terminal/tuish/src/${_m}.sh" > "${_src}/${_m}.sh" \
		|| { echo "failed to fetch ${_m}.sh" >&2; rm -rf "$_src"; exit 1; }
	done
fi

. "${_src}/compat.sh"
. "${_src}/ord.sh"
. "${_src}/tui.sh"
. "${_src}/term.sh"
. "${_src}/canvas.sh"
. "${_src}/event.sh"
. "${_src}/hid.sh"
. "${_src}/viewport.sh"
. "${_src}/str.sh"
. "${_src}/draw.sh"
. "${_src}/keybind.sh"
. "${_src}/buf.sh"
. "${_src}/clip.sh"

_US=$(printf '\037')            # field delimiter inside .tui records

# ─── Palette — the site's own dark theme (style.css :root[data-theme=dark]) ──
C_BG='23:27:32'                 # --bg-base         #171b20
C_FG='211:218:225'              # --text-main       #d3dae1
C_MUTED='179:190:201'           # --text-muted      #b3bec9
C_FAINT='147:160:172'           # --text-faint      #93a0ac
C_H1='122:203:130'              # --heading-link    #7acb82  (the alganet mark, h2 links)
C_H2='103:168:110'              # --heading-2       #67a86e  (section / post titles, h3)
C_LINK='143:184:255'            # --link-main       #8fb8ff
C_BORDER='58:67:77'             # --border-muted    #3a434d
C_CODEBG='28:34:42'             # --surface-code    #1c222a
C_CODE='195:204:214'            # --code-output-text #c3ccd6  (default code text)
C_INLINE='79:209:194'           # --code-string     #4fd1c2  (inline code)
C_SELBG='42:50:60'              # focused/hovered link tint

# Code syntax colours — the site's dark-theme --code-* palette. Mapped from the
# posts' <span class="code-*"> classes (see build.sh _cmap / reader _paint_code).
CC_KW='240:138:118'             # statement / keyword  #f08a76
CC_STR='79:209:194'             # string               #4fd1c2
CC_CMT='88:210:123'             # comment              #58d27b
CC_FN='122:191:254'             # function / result    #7abffe
CC_CLS='209:179:255'            # class                #d1b3ff
CC_NUM='169:180:191'            # number               #a9b4bf
CC_ATTR='255:196:120'           # attr                 #ffc478
CC_OP='184:194:205'             # operator / punct     #b8c2cd
CC_BI='255:178:223'             # builtin              #ffb2df
CC_ERR='255:127:127'            # stderr               #ff7f7f

# ─── State ───────────────────────────────────────────────────────────
_lang=en                        # en | pt
_view=home                      # home | list | post
_post_base=                     # post basename (the .tui/.html stem) when _view=post
_en_base=                       # this post's EN basename  (from the 'a' pair record)
_pt_base=                       # this post's PT basename
_return=home                    # where 'back' goes from a post
_scroll=0                       # content scroll offset (top line, 0-based)
_content_h=0                    # rows in the content buffer
_sel=0                          # focused link/entry (1-based; 0 = none) — Tab moves it
_ent_hover=0                    # entry under the mouse (1-based; 0 = none)
_ent_n=0                        # number of selectable entries built
_csel=0                         # focused code block (1-based; 0 = none) — posts
_cb_n=0                         # number of code blocks built
_copyflash=0                    # 1 briefly after a copy (shows "copied ✓")
_NL='
'                               # a literal newline, for joining copied code lines
_started=no
_built_w=-1                     # width the content buffer was last wrapped at (resize cache)
_lang_x=0                       # screen column where the masthead language switch begins
_scroll_drawn=0                 # the _scroll value the body was last painted at (scroll-shift cache)
_geom_drawn=                    # body geometry at that paint ("_ci_r:_ci_h:_colc:_colw")
_body_dirty=1                   # 1 = something other than _scroll changed; next body paint must be full

# ─── Layout — a centered column, no chrome ───────────────────────────
_MAXCOL=76                      # max readable column width (≈ the site's 40em measure)
_ARTW=19                        # display width of the widest wordmark row
_lay ()
{
	_W=$TUISH_COLUMNS; _H=$TUISH_LINES
	_mast_r=1                        # the alganet wordmark (2 dense rows of art…)
	_lang_r=2                        # language switch, centered against the art
	_rule_r=3                        # …then the rule ON the baseline, the g's leg crossing it
	_ci_r=5                          # first content row (blank row 4 for breathing room)
	_ci_h=$(( _H - _ci_r + 1 ))      # content runs to the last row
	_colw=$(( _W - 6 ))              # leave side margins + a lane for the scrollbar
	test "$_colw" -gt "$_MAXCOL" && _colw=$_MAXCOL
	test "$_colw" -lt 16 && _colw=$(( _W - 2 ))
	test "$_colw" -lt 1 && _colw=1
	_colc=$(( (_W - _colw) / 2 + 1 ))   # centered
	_sb_c=$(( _colc + _colw + 1 ))       # scrollbar just right of the column
	test "$_sb_c" -gt "$_W" && _sb_c=$_W
}
_text_w () { _TEXT_W=$_colw; test "$_TEXT_W" -ge 1 || _TEXT_W=1; }

# ─── Loading content ─────────────────────────────────────────────────
_suf () { test "$_lang" = pt && echo .pt; }

# Content root: hosted → /content (mounted); native → the site root (fetched).
if test "$_NATIVE" -eq 1; then _croot="$_BASE"; else _croot="${TUISH_CONTENT:-/content}"; fi
_cpath ()
{
	case $_view in
		home) echo "${_croot}/index$(_suf).tui";;
		list) echo "${_croot}/blog$(_suf).tui";;
		post) echo "${_croot}/blog/${_post_base}.tui";;
	esac
}
_hpath ()
{
	case $_view in
		home) test "$_lang" = pt && echo /index.pt.html || echo / ;;
		list) test "$_lang" = pt && echo /blog.pt.html  || echo /blog.html ;;
		post) echo "/blog/${_post_base}.html";;
	esac
}

_load ()
{
	tuish_buf_init raw
	_en_base=''; _pt_base=''; _ptitle=''
	local _f _l _sty
	_f=$(_cpath)
	# Native: sub-curl the page to a temp file, then read it exactly like a mounted
	# one (a file redirect, never a pipe — the parse loop must stay in this shell).
	if test "$_NATIVE" -eq 1
	then _fetch "$_f" > "$_TMPF" 2>/dev/null || : ; _f=$_TMPF; fi
	if test -f "$_f" && test -s "$_f"
	then
		while IFS= read -r _l || test -n "$_l"
		do
			_sty="${_l%%	*}"
			if test "$_sty" = a
			then
				_l="${_l#*	}"
				_en_base="${_l%%${_US}*}"
				_pt_base="${_l#*${_US}}"
			else
				test "$_sty" = t && _ptitle="${_l#*	}"
				tuish_buf_append raw "$_l"
			fi
		done < "$_f"
	else
		tuish_buf_append raw "h	Not found"
		tuish_buf_append raw "p	x${_US}This page has no shell version yet."
	fi
	tuish_buf_count raw; _raw_h=$TUISH_BUF_COUNT
}

# ─── Segment-aware word wrap ─────────────────────────────────────────
# A paragraph/bullet/quote record's PAYLOAD is spans of "style<US>text". _wrap
# turns one into as many fitted 'S' lines as the width needs, each an alternating
# "style<US>text" piece list the renderer paints left to right. Words are built
# ACROSS spans so a link's text and its dim URL flow together and punctuation
# glued to a span (")," after a URL) stays glued.
_words_from_payload ()
{
	_wn=0
	local _rest=$1 _sty _txt _prevtrail=1 _lead _trail _n _ci _ch _tmp
	while test -n "$_rest"
	do
		_sty="${_rest%%${_US}*}"; _rest="${_rest#*${_US}}"
		case $_rest in
			*${_US}*) _txt="${_rest%%${_US}*}"; _rest="${_rest#*${_US}}";;
			*)        _txt="$_rest"; _rest='';;
		esac
		test -z "$_txt" && continue
		case $_txt in ' '*) _lead=1;; *) _lead=0;; esac
		case $_txt in *' ') _trail=1;; *) _trail=0;; esac
		local IFS=' '
		set -- $_txt
		unset IFS
		_n=$#; _ci=0
		for _ch in "$@"
		do
			_ci=$(( _ci + 1 ))
			if test $_ci -eq 1 && test $_lead -eq 0 && test $_prevtrail -eq 0 && test $_wn -gt 0
			then
				_tmp=$_ch; tuish_str_width _tmp
				eval "_wpay_$_wn=\"\$_wpay_$_wn\${_US}\$_sty\${_US}\$_ch\"
				      _wwid_$_wn=\$(( \$_wwid_$_wn + $TUISH_SWIDTH ))"
			else
				_wn=$(( _wn + 1 ))
				_tmp=$_ch; tuish_str_width _tmp
				eval "_wpay_$_wn=\"\$_sty\${_US}\$_ch\"  _wwid_$_wn=$TUISH_SWIDTH"
			fi
		done
		if test $_n -eq 0; then _prevtrail=1; else _prevtrail=$_trail; fi
	done
}

# Prefixes/indents differ between the FIRST wrapped line and continuations:
#   p — first line indented (book-style first-line indent), rest flush
#   b — first line a bullet, continuations hang-indented to align
#   q — a quote bar on every line
_wrap ()
{
	local _sty=$1
	local _pf='' _pc='' _indf=0 _indc=0
	case $_sty in
		p) _pf="x${_US}   "; _indf=3;;
		b) _pf="A${_US}  • "; _pc="x${_US}    "; _indf=4; _indc=4;;
		q) _pf="d${_US}▏ "; _pc="d${_US}▏ "; _indf=2; _indc=2;;
	esac
	_words_from_payload "$2"
	if test "$_wn" -eq 0; then tuish_buf_append content "_	"; return 0; fi

	local _i=1 _lineno=0 _linepay='' _linew=0 _wp _ww _pfx _tgt
	_tgt=$(( _TEXT_W - _indf )); test "$_tgt" -lt 1 && _tgt=1
	while test $_i -le "$_wn"
	do
		eval "_wp=\$_wpay_$_i _ww=\$_wwid_$_i"
		if test "$_linew" -eq 0
		then
			_linepay=$_wp; _linew=$_ww; _i=$(( _i + 1 ))
		elif test $(( _linew + 1 + _ww )) -le "$_tgt"
		then
			_linepay="${_linepay}${_US}x${_US} ${_US}${_wp}"; _linew=$(( _linew + 1 + _ww )); _i=$(( _i + 1 ))
		else
			test $_lineno -eq 0 && _pfx=$_pf || _pfx=$_pc
			tuish_buf_append content "S	${_pfx:+$_pfx$_US}$_linepay"
			_lineno=$(( _lineno + 1 )); _linepay=''; _linew=0
			_tgt=$(( _TEXT_W - _indc )); test "$_tgt" -lt 1 && _tgt=1
		fi
	done
	test $_lineno -eq 0 && _pfx=$_pf || _pfx=$_pc
	tuish_buf_append content "S	${_pfx:+$_pfx$_US}$_linepay"
}

# ─── Build the display buffer from the raw records (wrap to current width) ────
_build_content ()
{
	_text_w
	tuish_buf_init content
	_ent_n=0 _cb_n=0
	local _i=1 _raw _sty _pay _incode=0
	while test $_i -le "$_raw_h"
	do
		tuish_buf_get raw "$_i"; _raw="$TUISH_BLINE"
		_sty="${_raw%%	*}"; _pay="${_raw#*	}"
		# A blank code-bg row above and below each run of code (padding), and each
		# run is registered as a copiable block: its line range and its plain text.
		if test "$_sty" = c
		then
			if test "$_incode" -eq 0
			then tuish_buf_append content "y	"; _incode=1
			     _cb_n=$(( _cb_n + 1 )); tuish_buf_count content
			     eval "_cb_top_$_cb_n=$TUISH_BUF_COUNT _cb_text_$_cb_n=''"
			fi
			_seg_text "$_pay"
			eval "_cb_text_$_cb_n=\"\${_cb_text_$_cb_n}\${_SEGTEXT}\${_NL}\""
		else
			if test "$_incode" -eq 1
			then tuish_buf_append content "y	"; _incode=0
			     tuish_buf_count content; eval "_cb_bot_$_cb_n=$TUISH_BUF_COUNT"
			fi
		fi
		case $_sty in
			p|q) _wrap "$_sty" "$_pay";;
			b)   _wrap b "$_pay";;
			e)   _ent_n=$(( _ent_n + 1 ))
			     local _b="${_pay%%${_US}*}" _r2="${_pay#*${_US}}"
			     local _t="${_r2%%${_US}*}" _d="${_r2#*${_US}}"
			     eval "_ent_base_$_ent_n=\$_b _ent_title_$_ent_n=\$_t _ent_date_$_ent_n=\$_d"
			     tuish_buf_append content "E	$_ent_n"
			     eval "_ent_line_$_ent_n=\$TUISH_BUF_COUNT" ;;
			t)   tuish_buf_append content "t	$_pay"; tuish_buf_append content "_	";;
			i)   tuish_buf_append content "i	$_pay"; tuish_buf_append content "r	"; tuish_buf_append content "_	";;
			h)   tuish_buf_append content "_	"; tuish_buf_append content "h	$_pay"; tuish_buf_append content "_	";;
			c)   tuish_buf_append content "c	$_pay";;
			g)   tuish_buf_append content "g	$_pay";;
			d)   tuish_buf_append content "d	$_pay";;
			r)   tuish_buf_append content "r	";;
			*)   tuish_buf_append content "_	";;
		esac
		_i=$(( _i + 1 ))
	done
	if test "$_incode" -eq 1
	then tuish_buf_append content "y	"; tuish_buf_count content; eval "_cb_bot_$_cb_n=$TUISH_BUF_COUNT"; fi
	tuish_buf_count content; _content_h=$TUISH_BUF_COUNT
	_built_w=$_TEXT_W
	if test "$_ent_n" -gt 0
	then test "$_sel" -lt 1 && _sel=1; test "$_sel" -gt "$_ent_n" && _sel=$_ent_n
	else _sel=0; fi
	if test "$_cb_n" -gt 0
	then test "$_csel" -lt 1 && _csel=1; test "$_csel" -gt "$_cb_n" && _csel=$_cb_n
	else _csel=0; fi
	return 0
}

# _seg_text PAYLOAD -> _SEGTEXT: the plain text of a segmented payload (styles
# dropped, texts concatenated) — used to reconstruct a code block for copying.
_seg_text ()
{
	_SEGTEXT=''
	local _rest=$1 _t
	while test -n "$_rest"
	do
		_rest="${_rest#*${_US}}"               # drop the style field
		case $_rest in
			*${_US}*) _t="${_rest%%${_US}*}"; _rest="${_rest#*${_US}}";;
			*)        _t="$_rest"; _rest='';;
		esac
		_SEGTEXT="$_SEGTEXT$_t"
	done
}

# ─── Router ──────────────────────────────────────────────────────────
_page_token () { case $_view in home) echo home;; list) echo blog;; post) echo "post:$_post_base";; esac; }

# Report page AND language on stderr (framed by \036 RS) so a resize-respawn
# restores where we were.  Format: page:<lang>:<token>
_report_page () { _ctrl '\036page:%s:%s\036' "$_lang" "$(_page_token)"; _set_title; }
_ctrl () { test -n "${TUISH_CTRL:-}" || return 0; printf "$@" >&2; return 0; }

# Set the browser tab title (OSC 2 → xterm onTitleChange → document.title),
# mirroring the HTML pages' <title>: the post title on a post, else "alganet".
_set_title ()
{
	local _tt=alganet
	test "$_view" = post && test -n "$_ptitle" && _tt=$_ptitle
	printf '\033]2;%s\033\\' "$_tt"
	return 0
}

_open ()   # $1=view  [$2=base]
{
	_view=$1
	test "$1" = post && _post_base=$2
	_scroll=0; _sel=0; _csel=0; _copyflash=0
	_load; _build_content
	_report_page
	tuish_request_redraw -1
}

_activate_entry ()   # $1 = entry index
{
	local _b; eval "_b=\$_ent_base_$1"
	case $_b in
		@blog) _open list;;
		*)     _return=$_view; _open post "$_b";;
	esac
}

_toggle_lang ()
{
	if test "$_view" = post
	then
		local _new
		if test "$_lang" = en; then _new=$_pt_base; else _new=$_en_base; fi
		test -n "$_new" || return 0
		test "$_lang" = en && _lang=pt || _lang=en
		_open post "$_new"
	else
		test "$_lang" = en && _lang=pt || _lang=en
		_open "$_view"
	fi
}

_home () { test "$_view" = home || _open home; return 0; }
_back () { test "$_view" = post && { _open "$_return"; return 0; }; test "$_view" = list && _open home; return 0; }
# "View as HTML": hosted → tell the browser to navigate; native → open the URL in
# the desktop browser (best-effort).
_view_html ()
{
	if test "$_NATIVE" -eq 1
	then
		local _u; _u=$(_hpath); case $_u in /*) _u="${_BASE}${_u}";; esac
		( xdg-open "$_u" >/dev/null 2>&1 || open "$_u" >/dev/null 2>&1 || : ) &
	else _ctrl '\036nav:%s\036' "$(_hpath)"
	fi
	return 0
}

# ─── Rendering ───────────────────────────────────────────────────────
_render ()      { _lay; tuish_begin; tuish_draw_fill 1 1 "$_W" "$_H" bg=$C_BG; _draw_masthead; _draw_body; tuish_end; }
_render_mast () { _lay; tuish_begin; _draw_masthead; tuish_end; }

# A scroll is the common body redraw. Rather than repaint every visible line,
# shift the rows that stay on screen with the terminal's own scroll region and
# paint only the few that scrolled into view (_scroll_body). Fall back to a full
# _draw_body when the body changed for a non-scroll reason (_body_dirty), the
# geometry moved (resize/rebuild), or the jump clears the whole screen anyway.
_render_body ()
{
	_lay
	tuish_begin
	local _d=$(( _scroll - _scroll_drawn )) _ad
	_ad=${_d#-}
	if test "$_d" -ne 0 && test "$_body_dirty" -eq 0 \
	   && test "$_geom_drawn" = "${_ci_r}:${_ci_h}:${_colc}:${_colw}" \
	   && test "$_ad" -lt "$_ci_h"
	then _scroll_body "$_d"
	else _draw_body
	fi
	tuish_end
}

# A rule (─) drawn as text so it carries the app background — tuish_draw_hline
# only sets fg, which would leave the cells on the terminal's default (black) bg.
_rule () { tuish_str_repeat '─' "$3"; tuish_text "$1" "$2" "$TUISH_SREPEATED" fg=$C_BORDER bg=$C_BG maxwidth=$3; }

# Bold text: tuish_text has no bold option, but it always resets SGR afterward, so
# arming bold just before it (and nothing between but the cursor move) prints bold.
_btext () { tuish_bold; tuish_text "$@"; }

# The masthead: the `alganet` wordmark as terminal art (figlet "threepoint",
# lowercase like the site's serif h1), the language switch on the right, and a rule
# beneath — the site's h1 → border → nav.lang, minus the browser chrome.
_draw_masthead ()
{
	tuish_draw_fill 1 1 "$_W" "$_rule_r" bg=$C_BG
	# The two dense art rows (the body of the wordmark), above the baseline.
	_btext "$_mast_r"        "$_colc" ' _ | _  _  _  _ _|_' fg=$C_H1 bg=$C_BG
	_btext $(( _mast_r + 1 )) "$_colc" '(_||(_|(_|| |(/_ |'  fg=$C_H1 bg=$C_BG
	# Language switch, right-aligned, centered against the art.
	local _en="English" _pt="Português" _sep="  ·  "
	local _we _wp _ws _tot
	tuish_str_width _en; _we=$TUISH_SWIDTH
	tuish_str_width _pt; _wp=$TUISH_SWIDTH
	_ws=${#_sep}
	_tot=$(( _we + _ws + _wp ))
	_lang_x=$(( _colc + _colw - _tot ))
	test "$_lang_x" -lt $(( _colc + _ARTW + 1 )) && _lang_x=$(( _colc + _ARTW + 1 ))
	local _enc=$C_FAINT _ptc=$C_FAINT
	test "$_lang" = en && _enc=$C_LINK || _ptc=$C_LINK
	tuish_text "$_lang_r" "$_lang_x" "English" fg=$_enc bg=$C_BG
	tuish_text "$_lang_r" $(( _lang_x + _we )) "$_sep" fg=$C_BORDER bg=$C_BG
	tuish_text "$_lang_r" $(( _lang_x + _we + _ws )) "Português" fg=$_ptc bg=$C_BG
	# The rule on the baseline (behind), then the g's descender leg on top of it —
	# like the site's h1 border-bottom crossing the g, its leg standing below.
	_rule "$_rule_r" "$_colc" "$_colw"
	_btext "$_rule_r" $(( _colc + 5 )) '_|' fg=$C_H1 bg=$C_BG
}

_max_scroll () { local _m=$(( _content_h - _ci_h )); test $_m -lt 0 && _m=0; echo $_m; }

# Paint one body row (0-based _i) from the content buffer. The canvas must be
# active and the row's background already laid: _draw_cline fills its own card
# background for code/entry rows but leaves plain text on whatever is beneath.
_paint_row ()   # $1 = 0-based row within the body
{
	local _i=$1 _row _raw _sty _txt
	_row=$(( _scroll + _i + 1 ))
	test "$_row" -ge 1 && test "$_row" -le "$_content_h" || return 0
	tuish_buf_get content "$_row"; _raw="$TUISH_BLINE"
	_sty="${_raw%%	*}"; _txt="${_raw#*	}"
	_draw_cline $(( _i + 1 )) "$_sty" "$_txt"
}

_draw_body ()
{
	tuish_draw_fill "$_ci_r" 1 "$_W" "$_ci_h" bg=$C_BG
	tuish_canvas "$_ci_r" "$_colc" "$_colw" "$_ci_h"
	local _i=0
	while test $_i -lt "$_ci_h"
	do
		_paint_row "$_i"
		_i=$(( _i + 1 ))
	done
	tuish_canvas_off
	_draw_cb_focus
	_draw_scrollbar
	# Record what is now on screen so a following scroll can shift instead of
	# repaint (see _render_body / _scroll_body).
	_scroll_drawn=$_scroll
	_geom_drawn="${_ci_r}:${_ci_h}:${_colc}:${_colw}"
	_body_dirty=0
}

# Shift the body with the terminal scroll region and repaint only the rows that
# scrolled into view. _d is the signed change in _scroll since the last paint:
# positive scrolls content up (new rows exposed at the bottom), negative down
# (new rows at the top). Caller guarantees 0 < |_d| < _ci_h and unchanged geometry.
_scroll_body ()   # $1 = signed scroll delta since the last body paint
{
	local _d=$1 _first _last _i _n
	tuish_scroll_region "$_ci_r" $(( _ci_r + _ci_h - 1 ))
	if test "$_d" -gt 0
	then tuish_scroll_up_n "$_d";              _first=$(( _ci_h - _d )); _last=$(( _ci_h - 1 ))
	else _n=${_d#-}; tuish_scroll_down_n "$_n"; _first=0;                _last=$(( _n - 1 ))
	fi
	tuish_reset_scroll
	# The rows the terminal freed carry ITS default background, not the app's, and
	# a plain-text line does not repaint the column's empty cells — so clear each
	# exposed row full width (covering the side margins) before painting content.
	_i=$_first
	while test "$_i" -le "$_last"
	do tuish_draw_fill $(( _ci_r + _i )) 1 "$_W" 1 bg=$C_BG; _i=$(( _i + 1 )); done
	tuish_canvas "$_ci_r" "$_colc" "$_colw" "$_ci_h"
	_i=$_first
	while test "$_i" -le "$_last"
	do _paint_row "$_i"; _i=$(( _i + 1 )); done
	tuish_canvas_off
	# Overlays live in the scrolled band too. The code-focus bar shifted with its
	# block, so redrawing it in place is correct; the scrollbar thumb tracks the
	# offset (not the content) and must be repainted wholesale over the shift.
	_draw_cb_focus
	_draw_scrollbar
	_scroll_drawn=$_scroll
}

# Mark the focused code block: an accent bar down its left padding and a copy hint
# on its top row (drawn after the canvas, in screen coords, on the card bg).
_draw_cb_focus ()
{
	test "$_csel" -ge 1 || return 0
	local _t _b; eval "_t=\$_cb_top_$_csel _b=\$_cb_bot_$_csel"
	local _rt=$(( _ci_r + _t - 1 - _scroll )) _rb=$(( _ci_r + _b - 1 - _scroll ))
	local _r=$_rt
	while test "$_r" -le "$_rb"
	do
		test "$_r" -ge "$_ci_r" && test "$_r" -lt $(( _ci_r + _ci_h )) \
			&& tuish_text "$_r" "$_colc" "▎" fg=$C_H2 bg=$C_CODEBG
		_r=$(( _r + 1 ))
	done
	# Hint on the block's top padding row, right-aligned in the card.
	if test "$_rt" -ge "$_ci_r" && test "$_rt" -lt $(( _ci_r + _ci_h ))
	then
		local _hint=" copy: y " _hc=$C_FAINT
		test "$_copyflash" -eq 1 && { _hint=" copied ✓ "; _hc=$C_H2; }
		tuish_str_width _hint; local _hw=$TUISH_SWIDTH
		tuish_text "$_rt" $(( _colc + _colw - _hw )) "$_hint" fg=$_hc bg=$C_CODEBG
	fi
	return 0
}

# Paint an alternating "style<US>text" piece list left→right from column _c.
_paint_pieces ()   # $1=row $2=col $3=payload $4=maxwidth
{
	local _r=$1 _col=$2 _rest=$3 _end=$(( $2 + $4 )) _sty _txt _fg _tw
	while test -n "$_rest"
	do
		_sty="${_rest%%${_US}*}"; _rest="${_rest#*${_US}}"
		case $_rest in
			*${_US}*) _txt="${_rest%%${_US}*}"; _rest="${_rest#*${_US}}";;
			*)        _txt="$_rest"; _rest='';;
		esac
		test "$_col" -ge "$_end" && break
		case $_sty in
			k) _fg=$C_LINK;; d) _fg=$C_FAINT;; m) _fg=$C_INLINE;; A) _fg=$C_H2;; *) _fg=$C_FG;;
		esac
		tuish_text "$_r" "$_col" "$_txt" fg=$_fg bg=$C_BG maxwidth=$(( _end - _col ))
		tuish_str_width _txt; _tw=$TUISH_SWIDTH
		_col=$(( _col + _tw ))
	done
}

# Like _paint_pieces, but on the code card, mapping the code-segment styles
# (build.sh _cmap) to the site's --code-* colours. Code does not wrap: it clips.
_paint_code ()   # $1=row $2=col $3=payload $4=maxwidth
{
	local _r=$1 _col=$2 _rest=$3 _end=$(( $2 + $4 )) _sty _txt _fg _tw
	while test -n "$_rest"
	do
		_sty="${_rest%%${_US}*}"; _rest="${_rest#*${_US}}"
		case $_rest in
			*${_US}*) _txt="${_rest%%${_US}*}"; _rest="${_rest#*${_US}}";;
			*)        _txt="$_rest"; _rest='';;
		esac
		test "$_col" -ge "$_end" && break
		case $_sty in
			K) _fg=$CC_KW;; S) _fg=$CC_STR;; C) _fg=$CC_CMT;; F) _fg=$CC_FN;;
			L) _fg=$CC_CLS;; N) _fg=$CC_NUM;; A) _fg=$CC_ATTR;; O) _fg=$CC_OP;;
			B) _fg=$CC_BI;; E) _fg=$CC_ERR;; *) _fg=$C_CODE;;
		esac
		tuish_text "$_r" "$_col" "$_txt" fg=$_fg bg=$C_CODEBG maxwidth=$(( _end - _col ))
		tuish_str_width _txt; _tw=$TUISH_SWIDTH
		_col=$(( _col + _tw ))
	done
}

_draw_cline ()   # $1 = canvas row  $2 = style  $3 = text/payload
{
	local _r=$1 _sty=$2 _txt=$3 _w=$_colw
	case $_sty in
		t)  _btext "$_r" 1 "$_txt" fg=$C_H1 bg=$C_BG maxwidth=$_w;;
		i)  tuish_text "$_r" 1 "$_txt" fg=$C_FAINT bg=$C_BG maxwidth=$_w;;
		h)  _btext "$_r" 1 "$_txt" fg=$C_H2 bg=$C_BG maxwidth=$_w;;
		c)  tuish_draw_fill "$_r" 1 "$_w" 1 bg=$C_CODEBG
		    _paint_code "$_r" 3 "$_txt" $(( _w - 4 ));;
		y)  tuish_draw_fill "$_r" 1 "$_w" 1 bg=$C_CODEBG;;
		g)  tuish_text "$_r" 1 "▧ $_txt" fg=$C_FAINT bg=$C_BG maxwidth=$_w;;
		d)  tuish_text "$_r" 3 "$_txt" fg=$C_FAINT bg=$C_BG maxwidth=$(( _w - 3 ));;
		r)  _rule "$_r" 1 "$_w";;
		S)  _paint_pieces "$_r" 1 "$_txt" "$_w";;
		E)  _draw_entry "$_r" "$_txt";;
		*)  ;;
	esac
}

_draw_entry ()   # $1 = canvas row  $2 = entry index
{
	local _r=$1 _idx=$2 _t _d _base _bg=$C_BG _fg=$C_LINK _w=$_colw
	eval "_t=\$_ent_title_$_idx _d=\$_ent_date_$_idx _base=\$_ent_base_$_idx"
	if test "$_idx" -eq "$_sel" || test "$_idx" -eq "$_ent_hover"
	then _bg=$C_SELBG; fi
	tuish_draw_fill "$_r" 1 "$_w" 1 bg=$_bg
	# Bulleted, like the site's <ul class=blog>: two spaces, bullet, one space, text.
	tuish_text "$_r" 3 "•" fg=$C_H2 bg=$_bg
	case $_base in
		@blog) tuish_text "$_r" 5 "$_t →" fg=$C_LINK bg=$_bg maxwidth=$(( _w - 4 )); return 0;;
	esac
	local _dw=0
	if test -n "$_d"; then tuish_str_width _d; _dw=$(( TUISH_SWIDTH + 2 )); fi
	tuish_text "$_r" 5 "$_t" fg=$_fg bg=$_bg maxwidth=$(( _w - 4 - _dw ))
	test -n "$_d" && tuish_text "$_r" $(( _w - _dw + 3 )) "$_d" fg=$C_FAINT bg=$_bg
}

# A thin scrollbar hugging the far-right column — the only bit of "chrome".
_draw_scrollbar ()
{
	test "$_content_h" -le "$_ci_h" && return 0
	local _track=$_ci_h
	local _thumb=$(( (_track * _track) / _content_h )); test $_thumb -lt 1 && _thumb=1
	local _m=$(( _content_h - _ci_h ))
	local _pos=$(( (_scroll * (_track - _thumb)) / (_m>0?_m:1) )) _i=0
	while test $_i -lt "$_track"
	do
		if test $_i -ge "$_pos" && test $_i -lt $(( _pos + _thumb ))
		then tuish_text $(( _ci_r + _i )) "$_sb_c" "▐" fg=$C_H2 bg=$C_BG
		else tuish_text $(( _ci_r + _i )) "$_sb_c" "▕" fg=$C_BORDER bg=$C_BG
		fi
		_i=$(( _i + 1 ))
	done
}

# ─── Actions ─────────────────────────────────────────────────────────
_scroll_by ()
{
	local _m; _m=$(_max_scroll); local _old=$_scroll
	_scroll=$(( _scroll + $1 ))
	test $_scroll -lt 0 && _scroll=0
	test $_scroll -gt $_m && _scroll=$_m
	test $_scroll -eq $_old && return 0
	_copyflash=0
	tuish_request_redraw 2
}

_ensure_visible ()
{
	test "$_sel" -ge 1 || return 0
	local _ln; eval "_ln=\$_ent_line_$_sel"
	if test "$_ln" -le "$_scroll"; then _scroll=$(( _ln - 1 ))
	elif test "$_ln" -gt $(( _scroll + _ci_h )); then _scroll=$(( _ln - _ci_h )); fi
	test "$_scroll" -ge 0 || _scroll=0
	return 0
}

# Tab moves the focused link/snippet (web-style), scrolling it into view. Blog
# lists cycle entries; posts cycle the copiable code blocks.
_focus_move ()
{
	if test "$_ent_n" -gt 0
	then
		local _new=$(( _sel + $1 ))
		test $_new -lt 1 && _new=$_ent_n
		test $_new -gt "$_ent_n" && _new=1
		_sel=$_new; _ensure_visible
	elif test "$_cb_n" -gt 0
	then
		local _new=$(( _csel + $1 ))
		test $_new -lt 1 && _new=$_cb_n
		test $_new -gt "$_cb_n" && _new=1
		_csel=$_new; _copyflash=0; _ensure_cb_visible
	else
		_scroll_by "$1"; return 0
	fi
	_body_dirty=1                   # selection highlight moved — not a pure scroll
	tuish_request_redraw 2
}

# Keep the focused code block's top in view.
_ensure_cb_visible ()
{
	test "$_csel" -ge 1 || return 0
	local _t _b; eval "_t=\$_cb_top_$_csel _b=\$_cb_bot_$_csel"
	if test "$_t" -le "$_scroll"; then _scroll=$(( _t - 1 ))
	elif test "$_b" -gt $(( _scroll + _ci_h )); then _scroll=$(( _b - _ci_h )); fi
	test "$_scroll" -ge 0 || _scroll=0
	return 0
}

# Copy the focused code block to the system clipboard (OSC 52, via clip.sh).
_copy_cb ()
{
	test "$_csel" -ge 1 || return 0
	local _t; eval "_t=\$_cb_text_$_csel"
	tuish_clip_set "$_t"
	_copyflash=1
	_body_dirty=1                   # copy hint changed in place — not a pure scroll
	tuish_request_redraw 2
}

_enter ()
{
	test "$_sel" -ge 1 && { _activate_entry "$_sel"; return 0; }
	test "$_csel" -ge 1 && _copy_cb
	return 0
}
_quit ()  { tuish_quit_clear; }
_relayout () { _lay; _text_w; test "$_TEXT_W" -ne "$_built_w" && _build_content; tuish_request_redraw -1; return 0; }

# ─── Mouse ───────────────────────────────────────────────────────────
_ent_at ()   # $1 = screen y  -> _ehit (0 = none)
{
	_ehit=0
	local _row=$(( _scroll + $1 - _ci_r + 1 )) _i=1 _ln
	test "$_row" -ge 1 && test "$_row" -le "$_content_h" || return 0
	while test $_i -le "$_ent_n"
	do
		eval "_ln=\$_ent_line_$_i"
		test "$_ln" -eq "$_row" && { _ehit=$_i; return 0; }
		_i=$(( _i + 1 ))
	done
}

# Which code block sits under screen row _y (0 = none).
_cb_at ()   # $1 = screen y  -> _cbhit
{
	_cbhit=0
	local _row=$(( _scroll + $1 - _ci_r + 1 )) _i=1 _t _b
	test "$_row" -ge 1 && test "$_row" -le "$_content_h" || return 0
	while test $_i -le "$_cb_n"
	do
		eval "_t=\$_cb_top_$_i _b=\$_cb_bot_$_i"
		test "$_row" -ge "$_t" && test "$_row" -le "$_b" && { _cbhit=$_i; return 0; }
		_i=$(( _i + 1 ))
	done
}

_click ()
{
	local _x=$TUISH_MOUSE_X _y=$TUISH_MOUSE_Y
	# Masthead: the alganet mark → home; the language switch → toggle.
	if test "$_y" -le "$_rule_r"
	then
		if test "$_y" -eq "$_lang_r" && test "$_x" -ge "$_lang_x"; then _toggle_lang
		elif test "$_y" -lt "$_rule_r" && test "$_x" -ge "$_colc" && test "$_x" -lt $(( _colc + _ARTW )); then _home; fi
		return 0
	fi
	# Content: a click on a link/entry opens it; a click on a code block copies it.
	if test "$_x" -ge "$_colc" && test "$_x" -lt $(( _colc + _colw )) && test "$_y" -ge "$_ci_r"
	then
		_ent_at "$_y"
		test "$_ehit" -gt 0 && { _sel=$_ehit; _activate_entry "$_sel"; return 0; }
		_cb_at "$_y"
		test "$_cbhit" -gt 0 && { _csel=$_cbhit; _copy_cb; }
	fi
	return 0
}

_hover ()
{
	local _x=$TUISH_MOUSE_X _y=$TUISH_MOUSE_Y _eh=0
	if test "$_x" -ge "$_colc" && test "$_x" -lt $(( _colc + _colw )) && test "$_y" -ge "$_ci_r"
	then _ent_at "$_y"; _eh=$_ehit; fi
	if test "$_eh" -ne "$_ent_hover"; then _ent_hover=$_eh; _body_dirty=1; tuish_request_redraw 2; fi
}

# ─── Idle: first paint ───────────────────────────────────────────────
_idle () { if test "$_started" = no; then _started=yes; _lay; _build_content; _render; return 0; fi; }

_reader_on_event ()
{
	if test "$TUISH_EVENT_KIND" = signal; then _relayout; return 0; fi
	tuish_dispatch || :
	return 0
}
_reader_on_redraw ()
{
	if   test "$1" = 2; then _render_body
	elif test "$1" = 1; then _render_mast
	else _render; fi
}

# ─── Bindings ────────────────────────────────────────────────────────
tuish_bind 'idle'    '_idle'
tuish_bind 'resize'  '_relayout'
tuish_bind 'ctrl-w'  '_quit'
tuish_bind 'char q'  '_quit'
tuish_bind 'up'      '_scroll_by -1'
tuish_bind 'down'    '_scroll_by 1'
tuish_bind 'char k'  '_scroll_by -1'
tuish_bind 'char j'  '_scroll_by 1'
tuish_bind 'pgup'    '_scroll_by -8'
tuish_bind 'pgdn'    '_scroll_by 8'
tuish_bind 'whup'    '_scroll_by -3'
tuish_bind 'wdown'   '_scroll_by 3'
tuish_bind 'home'    '_scroll=0; tuish_request_redraw 2'
tuish_bind 'tab'     '_focus_move 1'
tuish_bind 'backtab' '_focus_move -1'
tuish_bind 'enter'   '_enter'
tuish_bind 'char y'  '_copy_cb'
tuish_bind 'char c'  '_copy_cb'
tuish_bind 'char l'  '_toggle_lang'
tuish_bind 'char g'  '_home'
tuish_bind 'char w'  '_view_html'
tuish_bind 'char b'  '_back'
tuish_bind 'esc'     '_back'
tuish_bind 'lclik'   '_click'
tuish_bind 'move'    '_hover'

# ─── Boot ────────────────────────────────────────────────────────────
for _a in "$@"
do
	case $_a in
		en|pt)  _lang=$_a;;
		home)   _view=home;;
		blog)   _view=list;;
		post:*) _view=post; _post_base="${_a#post:}";;
	esac
done
_load
_report_page

tuish_init
tuish_on_redraw _reader_on_redraw
tuish_on_event  _reader_on_event
tuish_mouse_on
tuish_viewport fullscreen
tuish_run || :
tuish_fini

# Native mode leaves a temp dir of fetched toolkit modules + the page buffer.
test "$_NATIVE" -eq 1 && rm -rf "$_src"
exit 0
