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