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