An Improved ii IRC Setup: Automation, Multiple Networks, and Better User Experience

Back in 2018, I wrote about setting up ii (IRC it), the minimalist filesystem-based IRC client from suckless. While the basic concept remains brilliant—using standard Unix tools like tail and echo to interact with IRC—the setup I described was fairly manual and inefficient.

Fast forward to 2025, and I’ve completely overhauled my ii workflow with proper automation, multi-network support, and significantly better user experience. Here’s how to set up a modern, robust ii IRC environment.

What Makes This Setup Better

The new approach addresses several pain points from the original setup:

Prerequisites

You’ll need these packages installed:

# On Arch Linux
sudo pacman -S ii stunnel multitail kitty

# On other distributions, adjust package names accordingly

Initial Setup

1. Install and Configure stunnel

Create the stunnel configuration for SSL/TLS support:

# /etc/stunnel/stunnel.conf
setuid = stunnel
setgid = stunnel
CAfile = /etc/ssl/certs/ca-certificates.crt
verify = 2

# Libera Chat (formerly Freenode)
[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 the stunnel service:

sudo systemctl enable stunnel

2. Create the Main Management Script

Save this as ~/bin/ii-start and make it executable:

#!/bin/bash
# ii-start - Automated ii IRC client management

# Configuration
IRC_HOME="$HOME/irc"
CREDENTIALS_FILE="$HOME/.config/ii/credentials"

# Server configurations (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"
)

# Channel configurations
declare -A CHANNELS=(
    ["lb"]="#archlinux #linux #bash"
    ["sn"]="#help"
    ["of"]="#debian"
)

# Built-in credentials (optional - you can put passwords here or use credentials file)
declare -A NETWORK_CREDENTIALS=(
    ["lb:your_nick"]="your_libera_password"
    ["sn:your_nick"]="your_snoonet_password"
    ["of:your_nick"]="your_oftc_password"
)

# Check if stunnel is running
check_stunnel() {
    if ! systemctl is-active --quiet stunnel; then
        echo "Starting stunnel via systemd..."
        sudo systemctl start stunnel || {
            echo "Failed to start stunnel service"
            exit 1
        }
        sleep 2
        echo "✓ stunnel service started"
    else
        echo "✓ stunnel service already running"
    fi
}

# Wait for ii instance to connect
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
}

# Start individual 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 "Starting ii instance: $name ($nick@$host:$port)"
    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 timeout"
        return 1
    fi
}

# Authenticate with NickServ
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)
    
    # Get password from built-in credentials or file
    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 use different IDENTIFY syntax
        case "$name" in
            "of")
                # OFTC uses: IDENTIFY password (no nickname)
                echo "/j NickServ IDENTIFY $password" > "$IRC_HOME/$name/$host/in"
                ;;
            *)
                # Most networks use: IDENTIFY nickname password  
                echo "/j NickServ IDENTIFY $nick $password" > "$IRC_HOME/$name/$host/in"
                ;;
        esac
        
        # Wait for authentication 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 successfully on $name"
                return 0
            fi
            
            sleep 2
            ((auth_wait += 2))
            echo "  Waiting for $nick authentication... (${auth_wait}s)"
        done
        
        echo "⚠ $nick authentication timeout on $name (may still be working)"
    else
        echo "⚠ No password found for $nick on $name"
    fi
}

# Join configured channels
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 "    ⚠ May not have joined $channel"
            fi
        done
    fi
}

# Main startup function
start_all() {
    echo "Starting ii IRC setup..."
    
    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 "✗ No ii instances started successfully"
        exit 1
    fi
    
    echo "Waiting for connections to stabilize..."
    sleep 8
    
    echo "=== Authentication Phase ==="
    for server in "${started_instances[@]}"; do
        authenticate "$server"
    done
    
    echo "Waiting for authentication to complete..."
    sleep 15
    
    echo "=== Channel Joining Phase ==="
    for server in "${started_instances[@]}"; do
        join_channels "$server"
        sleep 5
    done
    
    echo "✓ ii setup complete (${#started_instances[@]} instances started)"
    echo "You can check status with: $0 status"
    echo "View channels with: ii-chat"
}

# Stop all instances
stop_all() {
    echo "Stopping ii instances..."
    
    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 "✓ ii instances stopped"
}

# Show status
show_status() {
    echo "=== ii IRC Status ==="
    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 running"
    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 found"
    fi
}

# Generate multitail command
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 IRC channels found. Start ii first with: $0 start"
        fi
        return 1
    fi
    
    if [ "$quiet_mode" = true ]; then
        echo "$cmd"
    else
        echo "Multitail command ($files_found channels):"
        echo "$cmd"
    fi
}

# Main command processing
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          - Start stunnel and ii instances"
        echo "  stop           - Stop all ii instances" 
        echo "  restart        - Stop and restart everything"
        echo "  status         - Show current status"
        echo "  multitail      - Show multitail command for active channels"
        echo "  multitail-quiet - Output just the multitail command (for scripts)"
        exit 1
        ;;
esac

Make it executable:

chmod +x ~/bin/ii-start

3. Set Up Credentials (Optional)

If you prefer to store passwords in a separate file instead of the script:

mkdir -p ~/.config/ii
cat > ~/.config/ii/credentials << 'EOF'
# Format: network:nickname:password OR 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

4. Configure Multitail Colors

Create ~/.multitailrc for better IRC formatting:

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

5. Add Convenient Aliases

Add these to your ~/.bashrc or ~/.zshrc:

# ii IRC aliases
alias ii-start='~/bin/ii-start start'
alias ii-stop='~/bin/ii-start stop'
alias ii-restart='~/bin/ii-start restart'
alias ii-status='~/bin/ii-start status'

# ii-chat function
ii-chat() {
    local cmd=$(~/bin/ii-start multitail-quiet)
    if [ $? -eq 0 ] && [ -n "$cmd" ]; then
        kitty -e sh -c "$cmd"
    else
        echo "Error: Could not generate multitail command. Make sure ii is running."
        return 1
    fi
}

# Quick message functions
ii-msg() {
    local server="$1"
    local channel="$2"
    shift 2
    local message="$*"
    echo "$message" > "$HOME/irc/$server/127.0.0.1/$channel/in"
}

# Individual channel monitoring
alias ii-arch='tail -f ~/irc/lb/127.0.0.1/#archlinux/out'
alias ii-linux='tail -f ~/irc/lb/127.0.0.1/#linux/out'

Don’t forget to reload your shell configuration:

source ~/.bashrc  # or ~/.zshrc

Usage

Starting Everything

Simply run:

ii-start

The script will:

  1. Check and start stunnel if needed
  2. Connect to all configured networks
  3. Authenticate with NickServ on each network
  4. Join your configured channels
  5. Provide status updates throughout

Viewing Conversations

Launch the multitail viewer:

ii-chat

This opens a kitty terminal with multitail showing all your active channels in a split-screen view.

Sending Messages

You can send messages in several ways:

Quick one-liner:

ii-msg lb "#archlinux" "Hello from the command line!"

Interactive editing:

# Navigate to the channel directory
cd ~/irc/lb/127.0.0.1/#archlinux

# Edit your message in vim
vim message.txt

# Send it when ready
cat message.txt > in

Direct echo:

echo "Your message here" > ~/irc/lb/127.0.0.1/#archlinux/in

Management Commands

ii-status    # Check what's running
ii-stop      # Stop everything
ii-restart   # Restart everything

Sway/i3 Integration

If you use Sway or i3, add this keybinding to your config:

# Sway config (~/.config/sway/config)
bindsym $mod+i exec ~/bin/ii-sway chat

# i3 config (~/.config/i3/config)  
bindsym $mod+i exec ~/bin/ii-sway chat

Create the integration script as ~/bin/ii-sway:

#!/bin/bash
case "$1" in
    "chat")
        cmd=$(~/bin/ii-start multitail-quiet)
        if [ $? -eq 0 ] && [ -n "$cmd" ]; then
            exec kitty -e sh -c "$cmd"
        fi
        ;;
    "compose")
        # Quick message interface using wofi/rofi
        channel=$(echo -e "#archlinux\n#linux\n#bash" | wofi --dmenu --prompt "Channel:")
        if [ -n "$channel" ]; then
            message=$(echo "" | wofi --dmenu --prompt "Message to $channel:")
            if [ -n "$message" ]; then
                echo "$message" > "$HOME/irc/lb/127.0.0.1/$channel/in"
                notify-send "IRC" "Message sent to $channel"
            fi
        fi
        ;;
esac

Key Improvements Over the 2018 Setup

Reliability: No more guessing with sleep timers—the script actually waits for and verifies each step.

Multi-Network Support: Easily manage multiple IRC networks with different credentials.

Network-Specific Handling: Different networks have different requirements (like OFTC’s unique IDENTIFY syntax).

Error Handling: Comprehensive error detection and helpful status messages.

Automation: One command starts everything, handles authentication, and joins channels.

Status Monitoring: Always know what’s running and what’s not.

Dynamic Integration: Multitail automatically adapts to your active channels.

Customization

The script is designed to be easily customizable:

Conclusion

This modern ii setup transforms the minimalist IRC client into a practical, automated solution. While ii’s filesystem-based approach remains unchanged, the surrounding automation makes it much more pleasant to use daily.

The beauty of ii is still there—you’re just using standard Unix tools to interact with IRC. But now those tools are wrapped in intelligent automation that handles the tedious setup work, letting you focus on the conversations.

Whether you’re monitoring development channels, participating in community support, or just enjoying the simple pleasure of a text-based IRC client, this setup provides a robust foundation that’s both powerful and maintainable.

Happy IRC-ing!

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