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:
- Automated authentication with network-specific password handling
- Multiple IRC networks with different credentials
- Intelligent timing instead of blind sleep delays
- Proper error handling and status monitoring
- One-command startup and management
- Dynamic multitail integration that adapts to active channels
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:
- Check and start stunnel if needed
- Connect to all configured networks
- Authenticate with NickServ on each network
- Join your configured channels
- 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:
- Add networks: Extend the
SERVERSarray - Configure channels: Modify the
CHANNELSarray - Adjust timing: Change sleep values if your network is slow
- Add features: The modular design makes it easy to extend
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!