git.strcat.st

/strcat/irc.git/ - summarytreelogarchive

subject
inital commit
commit
f45ab39571d215fbf1142369af719ef1d5a770db
date
2026-04-18T23:33:18Z
message
diff
 install.sh |   8 +
 irc        | 324 +++++++++++++++++++++++++++++++++++
 ircf       | 556 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 888 insertions(+)

diff --git a/install.sh b/install.sh
new file mode 100644
index 0000000..f8ad7d4
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+set -eu
+dest=${1:-"$HOME/.local/bin"}
+mkdir -p "$dest"
+cp "./irc" "$dest/irc"
+cp "./ircf" "$dest/ircf"
+printf "installed to %s\n" "$dest"
+printf "add to PATH if needed: export PATH=\"%s:\$PATH\"\n" "$dest"
diff --git a/irc b/irc
new file mode 100755
index 0000000..ba2ba7d
--- /dev/null
+++ b/irc
@@ -0,0 +1,324 @@
+#!/bin/sh
+# Usage: irc [<server>] [<port>] [<nick>] [<username>] [<realname>]
+
+[ -z "$IRC" ] && {
+	fd="/tmp/.irc2"; rm -f "$fd"; mkfifo "$fd"
+	nc "${1:-irc.libera.chat}" "${2:-6667}" <"$fd" | \
+		IRC=1 OUTFD=3 exec "$0" "$@" 3>&1 >"$fd"
+}
+
+stty_save=$(stty -g </dev/tty)
+stty -echo -icanon min 1 time 0 onlcr </dev/tty
+trap 'stty "$stty_save" </dev/tty; kill 0' EXIT HUP INT
+if [ -n "$OUTFD" ]; then
+	out="/dev/fd/$OUTFD"
+else
+	out="/dev/tty"
+fi
+printf 'NICK %s\r\n' "${nick:=${3:-$(id -un)}}"
+printf 'USER %s localhost * :%s\r\n' "${4:-$nick}" "${5:-$nick}"
+
+curr=""
+[ -n "${IRC_CMD_PREFIXES:-}" ] || IRC_CMD_PREFIXES="/ ; : &"
+
+is_cmd_prefix() {
+	cmd=$1
+	[ -z "$cmd" ] && return 1
+	first=${cmd%"${cmd#?}"}
+	case $IRC_CMD_PREFIXES in
+		*' '*) 
+			for p in $IRC_CMD_PREFIXES; do
+				[ "$first" = "$p" ] && return 0
+			done
+			return 1
+		;;
+	esac
+	case $IRC_CMD_PREFIXES in
+		*"$first"*) return 0 ;;
+	esac
+	return 1
+}
+
+handle_cmd() {
+	cmd=$1
+	[ -z "$cmd" ] && return
+	if is_cmd_prefix "$cmd"; then
+		[ "${cmd#?}" = "" ] && return
+		cmd="/${cmd#?}"
+	fi
+	case $cmd in
+		/join*)
+			curr=${cmd#* }
+			printf '%s\r\n' "${cmd#/}"
+		;;
+		/part*)
+			channel=${cmd#* }
+			printf 'PART %s\r\n' "$channel"
+		;;
+		/quit*)
+			reason=${cmd#* }
+			[ -z "$reason" ] && reason="Leaving"
+			printf 'QUIT :%s\r\n' "$reason"
+			exit 0
+		;;
+		/nick*)
+			newnick=${cmd#* }
+			printf 'NICK %s\r\n' "$newnick"
+		;;
+		/whois*)
+			target=${cmd#* }
+			printf 'WHOIS %s\r\n' "$target"
+		;;
+		/list*)
+			channel=${cmd#* }
+			if [ -z "$channel" ]; then
+				printf 'LIST\r\n'
+			else
+				printf 'LIST %s\r\n' "$channel"
+			fi
+		;;
+		/topic*)
+			channel=${cmd#* }
+			printf 'TOPIC %s\r\n' "$channel"
+		;;
+		/msg*)
+			target_msg=${cmd#* }
+			target=${target_msg%% *}
+			curr=$target
+			if [ "$target_msg" != "$target" ]; then
+				msg=${target_msg#* }
+				printf 'PRIVMSG %s :%s\r\n' "$target" "$msg"
+				printf ':%s!%s@localhost PRIVMSG %s :%s\n' "$nick" "${4:-$nick}" "$target" "$msg" >"$out"
+			fi
+		;;
+		/invite*)
+			user_channel=${cmd#* }
+			user=${user_channel%% *}
+			channel=${user_channel#* }
+			printf 'INVITE %s %s\r\n' "$user" "$channel"
+		;;
+		/kick*)
+			channel_user=${cmd#* }
+			channel=${channel_user%% *}
+			user=${channel_user#* }
+			printf 'KICK %s %s\r\n' "$channel" "$user"
+		;;
+		/mode*)
+			channel_mode=${cmd#* }
+			channel=${channel_mode%% *}
+			mode=${channel_mode#* }
+			printf 'MODE %s %s\r\n' "$channel" "$mode"
+		;;
+		/away*)
+			message=${cmd#* }
+			if [ -z "$message" ]; then
+				printf 'AWAY\r\n'
+			else
+				printf 'AWAY :%s\r\n' "$message"
+			fi
+		;;
+		/me*)
+			action=${cmd#* }
+			printf 'PRIVMSG %s :\001ACTION %s\001\r\n' "$curr" "$action"
+			printf ':%s!%s@localhost PRIVMSG %s :\001ACTION %s\001\n' "$nick" "${4:-$nick}" "$curr" "$action" >"$out"
+		;;
+		/*)
+			printf '%s\r\n' "${cmd#/}"
+		;;
+		*)
+			printf 'PRIVMSG %s :%s\r\n' "$curr" "$cmd"
+			printf ':%s!%s@localhost PRIVMSG %s :%s\n' "$nick" "${4:-$nick}" "$curr" "$cmd" >"$out"
+		;;
+	esac
+}
+
+buf=""
+cur=0
+
+buf_len() {
+	printf '%s' "$buf" | awk '{ print length($0) }'
+}
+
+norm_int() {
+	case ${1:-0} in
+		''|*[!0-9]*)
+			printf '0'
+		;;
+		*)
+			printf '%s' "$1"
+		;;
+	esac
+}
+
+calc_add() {
+	awk -v a="$1" -v b="$2" 'BEGIN{
+		if (a !~ /^[0-9]+$/) a = 0
+		if (b !~ /^[0-9]+$/) b = 0
+		print a + b
+	}'
+}
+
+calc_sub_nonneg() {
+	awk -v a="$1" -v b="$2" 'BEGIN{
+		if (a !~ /^[0-9]+$/) a = 0
+		if (b !~ /^[0-9]+$/) b = 0
+		d = a - b
+		if (d < 0) d = 0
+		print d
+	}'
+}
+
+buf_left() {
+	n=$(norm_int "${1:-0}")
+	[ "$n" -le 0 ] && { printf ''; return; }
+	printf '%s' "$buf" | awk -v n="$n" '{ print substr($0, 1, n) }'
+}
+
+buf_right() {
+	n=$(norm_int "${1:-0}")
+	[ "$n" -le 0 ] && { printf '%s' "$buf"; return; }
+	printf '%s' "$buf" | awk -v n="$n" '{ print substr($0, n + 1) }'
+}
+
+buf_insert() {
+	ch=$1
+	cur=$(norm_int "${cur:-0}")
+	left=$(buf_left "$cur")
+	right=$(buf_right "$cur")
+	buf=$left$ch$right
+	cur=$(calc_add "$cur" 1)
+}
+
+buf_backspace() {
+	cur=$(norm_int "${cur:-0}")
+	[ "$cur" -le 0 ] && return
+	left=$(buf_left "$(calc_sub_nonneg "$cur" 1)")
+	right=$(buf_right "$cur")
+	buf=$left$right
+	cur=$(calc_sub_nonneg "$cur" 1)
+}
+
+buf_delete() {
+	cur=$(norm_int "${cur:-0}")
+	len=$(buf_len)
+	len=$(norm_int "$len")
+	[ "$cur" -ge "$len" ] && return
+	left=$(buf_left "$cur")
+	right=$(buf_right "$(calc_add "$cur" 1)")
+	buf=$left$right
+}
+
+buf_move_left() {
+	cur=$(norm_int "${cur:-0}")
+	[ "$cur" -gt 0 ] && cur=$(calc_sub_nonneg "$cur" 1)
+}
+
+buf_move_right() {
+	cur=$(norm_int "${cur:-0}")
+	len=$(buf_len)
+	len=$(norm_int "$len")
+	[ "$cur" -lt "$len" ] && cur=$(calc_add "$cur" 1)
+}
+
+buf_move_home() {
+	cur=0
+}
+
+buf_move_end() {
+	cur=$(norm_int "$(buf_len)")
+}
+
+render_line() {
+	cur=$(norm_int "${cur:-0}")
+	len=$(norm_int "$(buf_len)")
+	[ "$cur" -lt 0 ] && cur=0
+	[ "$cur" -gt "$len" ] && cur=$len
+	printf '\r\033[2K' >/dev/tty
+	printf '%s' "$buf" >/dev/tty
+	if [ "$cur" -lt "$len" ]; then
+		back=$(calc_sub_nonneg "$len" "$cur")
+		printf '\033[%sD' "$back" >/dev/tty
+	fi
+}
+
+read_char() {
+	dd bs=1 count=1 2>/dev/null </dev/tty
+}
+while :; do
+	ch=$(dd bs=1 count=1 2>/dev/null </dev/tty)
+	case $ch in
+		"$(printf '\r')"|"$(printf '\n')")
+			printf '\r\033[2K' >/dev/tty
+			handle_cmd "$buf"
+			buf=""
+			cur=0
+		;;
+		"$(printf '\177')"|"$(printf '\010')")
+			if [ "$cur" -gt 0 ]; then
+				buf_backspace
+				render_line
+			fi
+		;;
+		"$(printf '\003')")
+			printf '^C\r\n' >/dev/tty
+			exit 130
+		;;
+		"$(printf '\001')") # Ctrl-A
+			buf_move_home
+			render_line
+		;;
+		"$(printf '\005')") # Ctrl-E
+			buf_move_end
+			render_line
+		;;
+		"$(printf '\033')") # escape sequence
+			ch2=$(read_char)
+			if [ "$ch2" = "[" ]; then
+				ch3=$(read_char)
+				case $ch3 in
+					"D") # left
+						buf_move_left
+						render_line
+					;;
+					"C") # right
+						buf_move_right
+						render_line
+					;;
+					"H") # home
+						buf_move_home
+						render_line
+					;;
+					"F") # end
+						buf_move_end
+						render_line
+					;;
+					"3") # delete
+						ch4=$(read_char)
+						if [ "$ch4" = "~" ]; then
+							buf_delete
+							render_line
+						fi
+					;;
+				esac
+			fi
+		;;
+		*)
+			buf_insert "$ch"
+			render_line
+		;;
+	esac
+done 2>/dev/null &
+
+while IFS= read -r line; do
+	case $line in
+		'PING '*)
+			printf 'PONG %s\r\n' "${line#* }"
+		;;
+		'*'/*)
+			printf '%s\n' "$line" >"$out"
+		;;
+		*)
+			printf '%s\n' "$line" >"$out"
+		;;
+	esac
+done
diff --git a/ircf b/ircf
new file mode 100755
index 0000000..d292b25
--- /dev/null
+++ b/ircf
@@ -0,0 +1,556 @@
+#!/bin/sh
+
+: "${IRC_TZ:=UTC}"
+: "${IRC_HL_COLOR:=blue}"
+: "${IRC_CHAN_WIDTH:=12}"
+: "${IRC_NICK_WIDTH:=9}"
+: "${IRC_MSG_MAX:=0}"
+: "${IRC_TIME_FMT:=%H:%M}"
+: "${IRC_SHOW_TS:=1}"
+: "${IRC_PM_LABEL:=pm}"
+: "${IRC_HL_BOLD:=0}"
+: "${IRC_COLOR:=1}"
+: "${IRC_MAX_NICK:=0}"
+: "${IRC_MAX_CHAN:=0}"
+: "${IRC_TAB_WIDTH:=0}"
+: "${IRC_TRUNC_SUFFIX:=...}"
+: "${IRC_ACTION_COLOR:=}"
+: "${IRC_NOTICE_COLOR:=}"
+TZ="$IRC_TZ"
+export TZ
+
+awk -v hl_color="$IRC_HL_COLOR" -v chan_width="$IRC_CHAN_WIDTH" -v nick_width="$IRC_NICK_WIDTH" -v msg_max="$IRC_MSG_MAX" \
+	-v time_fmt="$IRC_TIME_FMT" -v show_ts="$IRC_SHOW_TS" -v pm_label="$IRC_PM_LABEL" -v hl_bold="$IRC_HL_BOLD" \
+	-v color_on="$IRC_COLOR" -v max_nick="$IRC_MAX_NICK" -v max_chan="$IRC_MAX_CHAN" -v tab_width="$IRC_TAB_WIDTH" \
+	-v trunc_suffix="$IRC_TRUNC_SUFFIX" -v action_color="$IRC_ACTION_COLOR" -v notice_color="$IRC_NOTICE_COLOR" '
+function ltrim(s) { sub(/^[ \t\r\n]+/, "", s); return s }
+function rtrim(s) { sub(/[ \t\r\n]+$/, "", s); return s }
+function trim(s) { return rtrim(ltrim(s)) }
+function nick_from_prefix(p,  n) {
+	n = p
+	sub(/^:/, "", n)
+	sub(/!.*/, "", n)
+	return n
+}
+function raw_trailing(raw,   tmp, i) {
+	tmp = raw
+	i = match(tmp, / :/)
+	if (i > 0) return substr(tmp, i + 2)
+	if (tmp ~ /^:/ && tmp ~ /^:[^ ]+ [^ ]+ :/) {
+		sub(/^:[^ ]+ [^ ]+ :/, "", tmp)
+		return tmp
+	}
+	return ""
+}
+function ts() { return strftime(time_fmt) }
+function is_channel(t) { return (t ~ /^#|^\+|^&|^!/) }
+function pad_right(s, width,   pad, tabs, spaces) {
+	if (tab_width > 0) {
+		pad = width - length(s)
+		if (pad <= 0) return s
+		tabs = int(pad / tab_width)
+		spaces = pad - (tabs * tab_width)
+		return s sprintf("%" tabs "s", "") sprintf("%" spaces "s", "")
+	}
+	return sprintf("%-" width "." width "s", s)
+}
+function pad_left(s, width,   pad) { return sprintf("%" width "." width "s", s) }
+function pad16(s,   out) { return pad_right(s, chan_width) }
+function padnick(s,   out) { return pad_left(s, nick_width) }
+function padnick_colored(raw, colored,   w, pad) {
+	raw = substr(raw, 1, nick_width)
+	w = length(raw)
+	pad = sprintf("%" (nick_width - w) "s", "")
+	return pad colored
+}
+function quote_reason(s) {
+	if (s == "") return ""
+	gsub(/"/, "\\\"", s)
+	return "\"" s "\""
+}
+function fmt_action(msg, chan,   left) {
+	left = pad16(trim_chan(chan))
+	if (show_ts) return sprintf("[%s] %s %s", ts(), left, msg)
+	return sprintf("%s %s", left, msg)
+}
+function fmt_action_line(chan, nick, text,   left, who) {
+	left = pad16(trim_chan(chan))
+	who = padnick(trim_nick(nick))
+	if (show_ts) return sprintf("[%s] %s %s : %s", ts(), left, who, text)
+	return sprintf("%s %s : %s", left, who, text)
+}
+function color_code(name) {
+	if (!color_on) return ""
+	if (name == "black") return "\033[30m"
+	if (name == "red") return "\033[31m"
+	if (name == "green") return "\033[32m"
+	if (name == "yellow") return "\033[33m"
+	if (name == "blue") return "\033[34m"
+	if (name == "magenta") return "\033[35m"
+	if (name == "cyan") return "\033[36m"
+	if (name == "white") return "\033[37m"
+	if (name == "bright_black") return "\033[90m"
+	if (name == "bright_red") return "\033[91m"
+	if (name == "bright_green") return "\033[92m"
+	if (name == "bright_yellow") return "\033[93m"
+	if (name == "bright_blue") return "\033[94m"
+	if (name == "bright_magenta") return "\033[95m"
+	if (name == "bright_cyan") return "\033[96m"
+	if (name == "bright_white") return "\033[97m"
+	return "\033[34m"
+}
+function color_reset() { return (color_on ? "\033[0m" : "") }
+function bold_code() { return (color_on ? "\033[1m" : "") }
+function colorize(s, c, b,   out) {
+	if (!color_on || c == "") return s
+	out = color_code(c)
+	if (b) out = out bold_code()
+	return out s color_reset()
+}
+function should_highlight(msg,   n) {
+	if (mynick == "") return 0
+	n = tolower(mynick)
+	return (tolower(msg) ~ ("(^|[^[:alnum:]_])" n "([^[:alnum:]_]|$)"))
+}
+function label_for(code) {
+	if (code == "001") return "WELCOME"
+	if (code == "002") return "YOURHOST"
+	if (code == "003") return "CREATED"
+	if (code == "004") return "MYINFO"
+	if (code == "005") return "ISUPPORT"
+	if (code == "251") return "LUSER"
+	if (code == "252") return "LUSEROP"
+	if (code == "253") return "LUSERUNKNOWN"
+	if (code == "254") return "LUSERCHANNELS"
+	if (code == "255") return "LUSERME"
+	if (code == "256") return "ADMINME"
+	if (code == "257") return "ADMINLOC"
+	if (code == "258") return "ADMINLOC"
+	if (code == "259") return "ADMINEMAIL"
+	if (code == "263") return "TRYAGAIN"
+	if (code == "301") return "AWAY"
+	if (code == "302") return "USERHOST"
+	if (code == "303") return "ISON"
+	if (code == "305") return "UNAWAY"
+	if (code == "306") return "NOWAWAY"
+	if (code == "311") return "WHOIS"
+	if (code == "312") return "WHOIS"
+	if (code == "313") return "WHOIS"
+	if (code == "317") return "WHOIS"
+	if (code == "318") return "WHOIS"
+	if (code == "319") return "WHOIS"
+	if (code == "321") return "LIST"
+	if (code == "322") return "LIST"
+	if (code == "323") return "LIST"
+	if (code == "331") return "TOPIC"
+	if (code == "332") return "TOPIC"
+	if (code == "333") return "TOPIC"
+	if (code == "341") return "INVITE"
+	if (code == "351") return "VERSION"
+	if (code == "352") return "WHO"
+	if (code == "315") return "WHO"
+	if (code == "353") return "NAMES"
+	if (code == "366") return "NAMES"
+	if (code == "367") return "BANLIST"
+	if (code == "368") return "BANLIST"
+	if (code == "371") return "INFO"
+	if (code == "372") return "MOTD"
+	if (code == "374") return "ENDOFINFO"
+	if (code == "375") return "MOTD"
+	if (code == "376") return "MOTD"
+	if (code == "381") return "OPER"
+	if (code == "382") return "REHASH"
+	if (code == "391") return "TIME"
+	if (code == "401") return "NOSUCHNICK"
+	if (code == "402") return "NOSUCHSERVER"
+	if (code == "403") return "NOSUCHCHANNEL"
+	if (code == "404") return "CANNOTSEND"
+	if (code == "405") return "TOOMANYCHANNELS"
+	if (code == "406") return "WASNOSUCHNICK"
+	if (code == "407") return "TOOMANYTARGETS"
+	if (code == "409") return "NOORIGIN"
+	if (code == "411") return "NORECIPIENT"
+	if (code == "412") return "NOTEXTTOSEND"
+	if (code == "413") return "NOTOPLEVEL"
+	if (code == "414") return "WILDTOPLEVEL"
+	if (code == "421") return "UNKNOWNCOMMAND"
+	if (code == "422") return "NOMOTD"
+	if (code == "423") return "NOADMININFO"
+	if (code == "424") return "FILEERROR"
+	if (code == "431") return "NONICKNAMEGIVEN"
+	if (code == "432") return "ERRONEUSNICKNAME"
+	if (code == "433") return "NICKNAMEINUSE"
+	if (code == "436") return "NICKCOLLISION"
+	if (code == "437") return "UNAVAILRESOURCE"
+	if (code == "441") return "USERNOTINCHANNEL"
+	if (code == "442") return "NOTONCHANNEL"
+	if (code == "443") return "USERONCHANNEL"
+	if (code == "444") return "NOLOGIN"
+	if (code == "445") return "SUMMONDISABLED"
+	if (code == "446") return "USERSDISABLED"
+	if (code == "451") return "NOTREGISTERED"
+	if (code == "461") return "NEEDMOREPARAMS"
+	if (code == "462") return "ALREADYREGISTRED"
+	if (code == "463") return "NOPERMFORHOST"
+	if (code == "464") return "PASSWDMISMATCH"
+	if (code == "465") return "YOUREBANNED"
+	if (code == "467") return "KEYSET"
+	if (code == "471") return "CHANNELISFULL"
+	if (code == "472") return "UNKNOWNMODE"
+	if (code == "473") return "INVITEONLYCHAN"
+	if (code == "474") return "BANNEDFROMCHAN"
+	if (code == "475") return "BADCHANNELKEY"
+	if (code == "476") return "BADCHANMASK"
+	if (code == "477") return "NOCHANMODES"
+	if (code == "478") return "BANLISTFULL"
+	if (code == "481") return "NOPRIVILEGES"
+	if (code == "482") return "CHANOPRIVSNEEDED"
+	if (code == "483") return "CANTKILLSERVER"
+	if (code == "484") return "RESTRICTED"
+	if (code == "485") return "UNIQOPPRIVSNEEDED"
+	if (code == "491") return "NOOPERHOST"
+	if (code == "501") return "UMODEUNKNOWNFLAG"
+	if (code == "502") return "USERSDONTMATCH"
+	return ""
+}
+function parse(line,   rest, i, part, n, tok) {
+	delete params
+	prefix = ""
+	cmd = ""
+	params_n = 0
+	trailing = ""
+	rest = line
+	if (rest ~ /^:/) {
+		i = index(rest, " ")
+		if (i > 0) {
+			prefix = substr(rest, 1, i - 1)
+			rest = substr(rest, i + 1)
+		} else {
+			prefix = rest
+			rest = ""
+		}
+	}
+	i = index(rest, " ")
+	if (i > 0) {
+		cmd = substr(rest, 1, i - 1)
+		rest = substr(rest, i + 1)
+	} else {
+		cmd = rest
+		rest = ""
+	}
+	rest = ltrim(rest)
+	if (rest ~ / :/) {
+		i = match(rest, / :/)
+		part = substr(rest, 1, i - 1)
+		trailing = substr(rest, i + 2)
+	} else if (rest ~ /^:/) {
+		trailing = substr(rest, 2)
+		part = ""
+	} else {
+		part = rest
+	}
+	part = trim(part)
+	if (part != "") {
+		n = split(part, tok, /[ ]+/)
+		for (i = 1; i <= n; i++) params[i] = tok[i]
+		params_n = n
+	} else {
+		params_n = 0
+	}
+}
+function fmt_numeric(code,   s, target) {
+	target = (params_n >= 1 ? params[1] : "")
+	s = trailing
+	if (code == "005") {
+		s = ""
+		if (params_n > 1) {
+			for (i = 2; i <= params_n; i++) s = s (i > 2 ? " " : "") params[i]
+		}
+		if (trailing != "") s = s (s != "" ? " " : "") trailing
+	}
+	if (s == "") {
+		s = ""
+		if (params_n > 1) {
+			for (i = 2; i <= params_n; i++) s = s (i > 2 ? " " : "") params[i]
+		}
+	}
+	s = trim(s)
+	if (s == "") {
+		s = trim(raw_trailing(line))
+	}
+	if (s == "") return ""
+
+	if (code == "353") {
+		chan = (params_n >= 3 ? params[3] : "")
+		return sprintf("NAMES %s: %s", chan, s)
+	}
+	if (code == "366") {
+		chan = (params_n >= 2 ? params[2] : "")
+		return sprintf("NAMES: end %s", chan)
+	}
+	if (code == "332") {
+		chan = (params_n >= 2 ? params[2] : "")
+		return sprintf("TOPIC %s: %s", chan, s)
+	}
+	if (code == "331") {
+		chan = (params_n >= 2 ? params[2] : "")
+		return sprintf("TOPIC %s: %s", chan, s)
+	}
+	if (code == "333") {
+		chan = (params_n >= 2 ? params[2] : "")
+		return sprintf("TOPIC %s: %s", chan, s)
+	}
+	if (code == "322") {
+		chan = (params_n >= 2 ? params[2] : "")
+		users = (params_n >= 3 ? params[3] : "")
+		return sprintf("LIST %s (%s): %s", chan, users, s)
+	}
+	if (code == "321" || code == "323") {
+		return sprintf("LIST: %s", s)
+	}
+	if (code == "311") {
+		nick = (params_n >= 2 ? params[2] : "")
+		user = (params_n >= 3 ? params[3] : "")
+		host = (params_n >= 4 ? params[4] : "")
+		return sprintf("WHOIS %s: %s@%s %s", nick, user, host, s)
+	}
+	if (code == "312") {
+		nick = (params_n >= 2 ? params[2] : "")
+		server = (params_n >= 3 ? params[3] : "")
+		return sprintf("WHOIS %s: %s %s", nick, server, s)
+	}
+	if (code == "313") {
+		nick = (params_n >= 2 ? params[2] : "")
+		return sprintf("WHOIS %s: %s", nick, s)
+	}
+	if (code == "317") {
+		nick = (params_n >= 2 ? params[2] : "")
+		idle = (params_n >= 3 ? params[3] : "")
+		return sprintf("WHOIS %s: idle %ss %s", nick, idle, s)
+	}
+	if (code == "318") {
+		nick = (params_n >= 2 ? params[2] : "")
+		return sprintf("WHOIS %s: %s", nick, s)
+	}
+	if (code == "319") {
+		nick = (params_n >= 2 ? params[2] : "")
+		return sprintf("WHOIS %s: %s", nick, s)
+	}
+	if (code == "352") {
+		chan = (params_n >= 2 ? params[2] : "")
+		user = (params_n >= 3 ? params[3] : "")
+		host = (params_n >= 4 ? params[4] : "")
+		nick = (params_n >= 6 ? params[6] : "")
+		return sprintf("WHO %s: %s %s@%s %s", chan, nick, user, host, s)
+	}
+	if (code == "315") {
+		return sprintf("WHO: %s", s)
+	}
+	if (code == "341") {
+		nick = (params_n >= 2 ? params[2] : "")
+		chan = (params_n >= 3 ? params[3] : "")
+		return sprintf("INVITE: %s %s", nick, chan)
+	}
+	if (code == "372" || code == "375" || code == "376") {
+		return sprintf("MOTD: %s", s)
+	}
+
+	label = label_for(code)
+	if (code ~ /^[4-5][0-9][0-9]$/) {
+		return sprintf("ERROR: %s", s)
+	}
+	if (label != "") {
+		return sprintf("%s: %s", label, s)
+	}
+	return sprintf("INFO: %s", s)
+}
+function sanitize_line(s) {
+	if (s ~ /sh: out of range/) return ""
+	if (s ~ /^\[/) return s
+	if (s ~ /^\033\[/) return s
+	if (s ~ /^\)[^ ]+ quit /) {
+		sub(/^\)/, "", s)
+		return s
+	}
+	if (s ~ /^\)[^ ]+ (left|joined) /) sub(/^\)/, "", s)
+	if (s ~ /^\)[^ ]+ (left|joined) /) {
+		tmp = s
+		sub(/^\)/, "", tmp)
+		i = index(tmp, " ")
+		if (i > 0) {
+			nick = substr(tmp, 1, i - 1)
+			tmp = substr(tmp, i + 1)
+			i = index(tmp, " ")
+			if (i > 0) {
+				action = substr(tmp, 1, i - 1)
+				tmp = substr(tmp, i + 1)
+				i = index(tmp, " ")
+				if (i > 0) {
+					chan = substr(tmp, 1, i - 1)
+					reason = substr(tmp, i + 1)
+				} else {
+					chan = tmp
+					reason = ""
+				}
+				if (reason ~ /^\(.*\)$/) {
+					reason = substr(reason, 2, length(reason) - 2)
+				} else {
+					reason = ""
+				}
+				if (reason != "" && length(reason) > length(nick)) {
+					if (substr(reason, length(reason) - length(nick) + 1) == nick) {
+						nick = reason
+					}
+				}
+				s = nick " " action " " chan
+				if (reason != "") s = s " " reason
+			}
+		}
+	}
+	return s
+}
+function print_line(s) {
+	if (s == "") return
+	s = sanitize_line(s)
+	if (s != "") print s
+}
+function limit_with_suffix(s, max,   suffix, keep) {
+	if (max <= 0 || length(s) <= max) return s
+	suffix = (trunc_suffix != "" ? trunc_suffix : "")
+	keep = max - length(suffix)
+	if (keep <= 0) return substr(s, 1, max)
+	return substr(s, 1, keep) suffix
+}
+function trim_nick(s) { return limit_with_suffix(s, max_nick) }
+function trim_chan(s) { return limit_with_suffix(s, max_chan) }
+function limit_msg(s) {
+	return limit_with_suffix(s, msg_max)
+}
+function extract_trailing(raw, cmd,   tmp) {
+	tmp = raw
+	sub(/^:[^ ]+ /, "", tmp)
+	if (cmd != "") sub(cmd " [^ ]+ ?", "", tmp)
+	sub(/^:/, "", tmp)
+	return trim(tmp)
+}
+{
+	line = $0
+	gsub(/\r/, "", line)
+	if (line !~ /^:/ && line !~ /^PING /) {
+		next
+	}
+	if (line ~ /^PING /) {
+		print_line("** PING " substr(line, 6))
+		next
+	}
+	parse(line)
+	if (cmd ~ /^[0-9][0-9][0-9]$/) {
+		if (cmd == "001" && params_n >= 1) mynick = params[1]
+		out = fmt_numeric(cmd)
+		if (out != "") print_line(out)
+		next
+	}
+	if (cmd == "PRIVMSG") {
+		target = (params_n >= 1 ? params[1] : "")
+		if (trailing == "") trailing = trim(raw_trailing(line))
+		trailing = limit_msg(trailing)
+		sender = trim_nick(nick_from_prefix(prefix))
+		if (should_highlight(trailing) && sender != mynick) {
+			sender_colored = colorize(sender, hl_color, hl_bold)
+			sender = padnick_colored(sender, sender_colored)
+		} else {
+			sender = padnick(sender)
+		}
+		if (is_channel(target)) {
+			target = pad16(trim_chan(target))
+			if (show_ts) print_line(sprintf("[%s] %s %s : %s", ts(), target, sender, trailing))
+			else print_line(sprintf("%s %s : %s", target, sender, trailing))
+		} else {
+			pm = "[" pm_label "]"
+			if (mynick != "" && sender == mynick && target != "") pm = "[" pm_label " " target "]"
+			pm = pad16(trim_chan(pm))
+			if (show_ts) print_line(sprintf("[%s] %s %s : %s", ts(), pm, sender, trailing))
+			else print_line(sprintf("%s %s : %s", pm, sender, trailing))
+		}
+		next
+	}
+	if (cmd == "NOTICE") {
+		target = (params_n >= 1 ? params[1] : "")
+		if (trailing == "") trailing = trim(raw_trailing(line))
+		trailing = limit_msg(trailing)
+		out = sprintf("-%s- %s", trim_nick(nick_from_prefix(prefix)), trailing)
+		if (notice_color != "") out = colorize(out, notice_color, 0)
+		print_line(fmt_action(out, ""))
+		next
+	}
+	if (cmd == "JOIN") {
+		chan = (trailing != "" ? trailing : (params_n >= 1 ? params[1] : ""))
+		if (chan == "") chan = trim(raw_trailing(line))
+		out = sprintf("JOIN %s", trim_chan(chan))
+		if (action_color != "") out = colorize(out, action_color, 0)
+		print fmt_action_line(chan, nick_from_prefix(prefix), out)
+		next
+	}
+	if (cmd == "PART") {
+		chan = (params_n >= 1 ? params[1] : "")
+		if (chan == "") chan = trim(raw_trailing(line))
+		if (trailing == "") trailing = trim(raw_trailing(line))
+		trailing = limit_msg(trailing)
+		if (trailing != "" && trailing != trim_nick(nick_from_prefix(prefix))) {
+			out = sprintf("PART %s %s", trim_chan(chan), quote_reason(trailing))
+		} else {
+			out = sprintf("PART %s", trim_chan(chan))
+		}
+		if (action_color != "") out = colorize(out, action_color, 0)
+		print fmt_action_line(chan, nick_from_prefix(prefix), out)
+		next
+	}
+	if (cmd == "QUIT") {
+		msg = trailing
+		if (msg == "") msg = trim(raw_trailing(line))
+		msg = limit_msg(msg)
+		if (msg != "") out = sprintf("QUIT %s", quote_reason(msg))
+		else out = sprintf("QUIT")
+		if (action_color != "") out = colorize(out, action_color, 0)
+		print fmt_action_line("", nick_from_prefix(prefix), out)
+		next
+	}
+	if (cmd == "NICK") {
+		newnick = (trailing != "" ? trailing : (params_n >= 1 ? params[1] : ""))
+		if (mynick != "" && nick_from_prefix(prefix) == mynick) mynick = newnick
+		out = sprintf("NICK %s", trim_nick(newnick))
+		if (action_color != "") out = colorize(out, action_color, 0)
+		print fmt_action_line("", nick_from_prefix(prefix), out)
+		next
+	}
+	if (cmd == "TOPIC") {
+		chan = (params_n >= 1 ? params[1] : "")
+		out = sprintf("TOPIC %s : %s", trim_chan(chan), limit_msg(trailing))
+		if (action_color != "") out = colorize(out, action_color, 0)
+		print fmt_action_line(chan, nick_from_prefix(prefix), out)
+		next
+	}
+	if (cmd == "MODE") {
+		target = (params_n >= 1 ? params[1] : "")
+		mode = ""
+		if (params_n > 1) {
+			for (i = 2; i <= params_n; i++) mode = mode (i > 2 ? " " : "") params[i]
+		}
+		out = sprintf("MODE %s %s", trim_chan(target), mode)
+		if (action_color != "") out = colorize(out, action_color, 0)
+		print fmt_action_line(target, nick_from_prefix(prefix), out)
+		next
+	}
+
+	if (prefix != "") {
+		out = nick_from_prefix(prefix) " " cmd
+	} else {
+		out = cmd
+	}
+	if (params_n > 0) {
+		for (i = 1; i <= params_n; i++) out = out " " params[i]
+	}
+	if (trailing != "") out = out " :" trailing
+	print_line(out)
+}
+'