Building a Better ii IRC Setup: From Manual Pain to Automated Bliss
Back in 2018, I wrote about setting up ii (IRC it), the wonderfully minimal filesystem-based IRC client from suckless. The core idea is brilliant: use standard Unix tools like tail
and echo
to chat on IRC. But honestly? My original setup was pretty clunky and manual.
Seven years later, I’ve completely rebuilt my ii workflow. What used to be a collection of manual steps and crossed fingers is now a smooth, automated system that actually makes IRC enjoyable to use daily.
What Makes This Better
The new setup fixes all the pain points that made me occasionally ragequit IRC. No more manual authentication dance thanks to proper password handling per network. Multiple IRC networks that just work without thinking about them. Smart timing instead of “sleep 5 and pray” everywhere. Actual error handling so you know when things break. One command to rule them all for startup and management. And dynamic multitail that automatically finds your active channels.
Getting Started
First, install what you need:
# On Arch Linux
sudo pacman -S ii stunnel multitail kitty
# Adjust for your distro
Setting Up stunnel for SSL
Create /etc/stunnel/stunnel.conf
:
setuid = stunnel
setgid = stunnel
CAfile = /etc/ssl/certs/ca-certificates.crt
verify = 2
# Libera Chat
[lb]
client = yes
accept = 127.0.0.1:6698
connect = irc.libera.chat:6697
verifyChain = yes
CApath = /etc/ssl/certs
# Snoonet
[sn]
client = yes
accept = 127.0.0.1:6697
connect = irc.snoonet.org:6697
verifyChain = yes
CApath = /etc/ssl/certs
# OFTC
[of]
client = yes
accept = 127.0.0.1:6699
connect = irc.oftc.net:6697
verifyChain = yes
CApath = /etc/ssl/certs
Enable it:
sudo systemctl enable stunnel
The Main Management Script
This is where the magic happens. Save this as ~/bin/ii-start
and make it executable:
#!/bin/bash
# ii-start - Because manually connecting to IRC is for masochists
# Where everything lives
IRC_HOME="$HOME/irc"
CREDENTIALS_FILE="$HOME/.config/ii/credentials"
# Server configs: network_name -> host:port:nickname
declare -A SERVERS=(
["lb"]="127.0.0.1:6698:your_nick"
["sn"]="127.0.0.1:6697:your_nick"
["of"]="127.0.0.1:6699:your_nick"
)
# What channels to join where
declare -A CHANNELS=(
["lb"]="#archlinux #linux #bash"
["sn"]="#help"
["of"]="#debian"
)
# Passwords (you can put them here or in a separate file)
declare -A NETWORK_CREDENTIALS=(
["lb:your_nick"]="your_libera_password"
["sn:your_nick"]="your_snoonet_password"
["of:your_nick"]="your_oftc_password"
)
# Make sure stunnel is actually running
check_stunnel() {
if ! systemctl is-active --quiet stunnel; then
echo "Starting stunnel..."
sudo systemctl start stunnel || {
echo "Stunnel failed to start. That's not good."
exit 1
}
sleep 2
echo "✓ stunnel is up"
else
echo "✓ stunnel already running"
fi
}
# Wait for ii to actually connect (no more blind sleeping!)
wait_for_connection() {
local server_dir="$1"
local timeout=30
local count=0
while [ $count -lt $timeout ]; do
if [ -p "$server_dir/in" ] && [ -f "$server_dir/out" ]; then
return 0
fi
sleep 1
((count++))
done
return 1
}
# Fire up an ii instance
start_ii_instance() {
local name="$1"
local server_info="${SERVERS[$name]}"
local host=$(echo "$server_info" | cut -d: -f1)
local port=$(echo "$server_info" | cut -d: -f2)
local nick=$(echo "$server_info" | cut -d: -f3)
echo "Connecting to $name as $nick..."
mkdir -p "$IRC_HOME/$name"
ii -i "$IRC_HOME/$name/" -n "$nick" -s "$host" -p "$port" &
if wait_for_connection "$IRC_HOME/$name/$host"; then
echo "✓ $name connected"
else
echo "✗ $name connection timed out"
return 1
fi
}
# Handle NickServ authentication
authenticate() {
local name="$1"
local server_info="${SERVERS[$name]}"
local host=$(echo "$server_info" | cut -d: -f1)
local nick=$(echo "$server_info" | cut -d: -f3)
# Try to find a password
local password="${NETWORK_CREDENTIALS[$name:$nick]}"
if [ -z "$password" ] && [ -f "$CREDENTIALS_FILE" ]; then
password=$(grep "^$name:$nick:" "$CREDENTIALS_FILE" | cut -d: -f3-)
if [ -z "$password" ]; then
password=$(grep "^$nick:" "$CREDENTIALS_FILE" | cut -d: -f2-)
fi
fi
if [ -n "$password" ]; then
echo "Authenticating $nick on $name..."
# Different networks, different syntax (because why make it easy?)
case "$name" in
"of")
echo "/j NickServ IDENTIFY $password" > "$IRC_HOME/$name/$host/in"
;;
*)
echo "/j NickServ IDENTIFY $nick $password" > "$IRC_HOME/$name/$host/in"
;;
esac
# Wait for confirmation
local auth_wait=0
while [ $auth_wait -lt 20 ]; do
local server_out="$IRC_HOME/$name/$host/out"
local nickserv_out="$IRC_HOME/$name/$host/nickserv/out"
if ([ -f "$server_out" ] && tail -n 10 "$server_out" 2>/dev/null | grep -q "You are now identified\|Password accepted\|successfully identified\|now recognized") || \
([ -f "$nickserv_out" ] && tail -n 10 "$nickserv_out" 2>/dev/null | grep -q "You are now identified\|Password accepted\|successfully identified\|now recognized"); then
echo "✓ $nick authenticated on $name"
return 0
fi
sleep 2
((auth_wait += 2))
echo " Still waiting for auth... (${auth_wait}s)"
done
echo "⚠ Auth timeout for $nick on $name (might still work)"
else
echo "⚠ No password found for $nick on $name"
fi
}
# Join all the channels we care about
join_channels() {
local name="$1"
local server_info="${SERVERS[$name]}"
local host=$(echo "$server_info" | cut -d: -f1)
local channels="${CHANNELS[$name]}"
if [ -n "$channels" ]; then
echo "Joining channels for $name: $channels"
for channel in $channels; do
echo " Joining $channel..."
echo "/j $channel" > "$IRC_HOME/$name/$host/in"
sleep 3
if tail -n 5 "$IRC_HOME/$name/$host/out" 2>/dev/null | grep -q "JOIN.*$channel"; then
echo " ✓ Joined $channel"
else
echo " ⚠ Might not have joined $channel"
fi
done
fi
}
# The main event
start_all() {
echo "Starting the IRC circus..."
check_stunnel || exit 1
local started_instances=()
for server in "${!SERVERS[@]}"; do
if start_ii_instance "$server"; then
started_instances+=("$server")
fi
done
if [ ${#started_instances[@]} -eq 0 ]; then
echo "✗ Nothing started. That's disappointing."
exit 1
fi
echo "Letting connections settle..."
sleep 8
echo "=== Time to authenticate ==="
for server in "${started_instances[@]}"; do
authenticate "$server"
done
echo "Waiting for auth to finish..."
sleep 15
echo "=== Joining channels ==="
for server in "${started_instances[@]}"; do
join_channels "$server"
sleep 5
done
echo "✓ All done! (${#started_instances[@]} networks connected)"
echo "Check status: $0 status"
echo "Start chatting: ii-chat"
}
# Kill everything
stop_all() {
echo "Shutting down IRC..."
for server in "${!SERVERS[@]}"; do
local server_info="${SERVERS[$server]}"
local host=$(echo "$server_info" | cut -d: -f1)
local server_dir="$IRC_HOME/$server/$host"
if [ -p "$server_dir/in" ]; then
echo "/quit" > "$server_dir/in"
fi
done
sleep 3
pkill -f "ii -i" 2>/dev/null || true
echo "✓ Everything stopped"
}
# Show what's actually running
show_status() {
echo "=== IRC Status Check ==="
echo
echo "stunnel service:"
if systemctl is-active --quiet stunnel; then
echo "✓ Running"
else
echo "✗ Not running"
fi
echo
echo "ii processes:"
local ii_procs=$(pgrep -f "ii -i" | wc -l)
if [ $ii_procs -gt 0 ]; then
echo "✓ $ii_procs instances running"
else
echo "✗ No ii processes found"
fi
echo
echo "Active channels:"
local channel_count=0
for server in "${!SERVERS[@]}"; do
local server_info="${SERVERS[$server]}"
local host=$(echo "$server_info" | cut -d: -f1)
local channels="${CHANNELS[$server]}"
for channel in $channels; do
local out_file="$IRC_HOME/$server/$host/$channel/out"
if [ -f "$out_file" ]; then
echo "✓ $server/$channel"
((channel_count++))
fi
done
done
if [ $channel_count -eq 0 ]; then
echo "✗ No active channels"
fi
}
# Generate the multitail command for viewing everything
show_multitail_command() {
local cmd="multitail -CS ii -s 2"
local files_found=0
local quiet_mode=false
if [ "$1" = "--quiet" ]; then
quiet_mode=true
fi
for server in "${!SERVERS[@]}"; do
local server_info="${SERVERS[$server]}"
local host=$(echo "$server_info" | cut -d: -f1)
local channels="${CHANNELS[$server]}"
for channel in $channels; do
local out_file="$IRC_HOME/$server/$host/$channel/out"
if [ -f "$out_file" ]; then
cmd="$cmd $out_file"
((files_found++))
fi
done
done
if [ $files_found -eq 0 ]; then
if [ "$quiet_mode" = false ]; then
echo "No active channels found. Start ii first!"
fi
return 1
fi
if [ "$quiet_mode" = true ]; then
echo "$cmd"
else
echo "Multitail command for $files_found channels:"
echo "$cmd"
fi
}
# Handle commands
case "${1:-start}" in
start)
start_all
;;
stop)
stop_all
;;
restart)
stop_all
sleep 3
start_all
;;
status)
show_status
;;
multitail)
show_multitail_command
;;
multitail-quiet)
show_multitail_command --quiet
;;
*)
echo "Usage: $0 {start|stop|restart|status|multitail|multitail-quiet}"
echo
echo "Commands:"
echo " start - Fire up stunnel and ii"
echo " stop - Kill everything"
echo " restart - Stop and start again"
echo " status - Show what's running"
echo " multitail - Show command for viewing all channels"
exit 1
;;
esac
Make it executable:
chmod +x ~/bin/ii-start
Optional: Separate Credentials File
If you don’t want passwords in the main script:
mkdir -p ~/.config/ii
cat > ~/.config/ii/credentials << 'EOF'
# Format: network:nickname:password OR just nickname:password
lb:your_nick:your_libera_password
sn:your_nick:your_snoonet_password
of:your_nick:your_oftc_password
EOF
chmod 600 ~/.config/ii/credentials
Making multitail Look Good
Create ~/.multitailrc
for better colors:
cat > ~/.multitailrc << 'EOF'
colorscheme:irc
cs_re:green:.*your_nick.*
cs_re_s:yellow:(((http|https|ftp|gopher)|mailto):(//)?[^ <>\"[:blank:]]*|(www|ftp)[0-9]?\.[-a-z0-9.]+)
cs_re:cyan:.*has joined #.*
cs_re:blue:.*changed mode.*
cs_re:red:.*has quit.*
cs_re:yellow:.*NOTICE.*
titlebar:%m %u@%h %f (%t) [%l]
EOF
Handy Aliases
Add these handy aliases to your shell config to make life easier. The basic ii management commands like ii-start, ii-stop, ii-restart, and ii-status make controlling everything simple. There’s also an ii-chat function that launches multitail with all your active channels in one window. For quick messaging, ii-msg lets you send messages from the command line. And if you want to monitor individual channels, you can set up aliases like ii-arch and ii-linux that tail specific channel logs.
Reload your shell:
source ~/.bashrc # or ~/.zshrc
Daily Usage
Here’s how you’d typically use this setup. Start everything with the simple command ii-start. View all your channels at once with ii-chat. Send quick messages using ii-msg followed by the server, channel, and your message. Check what’s running anytime with ii-status.
Sway Integration (Bonus)
If you use Sway, add this to your config:
bindsym $mod+i exec ~/bin/ii-sway chat
And create ~/bin/ii-sway
:
#!/bin/bash
# Sway integration for ii
case "$1" in
"chat")
cmd=$(~/bin/ii-start multitail-quiet)
if [ $? -eq 0 ] && [ -n "$cmd" ]; then
exec kitty -e sh -c "$cmd"
else
notify-send "ii IRC" "No active channels. Start ii first!"
fi
;;
"status")
status=$(~/bin/ii-start status 2>&1)
notify-send "ii IRC Status" "$status"
;;
*)
echo "Usage: $0 {chat|status}"
exit 1
;;
esac
Why This Is So Much Better
This setup is so much better for several reasons. It actually works reliably instead of using “sleep 5 and hope for the best” scripting since the new setup waits for actual confirmation at each step. Zero manual work is required because one command connects to all your networks, handles authentication, and joins your channels. Multi-network support is made easy since each network can have different settings, passwords, and channels without any mental overhead. You know when things break thanks to proper error handling and status reporting, which means no more mystery IRC failures. And best of all, it’s still just ii under the hood, so all the Unix philosophy goodness remains. You’re still just using files and pipes to chat, but the automation handles all the boring setup stuff.
Wrapping Up
This setup transforms ii from “interesting but painful” to “actually pleasant to use daily.” The core ii experience is unchanged, you’re still just echoing to files and tailing logs. But now all the tedious connection management happens automatically.
Whether you’re lurking in development channels, helping newbies, or just enjoying the simplicity of text-based IRC, this gives you a solid foundation that won’t make you want to ragequit when something breaks.
Time to get back to chatting!