main glub
  1#!/bin/mksh
  2# glub - IRC client in mksh
  3# Usage: glub [-h] [-s server] [-p port] [-unr name] [-c channels] [-U sasl_user] [-P sasl_pass]
  4
  5# shellcheck shell=ksh # Well, almost.
  6# Avoid unwanted surprises...
  7set -o noglob
  8
  9typeset -a bufs bhist
 10typeset -i port=6697 curbuf=0 scroll=0
 11typeset srv="irc.ergo.chat" user="$USER" nick="$USER" real="$USER"
 12typeset sasl_user sasl_pass
 13
 14typeset pref cmd trail ctcp input
 15typeset -i cursor
 16typeset -a params
 17
 18trap 'die' INT TERM QUIT
 19
 20help() {
 21	cat << EOF
 22Usage: ${0##*/} [-s server] [-p port] [-unr name] [-c channels] [-U sasl_user] [-P sasl_pass]
 23
 24Options:
 25  -h             Print this help message
 26  -s server      Server to connect to (irc.ergo.chat by default)
 27  -p port        Port to use when connecting (6697 by default)
 28  -unr name      Username, nick and realname to use (\$USER by default)
 29  -c channels    Comma-separated list of channels to join upon connection
 30  -U sasl_user,
 31  -P sasl_pass   Credentials to use when authenticating with SASL
 32EOF
 33}
 34
 35termin() {
 36	printf '\e[?1049h\e[2J\e[1;%dr\e[H\e[%d;1H' $((LINES - 1)) $LINES
 37	stty -echo -isig -icanon -iexten
 38}
 39
 40termout() {
 41	printf '\e[;r\e[?1049l'
 42	stty iexten icanon isig echo
 43}
 44
 45die() {
 46	exec 3>&-; exec 4>&-
 47	kill $brpid 2>/dev/null; wait $brpid 2>/dev/null
 48	termout
 49}
 50
 51prin() {
 52	printf '\e7\e[%d;1H\eD%b\e8' $((LINES - 1)) "$1"
 53}
 54
 55prompt() {
 56	local b
 57	[ "${bufs[curbuf]}" = '_' ] && b="<server>" || b="${bufs[curbuf]}"
 58	printf '\e[%d;1H\e[2K%s → %s' $LINES "$b" "$input"
 59}
 60
 61nclr() {
 62	local h="${1@#}" clr i
 63	clr=(31 32 33 34 35 36 91 92 93 94 95 96)
 64	i=$((0x$h % ${#clr[@]}))
 65	((i < 0)) && ((i = -i))
 66	printf '%d' "${clr[i]}"
 67}
 68
 69parse() {
 70	local line="$1"
 71	pref="" cmd="" trail="" ctcp=""
 72	params=()
 73	line="${line%$'\r'}"
 74
 75	if [[ "$line" == ':'* ]]; then
 76		pref="${line#*:}" pref="${pref%% *}"
 77		line="${line#* }"
 78	fi
 79
 80	cmd="${line%% *}"
 81	line="${line#* }"
 82
 83	if [[ "$line" == *' :'* ]]; then
 84		trail="${line#* :}"
 85		line="${line% " $trail"}"
 86	fi
 87
 88	[[ "$trail" == $'\x01'*$'\x01' ]] && \
 89		ctcp="${trail#$'\x01'}" ctcp="${ctcp%$'\x01'}"
 90
 91	# shellcheck disable=2086 # Word splitting is intentional here.
 92	set -A params -- $line
 93}
 94
 95addbuf() {
 96	local name="$1" i
 97	# Make sure we have no buffer with the same name.
 98	for i in "${!bufs[@]}"; do
 99		[[ "${bufs[i]}" == "$name" ]] && return
100	done
101	bufs+=("$name")
102	bhist[${#bufs[@]} - 1]=""
103}
104
105bmsg() {
106	if [ -z "${bhist[$1]+x}" ]; then
107		bhist[$1]="$2"
108	else
109		bhist[$1]+=$'\n'"$2"
110	fi
111}
112
113status() {
114	# shellcheck disable=2155 # I don't need those.
115	local s="$(printf '\e[90m%13s  %s\e[0m' '::' "$2")"
116	bmsg "$1" "$s"
117	((curbuf == $1)) && prin "$s"
118}
119
120bprint() {
121	local lines="${bhist[$1]}" c=0 line
122	while IFS= read -r line; do ((c++)); done <<< "$lines"
123
124	local st=$((c - (LINES - 1) + 1 - scroll))
125	((st < 1)) && st=1; ((st > c)) && ((st = c))
126
127	printf '\e[%d;1H\e[2J' $((LINES - 1))
128	local i=0
129	while IFS= read -r line; do
130		((i++))
131		((i < st)) && continue
132		printf '\n%b' "$line"
133	done <<< "$lines"
134}
135
136buf() {
137	local i
138	for i in "${!bufs[@]}"; do
139		if [[ "${bufs[i]}" == "$1" ]]; then
140			((curbuf = i))
141			bprint "$i"
142			return
143		fi
144	done
145
146	status "$curbuf" "No such buffer: $1"
147}
148
149b64() {
150	local a='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\
151+/'
152	local i=0
153	while ((i < ${#1})); do
154		# shellcheck disable=2155 # Sigh...
155		local b1=$(printf '%d' "'${input:i:1}")
156		# shellcheck disable=2155 # Siiiiighh.....
157		local b2=$(printf '%d' "'${input:i+1:1}")
158		# shellcheck disable=2155 # SIIIIIGHHHH.....
159		local b3=$(printf '%d' "'${input:i+2:2}")
160		b2=${b2:-0} b3=${b3:-0}
161
162		local bits=$(((b1 << 16) | (b2 << 8) | b3))
163		local c1=$(((bits >> 18) & 63)) c2=$(((bits >> 12) & 63))
164		local c3=$(((bits >> 6) & 63)) c4=$((bits & 63))
165		local rem=$((${#1} - i))
166
167		case "$rem" in
168			1) printf '%s%s==' "${a:c1:1}" "${a:c2:1}" ;;
169			2) printf '%s%s%s=' "${a:c1:1}" "${a:c2:1}" \
170				"${a:c3:1}" ;;
171			*) printf '%s%s%s%s' "${a:c1:1}" "${a:c2:1}" \
172				"${a:c3:1}" "${a:c4:1}" ;;
173		esac
174		((i += 3))
175	done
176}
177
178input() {
179	local line="$1"
180	prompt
181	[ -z "$line" ] && return
182
183	[[ "$line" != '/'* ]] && {
184		case "${bufs[curbuf]}" in ''|'_'|@*)
185			status 0 "Cannot send messages to server"
186			return
187			;;
188		esac
189
190		print -ru3 "PRIVMSG ${bufs[curbuf]} :${line}"
191		# shellcheck disable=2155
192		local hl="\e[$(nclr "$nick")m"
193		[[ "$line" == *"$nick"* ]] && hl="\e[7m${hl}"
194		local l=$((13 - ${#nick} - 1))
195		# shellcheck disable=2155
196		local s="$(printf '%*s%b %s \e[0m %s' "$l" '' "$hl" "$nick" \
197			"$line")"
198		bmsg "$curbuf" "$s"
199		prin "$s"
200		return
201	}
202
203	local cmd="${line%% *}" args="${line#* }"
204	[ "$args" = "$cmd" ] && args=""
205
206	case "$cmd" in
207		/b|/buffer)
208			local sub="${args%% *}" rest="${args#* }"
209			[[ "$args" == "$sub" ]] && rest=""
210
211			case "$sub" in
212				next)
213					((${#bufs[@]} <= 1)) && {
214						status "$curbuf" \
215							"No next buffer"
216						return
217					}
218					((curbuf = (curbuf + 1) % ${#bufs[@]}))
219					bprint "$curbuf"
220					;;
221				prev)
222					((${#bufs[@]} <= 1)) && {
223						status "$curbuf" \
224							"No previous buffer"
225						return
226					}
227					((curbuf = (curbuf - 1 + ${#bufs[@]}) \
228						 % ${#bufs[@]}))
229					bprint "$curbuf"
230					;;
231				*) buf "$sub" ;;
232			esac
233			;;
234		/h|/help)
235			status "$curbuf" "List of commands:"
236			# shellcheck disable=2155 # I don't care...
237			local m="$(cat <<EOF
238/h(elp), /b(uffer) prev/next/<name>, /j(oin) <#channel>, /me <action>, \
239/nick <nickname>, /p(art) [#channel], /q(uery) <nickname>, /quit, /quote <raw>
240EOF
241)"
242			status "$curbuf" "$m"
243			;;
244		/j|/join)
245			local target="${args%% *}"
246			[ -z "$target" ] && {
247				status "$curbuf" "/join: Join what?"
248				return
249			}
250			print -ru3 "JOIN $target"
251			addbuf "$target"
252			buf "$target"
253			;;
254		/me)
255			local target="${bufs[curbuf]}"
256			print -u3 "PRIVMSG $target :\x01ACTION $args\x01"
257			status "$curbuf" "$nick $args"
258			;;
259		/nick)
260			local new="${args%% *}"
261			print -ru3 "NICK ${new}"
262			nick="$new"
263			;;
264		/p|/part)
265			local target="${args%% *}"
266			target="${target:-${bufs[curbuf]}}"
267			[ "$target" = '_' ] && {
268				status 0 "Can't part in this buffer"
269				return
270			}
271			[[ "$target" == '#'* ]] && print -ru3 "PART ${target}"
272			local n=-1
273			for i in "${!bufs[@]}"; do
274				[ "${bufs[i]}" = "$target" ] && { n=$i; break; }
275			done
276			((n < 0)) && return
277			unset 'bufs[n]' 'bhist[n]'
278
279			local -a nb nh
280			for i in "${!bufs[@]}"; do
281				nb+=("${bufs[i]}")
282				nh+=("${bhist[i]}")
283			done
284			bufs=("${nb[@]}")
285			bhist=("${nh[@]}")
286
287			((${#bufs[@]} == 0)) && {
288				bufs=('_')
289				bhist=('')
290				curbuf=0
291				bprint "$curbuf"
292				return
293			}
294
295			((curbuf >= ${#bufs[@]})) && ((curbuf = ${#bufs[@]}-1))
296			bprint "$curbuf"
297			;;
298		/q|/query)
299			local target="${args%% *}"
300			[ -z "$target" ] && {
301				status "$curbuf" "/query: Query who?"
302				return
303			}
304			addbuf "$target"
305			buf "$target"
306			;;
307		/quit)
308			print -ru3 "QUIT :${args:-Client quit}"
309			return 1
310			;;
311		/quote)
312			print -u3 "$args"
313			;;
314		*)
315			status "$curbuf" "Unknown command: $cmd, see /help"
316			;;
317	esac
318}
319
320command -v brssl &>/dev/null || {
321	print -u2 "brssl not found, exiting"
322	exit 1
323}
324
325while getopts :hs:p:u:n:r:c:U:P: arg; do case "$arg" in
326	'h') help; exit 0 ;;
327	's') srv="$OPTARG" ;;
328	'p') port=$OPTARG ;;
329	'u') user="$OPTARG" ;;
330	'n') nick="$OPTARG" ;;
331	'r') real="$OPTARG" ;;
332	'c') IFS=',' read -rA chans <<< "$OPTARG" ;;
333	'U') sasl_user="$OPTARG" ;;
334	'P') sasl_pass="$OPTARG" ;;
335	'?') printf 'Unknown option -%c\n' "$arg"; exit 1 ;;
336	':') printf 'Argument needed for -%c\n' "$arg"; exit 1 ;;
337esac; done
338shift $((OPTIND - 1))
339
340bufs=('_')
341
342termin
343prompt
344status 0 "Welcome to glub! See /help for a summary of commands."
345status 0 "Connecting to ${srv}:${port}..."
346
347# Extremely shitty hack to assign 2 FDs for the read/write end of the pipe.
348t1=$(mktemp -u) t2=$(mktemp -u)
349mkfifo "$t1"; mkfifo "$t2"
350
351brssl client -q "$srv":$port <"$t1" >"$t2" & brpid=$!
352exec 3>"$t1"; exec 4<"$t2"
353rm -f "$t1" "$t2"
354
355print -rlu3 "CAP LS 302" "NICK ${nick}" \
356	"USER ${sasl_user:-"$user"} 0 * ${real}" "CAP REQ :sasl" "CAP END"
357for ch in "${chans[@]}"; do print -rlu3 "JOIN ${ch}"; done
358
359# shellcheck disable=SC2154 # False positive.
360while :; do if IFS='' read -ru4 -t0.02 line; then
361	[ -z "$line" ] && continue
362	parse "$line"
363
364	case "$cmd" in
365		CAP)
366			[ "${params[1]}" = "ACK" ] && [ "$trail" = "sasl" ] && {
367				print -lu3 "AUTHENTICATE PLAIN" \
368					"AUTHENTICATE $(b64 "$sasl_pass")"
369			}
370			;;
371		PING)
372			s="${params[0]:-}" s="${s#:}"
373			print -ru3 "PONG $s"
374			;;
375		PRIVMSG|NOTICE)
376			src="$pref" target="${params[0]}" text="$trail"
377			[ -n "$ctcp" ] && {
378				msg="${ctcp%% *}" args="${ctcp#"${msg}" }"
379				case "$msg" in
380					"ACTION")
381						s="${src%!*} $args"
382						for c in "${!bufs[@]}"; do
383							# lol
384							[ "${bufs[c]}" = \
385							  "$target" ] && \
386							  status "$c" "$s"
387						done
388						;;
389					"CLIENTINFO")
390						print -u3 "NOTICE $src :\
391CLIENTINFO ACTION CLIENTINFO TIME VERSION"
392						;;
393					"TIME")
394						print -u3 "NOTICE $src :\
395TIME $(date -uR)"
396						;;
397					"VERSION")
398						print -u3 "NOTICE $src :\
399glub v0 (see https://git.sr.ht/wf/glub)"
400						;;
401				esac
402				continue
403			}
404			from="${src%%!*}"
405			hl="\e[$(nclr "$from")m"
406			[[ "$text" == *"$nick"* ]] && hl="\e[7m${hl}"
407			l=$((13 - ${#from} - 1))
408			s="$(printf '%*s%b %s \e[0m %s' $l '' "$hl" \
409				"$from" "$text")"
410			[ "$target" == "$nick" ] && {
411				for i in "${!bufs[@]}"; do
412					[ "${bufs[i]}" = "$from" ] && {
413						bmsg "$i" "$s"
414						((i == curbuf)) && prin "$s"
415						break
416					}
417				done
418				continue
419			}
420
421			addbuf "$target"
422			for i in "${!bufs[@]}"; do
423				[ "${bufs[i]}" = "$target" ] && {
424					bmsg "$i" "$s"
425					[ "${bufs[curbuf]}" = "$target" ] && \
426						prin "$s"
427					break
428				}
429			done
430			;;
431		JOIN)
432			src="$pref" chan="${params[0]}"
433			s="${src%%!*} joined"
434			addbuf "$chan"
435			for i in "${!bufs[@]}"; do
436				[ "${bufs[i]}" = "$chan" ] && {
437					status "$i" "$s"
438					break
439				}
440			done
441			;;
442		PART)
443			src="$pref" chan="${params[0]}" reason="$trail"
444			s="${src%%!*} left${reason:+" ($reason)"}"
445			for i in "${!bufs[@]}"; do
446				[ "${bufs[i]}" = "$chan" ] && {
447					status "$i" "$s"
448					break
449				}
450			done
451			;;
452		QUIT)
453			# src="$pref" reason="$trail"
454			# s="${src%%!*} quit${reason:+" ($reason)"}"
455			# I didn't find a use case for this.
456			;;
457		NICK)
458			src="$pref" new="${params[0]}"
459			s="${src%%!*}→$new"
460			for i in "${!bufs[@]}"; do
461				[ "${bufs[i]}" = '_' ] && continue
462				status "$i" "$s"
463				break
464			done
465			;;
466		[0-9][0-9][0-9])
467			code="$cmd" msg="$trail"
468			s=()
469			[ -z "$msg" ] && msg="${params[*]}"
470
471			# status 0 "[$code] ${params[*]}"
472
473			target=""
474			case "$code" in
475				00[1-3]) # RPL_WELCOME, RPL_YOURHOST,
476					 # RPL_CREATED
477					s=("$msg")
478					;;
479				212) # RPL_STATSCOMMANDS
480					s=("Stats for ${params[1]}: \
481${params[2]} uses${params[3]:+", ${params[3]} bytes"}\
482${params[4]:+", ${params[4]} uses"}")
483					;;
484				221) # RPL_UMODEIS
485					s=("Your user modes are: ${params[1]}")
486					;;
487				242) # RPL_STATSUPTIME
488					s=("Uptime: $msg")
489					;;
490				251) # RPL_LUSERCLIENT
491					s=("Users: $msg")
492					;;
493				25[2-4]) # RPL_LUSEROP, RPL_USERUNKNOWN,
494					 # RPL_LUSERCHANNELS
495					s=("${params[1]} $msg")
496					;;
497				255) # RPL_LUSERME
498					s=("Server: $msg")
499					;;
500				256) # RPL_ADMINME
501					s=("Admin info for ${params[1]}:")
502					;;
503				257) # RPL_ADMINLOC1
504					s=("Location: $msg")
505					;;
506				258) # RPL_ADMINLOC2
507					s=("Administrated by: $msg")
508					;;
509				259) # RPL_ADMINEMAIL
510					s=("Email: $msg")
511					;;
512				263) # RPL_TRYAGAIN
513					s=("${params[1]}: $msg")
514					;;
515				26[5-6]) # RPL_LOCALUSERS, RPL_GLOBALUSERS
516					s=("$msg")
517					;;
518				276) # RPL_WHOISCERTFP
519					s=("${params[1]} $msg")
520					;;
521				301) # RPL_AWAY
522					s=("${params[1]} is away ($msg)")
523					;;
524				302) # RPL_USERHOST
525					# shellcheck disable=2086 # Intentional.
526					set -A reply -- $msg
527					for i in "${!reply[@]}"; do
528						t="${reply[i]}"
529						n="${t%%=*}"
530						h="${t##*=}"
531						isop=0 isaway=0
532						[[ "$n" == *'*' ]] && \
533							isop=1
534						n="${n%\*}"
535						[[ "$h" == '-'* ]] && \
536							isaway=1
537						h="${h#?}"
538
539						s[i]="$n"
540						((isop==1)) && s[i]+=" (is op)"
541						((isaway==1)) && \
542							s[i]+=" (is away)"
543						s[i]+=" - $h"
544					done
545					;;
546				305) # RPL_UNAWAY
547					s=("You are no longer marked as away")
548					;;
549				306) # RPL_NOWAWAY
550					s=("You are now marked as away")
551					;;
552				307) # RPL_WHOISREGNICK
553					s=("${params[1]} $msg")
554					;;
555				31[14]) # RPL_WHOISUSER, RPL_WHOWASUSER
556					# s=("Info for ${params[1]}:"
557					# "User: ${params[2]}"
558					# "Host: ${params[3]}"
559					# "Real name: $msg")
560					s=("${params[1]} is logged in as \
561${params[1]}, their realname is ${params[5]}" "${params[1]} is connecting from \
562${params[3]}")
563					;;
564				312) # RPL_WHOISSERVER
565					s=("${params[1]} is connected to \
566${params[2]} ($msg)")
567					;;
568				313) # RPL_WHOISOPERATOR
569					s=("${params[1]} $msg")
570					;;
571				315) # RPL_ENDOFWHO
572					s=("End of WHO list.")
573					;;
574				317) # RPL_WHOISIDLE
575					s=("${params[1]} has been idle for \
576${params[2]} seconds, they signed on at $(date -ud @"${params[3]}")")
577					;;
578				318) # RPL_ENDOFWHOIS
579					s=("End of WHOIS list.")
580					;;
581				319) # RPL_WHOISCHANNELS
582					s=("${params[1]} is joined to: ")
583					# shellcheck disable=2086 # Intentional.
584					set -A reply -- $msg
585					for i in "${!reply[@]}"; do
586						prefix='' c="${reply[i]}"
587						[[ "$c" == [\~\&@%+]* ]] && {
588							prefix="${c%%"${c#?}"}"
589							c="${c#?}"
590						}
591						s[0]+="$c \
592${prefix:+"($prefix) "}"
593					done
594					;;
595				320) # RPL_WHOISSPECIAL
596					s=("${params[1]} $msg")
597					;;
598				322) # RPL_LIST
599					topic="$trail"
600					s=("${params[1]} (${params[2]} users)\
601${topic:+" - $topic"}")
602					;;
603				323) # RPL_LISTEND
604					s=("End of LIST.")
605					;;
606				324) # RPL_CHANNELMODEIS
607					s=("Mode for ${params[1]}: \
608${params[2]}")
609					# TODO: Handle params[2..n].
610					;;
611				329) # RPL_CREATIONTIME
612					s=("${params[1]} was created at \
613$(date -d @"${params[2]}")")
614					;;
615				330) # RPL_WHOISACCOUNT
616					s=("${params[1]} $msg ${params[2]}")
617					;;
618				331) # RPL_NOTOPIC
619					target="${params[1]}"
620					s=("No topic set in ${params[1]}")
621					;;
622				332) # RPL_TOPIC
623					target="${params[1]}"
624					s=("Topic in ${params[1]}: $msg")
625					;;
626				333) # RPL_TOPICWHOTIME
627					target="${params[1]}"
628					s=("Topic for ${params[1]} was set by \
629${params[2]} at $(date -d @"${params[3]}")")
630					;;
631				336) # RPL_INVITELIST
632					s=("Invited to: ${params[1]}")
633					;;
634				337) # RPL_ENDOFINVITELIST
635					s=("End of INVITE list.")
636					;;
637				338) # RPL_WHOISACTUALLY
638					[ -n "${params[3]}" ] && \
639						s=("${params[1]} is actually \
640using host ${params[2]} (IP ${params[3]})")
641					;;
642				341) # RPL_INVITING
643					s=("Invited ${params[1]} to \
644${params[2]}")
645					;;
646				346) # RPL_INVEXLIST
647					s=("Invite-excepted from ${params[1]}: \
648${params[2]}")
649					;;
650				347) # RPL_ENDOFINVEXLIST
651					s=("End of invite-except list.")
652					;;
653				348) # RPL_EXCEPTLIST
654					s=("Excepted from ${params[1]}: \
655${params[2]}")
656					;;
657				349) # RPL_ENDOFEXCEPTLIST
658					s=("End of channel exception list.")
659					;;
660				351) # RPL_VERSION
661					s=("${params[2]} version ${params[1]} (\
662$msg)")
663					;;
664				352) # RPL_WHOREPLY
665					s=("${params[5]} is logged in as \
666${params[2]}, their realname is ${msg#* }" "${params[5]}'s host is ${params[3]}"
667"${params[5]} is authenticated to ${params[1]}, their flags are ${params[6]}")
668					;;
669				353) # RPL_NAMREPLY
670					s=("Names in ${params[2]}: $msg")
671					;;
672				364) # RPL_LINKS
673					s=("${params[1]} <-> ${params[2]} \
674(${msg% *} hops): ${msg#* }")
675					;;
676				365) # RPL_ENDOFLINKS
677					s=("End of LINKS list.")
678					;;
679				366) # RPL_ENDOFNAMES
680					s=("End of NAMES list.")
681					;;
682				367) # RPL_BANLIST
683					s=("Banned from ${params[1]}: \
684${params[2]}${params[3]:+" (banned by ${params[3]})"}${params[4]:+" (banned \
685at $(date -d @"${params[4]}")"}")
686					;;
687				368) # RPL_ENDOFBANLIST
688					s=("End of channel ban list.")
689					;;
690				369) # RPL_ENDOFWHOWAS
691					s=("End of WHOWAS.")
692					;;
693				371) # RPL_INFO
694					s=("Info: $msg")
695					;;
696				372) # RPL_MOTD
697					s=("$msg")
698					;;
699				374) # RPL_ENDOFINFO
700					s=("End of INFO list.")
701					;;
702				375) # RPL_MOTDSTART
703					s=("$msg")
704					;;
705				376) # RPL_ENDOFMOTD
706					s=("End of MOTD.")
707					;;
708				37[8-9]) # RPL_WHOISHOST, RPL_WHOISMODES
709					s=("${params[1]} $msg")
710					;;
711				381) # RPL_YOUREOPER
712					s=("$msg")
713					;;
714				382) # RPL_REHASHING
715					s=("$msg ${params[1]}")
716					;;
717				391) # RPL_TIME
718					s=("Time for ${params[1]}: $msg")
719					;;
720				400) # ERR_UNKNOWNERROR
721					s=("\"${params[*]}\": $msg")
722					;;
723				401) # ERR_NOSUCHNICK
724					s=("No such nick \"${params[1]}\"")
725					;;
726				402) # ERR_NOSUCHSERVER
727					s=("No such server \"${params[1]}\"")
728					;;
729				403) # ERR_NOSUCHCHANNEL
730					s=("No such channel \"${params[1]}\"")
731					;;
732				404) # ERR_CANNOTSENDTOCHAN
733					s=("Cannot send to channel \
734\"${params[1]}\"")
735					;;
736				405) # ERR_TOOMANYCHANNELS
737					s=("You have joined too many channels")
738					;;
739				406) # ERR_WASNOSUCHNICK
740					s=("\"${params[1]}\": There was no such\
741 nick")
742					;;
743				409) # ERR_NOORIGIN
744					s=("No origin/token for PING specified")
745					;;
746				41[12]|417) # ERR_NORECIPIENT, ERR_NOTEXTTOSEND,
747					    # ERR_INPUTTOOLONG
748					s=("$msg")
749					;;
750				421) # ERR_UNKNOWNCOMMAND
751					s=("\"${params[1]}\": $msg")
752					;;
753				422) # ERR_NOMOTD
754					s=("No MOTD available")
755					;;
756				431) # ERR_NONICKNAMEGIVEN
757					s=("No nickname given")
758					;;
759				432) # ERR_ERRONEUSNICKNAME (typo in spec?)
760					s=("Erroneous nickname: \
761\"${params[1]}\"")
762					;;
763				433) # ERR_NICKNAMEINUSE
764					s=("Nickname already in use: \
765\"${params[1]}\"")
766					;;
767				436) # ERR_NICKCOLLISION
768					s=("\"${params[1]}\": $msg")
769					;;
770				441) # ERR_USERNOTINCHANNEL
771					s=("${params[1]} is not on that channel\
772 (${params[2]})")
773					;;
774				442) # ERR_NOTONCHANNEL
775					s=("You're not on that channel \
776(${params[1]})")
777					;;
778				443) # ERR_USERONCHANNEL
779					s=("${params[1]} is already on channel \
780${params[3]}")
781					;;
782				451) # ERR_NOTREGISTERED
783					s=("You have not registered")
784					;;
785				461) # ERR_NEEDMOREPARAMS
786					s=("\"${params[1]}\": Not enough \
787parameters")
788					;;
789				462) # ERR_ALREADYREGISTERED
790					s=("You may not reregister")
791					;;
792				464) # ERR_PASSWDMISMATCH
793					s=("Password does not match")
794					;;
795				465) # ERR_YOUREBANNEDCREEP (lmfao)
796					s=("$msg")
797					;;
798				47[1345]) # ERR_CHANNELISFULL,
799					  # ERR_INVITEONLYCHAN,
800					  # ERR_BANNEDFROMCHAN,
801					  # ERR_BADCHANNELKEY
802					s=("${params[1]}: $msg")
803					;;
804				472) # ERR_UNKNOWNMODE
805					s=("'${params[1]}' $msg")
806					;;
807				476) # ERR_BADCHANMASK
808					s=("\"${params[1]}\": Bad channel name")
809					;;
810				481) # ERR_NOPRIVILEGES
811					s=("You're not an IRC operator")
812					;;
813				482) # ERR_CHANOPRIVSNEEDED
814					s=("${params[1]}: You're not a channel \
815operator")
816					;;
817				483) # ERR_CANTKILLSERVER
818					s=("You can't kill a server")
819					;;
820				491) # ERR_NOOPERHOST
821					s=("No O-lines for your host")
822					;;
823				501) # ERR_UMODEUNKNOWNFLAG
824					s=("Unknown mode flag")
825					;;
826				502) # ERR_USERSDONTMATCH
827					s=("$msg")
828					;;
829				524) # ERR_HELPNOTFOUND
830					s=("\"${params[1]}\": No help available\
831 on this topic")
832					;;
833				525) # ERR_INVALIDKEY
834					s=("${params[1]}: \
835Key is not well-formed")
836					;;
837				671) # RPL_WHOISSECURE
838					s=("${params[1]} is using a secure \
839connection")
840					;;
841				696) # ERR_INVALIDMODEPARAM
842					s=("Failed to set ${params[2]} on \
843${params[1]}: $msg")
844					;;
845				70[4-6]) # RPL_HELPSTART, RPL_HELPTXT,
846					 # RPL_ENDOFHELP
847					s=("$msg")
848					;;
849				723) # ERR_NOPRIVS
850					s=("${params[1]}: $msg")
851					;;
852				90[01]) # RPL_LOGGED{IN, OUT}
853					s=("$msg")
854					;;
855				902) # ERR_NICKLOCKED
856					s=("This nick does not belong to you")
857					;;
858				903) # RPL_SASLSUCCESS
859					s=("SASL authentication successful")
860					;;
861				904) # ERR_SASLFAIL
862					s=("SASL authentication failed")
863					;;
864				905) # ERR_SASLTOOLONG
865					s=("SASL message too long")
866					;;
867				906) # ERR_SASLABORTED
868					s=("SASL authentication aborted")
869					;;
870				907) # ERR_SASLALREADY
871					s=("You have already authenticated \
872using SASL")
873					;;
874				908) # RPL_SASLMECHS
875					s=("Available SASL mechanisms: \
876${params[1]}")
877					;;
878				*)
879					[[ "${params[0]:-}" == '#'* ]] && \
880						target="${params[0]}"
881					;;
882			esac
883
884			# Omit numerics that aren't printed.
885			((${#s[@]} == 0)) && continue 
886
887			if [ -n "$target" ]; then
888				for i in "${!bufs[@]}"; do
889					[ "${bufs[i]}" = "$target" ] && {
890						for l in "${s[@]}"; do
891							status "$i" "$l"
892						done
893						break
894					}
895				done
896			else
897				for l in "${s[@]}"; do
898					status "$curbuf" "$l"
899				done
900			fi
901			;;
902		ERROR)
903			status "$curbuf" "Fatal error: $trail"
904			;;
905		*)
906			# What the hell???
907			;;
908	esac
909else
910	if (($? > 128)); then
911		: # Timed out
912	else
913		# TODO: It would make sense here to try and reconnect.
914		break
915	fi
916fi
917
918# Warning: this delay heavily relies on the latency of the terminal being
919# somewhat low, so input may 'fall through' on slow terminals.
920k=()
921if IFS= read -rN1 -t0.004 "k[0]"; then
922	# shellcheck disable=2034 # Weird.
923	ki=1
924	[ "${k[0]}" = $'\e' ] && \
925		while IFS= read -rN1 -t0.001 "k[ki++]"; do :; done
926	OLDIFS=$IFS
927	IFS= t="${k[*]}"
928	IFS=$OLDIFS
929
930	((cursor > ${#input})) && ((cursor = ${#input}))
931	((cursor < 0)) && ((cursor = 0))
932
933	case "$t" in
934		$'\n')
935			input "$input" || break
936			input="" cursor=0
937			;;
938		$'\b'|$'\x7f')
939			if ((cursor == ${#input})); then
940				input="${input%?}"
941				((cursor--))
942			elif ((${#input} == 0)); then
943				:
944			else
945				input="${input:0:cursor-1}${input:cursor}"
946				((cursor--))
947			fi
948			;;
949		$'\e[A')
950			c=0 lines="${bhist[curbuf]}"
951			while IFS= read -r line; do ((c++)); done <<< "$lines"
952			max=$((c - (LINES - 1)))
953			((max < 0)) && max=0
954
955			((scroll < max)) && {
956				((scroll++))
957				printf '\e[H\eM'
958				l=$((c - LINES + 2 - scroll)) i=0
959				while IFS= read -r line; do
960					((i++))
961					((i == l)) && {
962						printf '%b' "$line"
963						break
964					}
965				done <<< "$lines"
966			}
967			;;
968		$'\e[B')
969			c=0 lines="${bhist[curbuf]}"
970			while IFS= read -r line; do ((c++)); done <<< "$lines"
971
972			((scroll > 0)) && {
973				((scroll--))
974				printf '\e[%d;1H\eD' $((LINES - 1))
975				l=$((c - scroll)) i=0
976				((l < 1)) && l=1
977				while IFS= read -r line; do
978					((i++))
979					((i == l)) && {
980						printf '%b' "$line"
981						break
982					}
983				done <<< "$lines"
984			}
985			;;
986		[[:print:]])
987			input+="$t"
988			((cursor++))
989			;;
990	esac
991
992	prompt
993fi; done
994
995die