#!/bin/sh
# Copyright (c) 2026 Jiaxing Hu <gahing@gahingwoo.com>
# SPDX-License-Identifier: GPL-2.0
#
# charsiu-config: a front end to config.ini -- $HOME/.charsiu's if there is one,
# /etc/charsiu's otherwise -- and to the charsiu tools.
#
# NO CURSES. kiln's equivalent is a whiptail TUI because kiln runs on Armbian,
# which has one. charsiu's rootfs is a minimal buildroot with no whiptail, no
# dialog, no newt and no slang, and the board is normally driven over a
# 1.5 Mbaud serial console where a full-screen redraw is worse than a numbered
# menu anyway. So the menu is plain text and depends on nothing. If whiptail IS
# present (running this on a real distro) it is used instead.
#
# config.ini stays the hand-editable source of truth: every write goes through
# an in-place editor that preserves comments and keys this script never heard of.
set -u

CHARSIU_HERE=$(dirname "$0")
for _l in ${CHARSIU_LIB:+"$CHARSIU_LIB/charsiu-lib.sh"} \
	  "$CHARSIU_HERE/charsiu-lib.sh" \
	  "$CHARSIU_HERE/../scripts/charsiu-lib.sh" \
	  "$CHARSIU_HERE/../../opt/charsiu/charsiu-lib.sh" \
	  /opt/charsiu/charsiu-lib.sh; do
	[ -r "$_l" ] && { . "$_l"; break; }
done
command -v ini_get >/dev/null 2>&1 \
	|| { echo "$(basename "$0"): charsiu-lib.sh not found" >&2; exit 1; }

# WHERE TO READ AND WHERE TO WRITE ARE NOT THE SAME QUESTION. charsiu_conf_w
# copies an unwritable system config into $HOME rather than replacing the NAME
# with one that does not exist, which is what made this screen say
# "now: (unset)" on a board whose config was fine and whose model was running.
CONF=$(charsiu_conf_w)
[ -f "$CONF" ] || CONF=$(charsiu_conf)
MODELS_DIR="${CHARSIU_MODELS_DIR:-${CHARSIU_MODELS:-$CHARSIU_HOME/models}}"
SELFURL=https://raw.githubusercontent.com/gahingwoo/charsiu/stable/scripts/charsiu-install.sh
for t in ${CHARSIU_LIB:+"$CHARSIU_LIB/charsiu-tui.sh"} \
	 "$(dirname "$0")/charsiu-tui.sh" \
	 "$(dirname "$0")/../scripts/charsiu-tui.sh" \
	 "$(dirname "$0")/../opt/charsiu/charsiu-tui.sh" \
	 /opt/charsiu/charsiu-tui.sh; do
	[ -r "$t" ] && { . "$t"; break; }
done
command -v ui_menu >/dev/null 2>&1 || { echo "charsiu-tui.sh not found" >&2; exit 1; }
CTUI_TITLE="charsiu"

here=$(dirname "$0")

if [ -t 1 ]; then
	C_B=$(printf '\033[1m'); C_G=$(printf '\033[1;32m')
	C_Y=$(printf '\033[1;33m'); C_D=$(printf '\033[2m'); C_0=$(printf '\033[0m')
else C_B=; C_G=; C_Y=; C_D=; C_0=; fi

# ---------------------------------------------------------------------------
# ini_set lives in charsiu-lib.sh: charsiu update writes settings too now.

# A cancelled ui_input must not silently write an empty value into the
# config. ask keeps the old value when the user backs out.
ask() {   # ask PROMPT DEFAULT -> the answer, or DEFAULT if cancelled
	v=$(ui_input "$1" "$2") || v="$2"
	[ -n "$v" ] && echo "$v" || echo "$2"
}

pause() { [ "$CTUI" = whiptail ] || { printf '\n%spress enter%s ' "$C_D" "$C_0"; read -r _ || true; }; }

# ---------------------------------------------------------------------------
page_status() {
	D=$(find_bin charsiu-doctor)
	if [ -n "$D" ]; then CHARSIU_CONFIG="$CONF" "$D" || true
	else echo "  charsiu-doctor not found"; fi
	pause
}

page_model() {
	while :; do
		cur=$(ini_get model path)
		set --
		# BOTH DIRECTORIES. Every other place in this file already looks
		# in /opt/charsiu/models as well, and only this screen did not, so
		# the board's own model was invisible to the one page whose job is
		# to pick it: the menu offered [get] and nothing else.
		for f in "$MODELS_DIR"/*.gguf /opt/charsiu/models/*.gguf; do
			[ -f "$f" ] || continue
			case " $* " in *" $(basename "$f") "*) continue ;; esac
			mb=$(( $(file_bytes "$f") / 1048576 ))
			mark=""; [ "$f" = "$cur" ] && mark="  <- in use"
			set -- "$@" "$(basename "$f")" "${mb}M$mark"
		done
		set -- "$@" "[get]" "download one" "[check]" "gate a file by hand" "[back]" ""
		sel=$(ui_menu "Model
now: ${cur:-(unset)}" "$@") || return
		case "$sel" in
		"[back]"|"") return ;;
		"[get]")
			G=$(find_bin charsiu-get)
			[ -n "$G" ] && CHARSIU_MODELS_DIR="$MODELS_DIR" CHARSIU_LIB="${CHARSIU_LIB:-/opt/charsiu}" "$G" --wizard
			pause ;;
		"[check]")
			CK=$(find_bin charsiu_check)
			f=$(ask "file to check" "$cur")
			[ -n "$CK" ] && "$CK" "$f"; pause ;;
		*)
			for _d in "$MODELS_DIR" /opt/charsiu/models; do
				[ -f "$_d/$sel" ] && { sel="$_d/$sel"; break; }
			done
			# never point the config at a file the runtime cannot load
			CK=$(find_bin charsiu_check)
			if [ -n "$CK" ] && ! out=$("$CK" -q "$sel" 2>&1); then
				ui_msg "Not selected.

charsiu cannot run that file:
  $out"
				continue
			fi
			ini_set model path "$sel"
			ui_note "set: $(basename "$sel")" ;;
		esac
	done
}

page_run() {
	printf '\n%sRun%s\n' "$C_B" "$C_0"
	t=$(ask "threads" "$(ini_get run threads)");  ini_set run threads "$t"
	c=$(ask "context, in tokens" "$(ini_get run context)"); ini_set run context "$c"
	printf '  %sRK3576 is 4x A53 at 0-3 and 4x A72 at 4-7. Round 379 measured the\n' "$C_D"
	printf '  A53s at 11.17 tok/s against 14.59, with no energy saving. Keep 4-7.%s\n' "$C_0"
	p=$(ask "cpus" "$(ini_get run cpus)"); ini_set run cpus "$p"
	pause
}

page_npu() {
	printf '\n%sNPU%s\n' "$C_B" "$C_0"
	e=$(ask "use the NPU (yes/no)" "$(ini_get npu enable)"); ini_set npu enable "$e"
	printf '  %sone value, written to both CHARSIU_NPU_KMAX and CHARSIU_NPU_W4_GROUP.\n' "$C_D"
	printf '  The group has to divide a tensor K or that tensor gets one scale\n'
	printf '  for its whole row, and the slice can be no wider than the group.%s\n' "$C_0"
	k=$(ask "K slice" "$(ini_get npu kslice)"); ini_set npu kslice "$k"
	m=$(ask "max N" "$(ini_get npu maxn)"); ini_set npu maxn "$m"
	f=$(ask "coefficient elements" "$(ini_get npu coef_elems)"); ini_set npu coef_elems "$f"
	printf '  %sgroup fit gives a tensor whose K misses the width above the widest\n' "$C_D"
	printf '  divisor of its own K instead of one scale a row. Measured on eight\n'
	printf '  models: two are better on BOTH perplexity and decode, three are\n'
	printf '  identical to the digit, two are worse. It is worth trying on your\n'
	printf '  model and scoring it, and it is off because two of eight lose.%s\n' "$C_0"
	g=$(ask "group fit (yes/no)" "$(ini_get npu group_fit)"); ini_set npu group_fit "$g"
	pause
}

page_debug() {
	printf '\n%sDebug%s\n' "$C_B" "$C_0"
	printf '  %sOn, every run prints where its time went per stage, and\n' "$C_D"
	printf '  `charsiu bench` also measures whether batching the projections\n'
	printf '  pays on this machine. Off by default: the timings cost about\n'
	printf '  5 us a token, enough to spoil a timing round.%s\n' "$C_0"
	e=$(ask "debug (yes/no)" "$(ini_get debug enable)"); ini_set debug enable "$e"
	pause
}

page_show() {
	L=$(find_bin charsiu)
	printf '\n%sThe command this config produces%s\n\n' "$C_B" "$C_0"
	[ -n "$L" ] && CHARSIU_CONFIG="$CONF" "$L" --show -p "The capital of France is" -n 64 \
		     || echo "  charsiu launcher not found"
	pause
}

# ---------------------------------------------------------------------------
# FIRST RUN. Not a separate program and not an init script: an init script would
# fight the round harness for the console on a test board, and a second program
# would drift from this one. `charsiu-config --setup` walks the same pages in
# the order a new board needs them.
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# FIRST RUN, as a list of steps rather than a queue of questions.
#
# Taken from codex's onboarding (codex-rs/tui/src/onboarding/): a list of
# steps, each of which ASSESSES ITSELF as hidden, complete or in progress. A
# step that is already satisfied is never shown, a finished one stays on screen
# as a record of what happened, and exactly one is being worked on. Somebody
# coming back a second time sees only what is left, which is the difference
# between a wizard and an interrogation.
#
# And from crush's splash: the empty state SHOWS EXAMPLES. A bare ">" teaches
# a new owner of this board nothing at all; three things they can type teach
# them the whole idea.
#
#   step_<name>_state  -> hidden | done | todo
#   step_<name>_label  -> one line, for the trail
#   step_<name>_do     -> make it done
# ---------------------------------------------------------------------------
STEPS="kernel model pick ready"

# NOT A TODO. Nothing this screen can do makes an NPU appear, and a step that
# reports "todo" forever is picked again on every pass -- which is exactly what
# the first version did, looping on the same message. codex's model has a place
# for this: a COMPLETE step whose label carries the finding. It is checked, it
# is reported, and the run continues on the CPU.
step_kernel_state() { echo done; }
step_kernel_label() {
	if [ -e "${CHARSIU_ACCEL_DEV:-/dev/accel/accel0}" ]; then
		echo "the NPU is here and this kernel drives it"
	else
		echo "no NPU here: it will run on the CPU, about three times slower"
	fi
}
step_model_state() {
	[ -n "$(ls "$MODELS_DIR"/*.gguf /opt/charsiu/models/*.gguf 2>/dev/null | head -1)" ] \
		&& echo done || echo todo
}
step_model_label() {
	n=$(ls "$MODELS_DIR"/*.gguf /opt/charsiu/models/*.gguf 2>/dev/null | wc -l)
	[ "$n" -gt 0 ] && echo "$n model(s) on this board" || echo "no model yet"
}
step_model_do() {
	G=$(find_bin charsiu-get)
	[ -n "$G" ] || { ui_msg "charsiu-get is missing."; return 1; }
	CHARSIU_MODELS_DIR="$MODELS_DIR" CHARSIU_LIB="${CHARSIU_LIB:-/opt/charsiu}" \
		"$G" --wizard
}

step_pick_state() {
	[ "$(step_model_state)" = done ] || { echo hidden; return; }
	cur=$(ini_get model path)
	[ -n "$cur" ] && [ -f "$cur" ] && echo done || echo todo
}
step_pick_label() {
	cur=$(ini_get model path)
	[ -n "$cur" ] && [ -f "$cur" ] && echo "using $(basename "$cur")" \
	                              || echo "no model chosen"
}
step_pick_do() {
	# One model and nothing to decide: choose it and say so, rather than
	# opening a menu with one row in it.
	only=$(ls "$MODELS_DIR"/*.gguf /opt/charsiu/models/*.gguf 2>/dev/null)
	n=$(echo "$only" | wc -l)
	if [ "$n" = 1 ]; then
		ini_set model path "$only"
		ui_note "using $(basename "$only")"
		return 0
	fi
	page_model
}

step_ready_state() {
	[ "$(step_pick_state)" = done ] && echo todo || echo hidden
}
step_ready_label() { echo "ready"; }
step_ready_do() {
	mkdir -p "$(dirname "$STAMP")" 2>/dev/null || true
	: > "$STAMP" 2>/dev/null || true
	# crush's lesson: do not hand anyone an empty prompt.
	hint=""
	[ -e "${CHARSIU_ACCEL_DEV:-/dev/accel/accel0}" ] || hint="

There is no NPU on this kernel, so this runs on the CPU. To fix
that, /opt/charsiu/charsiu-install.sh can fetch a kernel that has
the driver."
	ui_msg "Ready.$hint

Type  charsiu  and ask it something. To start with, try:

    what can you do
    explain what an NPU is, briefly
    write a haiku about a circuit board

  charsiu list     what is on this board
  charsiu pull     get another model
  charsiu doctor   when something looks wrong"
	return 0
}

setup_walk() {
	ui_msg "charsiu

An open runtime for the RK3576 NPU. The kernel driver is mainline
rocket and the userspace is this: no vendor stack anywhere in the
path.

A few things to check, then it runs."

	# up to one pass per step, plus one more so a step that was unblocked by
	# an earlier one gets its turn
	pass=0
	while [ "$pass" -lt 3 ]; do
		pass=$((pass + 1))
		todo=0
		trail=""
		for st in $STEPS; do
			s=$("step_${st}_state")
			l=$("step_${st}_label")
			case "$s" in
			hidden) continue ;;
			done)   trail="$trail
  ${T_G}done${T_0}  $l" ;;
			todo)   trail="$trail
  ${T_Y}next${T_0}  $l" ;;
			esac
		done
		[ -n "$trail" ] && printf '%s\n' "$trail" >&2

		for st in $STEPS; do
			[ "$("step_${st}_state")" = todo ] || continue
			todo=1
			"step_${st}_do" || true
			break
		done
		[ "$todo" = 0 ] && break
	done
	return 0
}

STAMP="${CHARSIU_STAMP:-$(dirname "$CONF")/.setup-done}"
case "${1:-}" in
"")           : ;;
--setup)      setup_walk; exit $? ;;
--setup-once) [ -e "$STAMP" ] && exit 0; setup_walk; exit $? ;;
-h|--help)
	# THIS WAS NOT HANDLED, so `charsiu-config --help` fell through to the
	# menu and opened a full-screen dialog. On a board whose test harness
	# logs to the same console that is not a small thing: whiptail clears the
	# screen, and round 381 lost its first two arms to it.
	cat <<'USAGE'
charsiu-config: pick a model, threads, context, and the NPU switches.

  charsiu-config              the menu
  charsiu-config --setup      the first-run walkthrough
  charsiu-config --setup-once the same, but only if it has never been run
  charsiu-config --help       this

  CHARSIU_CONFIG      the file to edit (default /etc/charsiu/config.ini)
  CHARSIU_MODELS_DIR  where to look for models
  CHARSIU_PLAIN=1     no full-screen dialogs, plain text instead
USAGE
	exit 0 ;;
*)
	echo "charsiu-config: unknown argument: $1" >&2
	echo "try: charsiu-config --help" >&2
	exit 2 ;;
esac

while :; do
	c=$(ui_menu "charsiu-config    $CONF" \
		status "what works and what does not" \
		model  "pick, fetch, or check one" \
		run    "threads, context, cores" \
		npu    "the switches that decide the fast path" \
		show   "the exact command this produces" \
		debug  "stage timings, and the batching benchmark" \
		chat   "run it now" \
		update "fetch the latest charsiu and reinstall it" \
		quit   "") || exit 0
	case "$c" in
	status) page_status ;;
	model)  page_model ;;
	run)    page_run ;;
	npu)    page_npu ;;
	show)   page_show ;;
	debug)  page_debug ;;
	chat)   L=$(find_bin charsiu); [ -n "$L" ] && CHARSIU_CONFIG="$CONF" "$L" --chat || true; pause ;;
	update)
		# IT REPLACES THE SCRIPTS THIS MENU IS RUNNING FROM, so hand
		# over with exec rather than returning into a page that is no
		# longer the one on disk.
		U=$(find_bin charsiu-update)
		if [ -z "$U" ]; then
			ui_msg "charsiu-update is not installed.

This charsiu predates it. Install again and it will be there:
  curl -fsSL $SELFURL -o install.sh && sh install.sh"
			pause
		else
			exec "$U"
		fi ;;
	quit|"") exit 0 ;;
	esac
done
