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!

· ii, irc, suckless, automation, shell, arch-linux