Skip to content

Repository files navigation

    ██████╗ █████╗ ██████╗  █████╗ ██╗   ██╗ █████╗ ███╗   ██╗
   ██╔════╝██╔══██╗██╔══██╗██╔══██╗██║   ██║██╔══██╗████╗  ██║
   ██║     ███████║██████╔╝███████║██║   ██║███████║██╔██╗ ██║
   ██║     ██╔══██║██╔══██╗██╔══██║╚██╗ ██╔╝██╔══██║██║╚██╗██║
   ╚██████╗██║  ██║██║  ██║██║  ██║ ╚████╔╝ ██║  ██║██║ ╚████║
    ╚═════╝╚═╝  ╚═╝╚═╝  ╚═╝╚═╝  ╚═╝  ╚═══╝  ╚═╝  ╚═╝╚═╝  ╚═══╝

A native macOS IRC client loosely inspired by the good ol' days of mIRC.

ci docs Swift 6.2 macOS 15+ BSD 3-Clause dependencies 0 stage 1: 11/11 stage 2: 9/17


What this is

mIRC got a great deal right in 1995 and most of it is still right. A tree of windows you can actually navigate. A status window that shows the raw protocol when something breaks. Scriptable everything. A client that treats IRC as a protocol rather than hiding it behind a chat metaphor.

This is that client, rebuilt natively for macOS: Swift 6 with strict concurrency, a SwiftUI shell with AppKit where AppKit is genuinely better, and zero external dependencies.

Note

Early days. The protocol layer is written and tested; the app currently launches to an empty window. Everything below marks what exists and what does not — see Progress.

Why another IRC client

  • The protocol is not hidden. A status window shows raw traffic both directions, /debug streams it to a window or a file, and every unrecognized line still lands somewhere visible instead of being silently dropped.
  • Bouncer-first. soju is a primary target, not an afterthought: bouncer-networks and chathistory land in stage 2, not stage 3.
  • Credentials are handled like credentials. Redaction happens on insert into the trace buffer, never at export, so a password is never resident anywhere it could be scraped later. Secrets live in the macOS Keychain, never in a config file.
  • Nothing is written to the source tree. Settings, logs and caches follow XDG paths — see Where your data lives.
  • No dependency chain. One SwiftPM package, four modules, nothing vendored but test fixtures.

The name

After Planet Caravan, Black Sabbath, 1970. The app is not themed after the band and will not be; the name simply fit. There may be an easter egg or two.

Non-goals

Not a Slack replacement, not a "modern reimagining" of chat, not a bridge to five other protocols. It is an IRC client.

The shape of it

Where this is going — a mIRC-style layout with a network tree, header bar, nick list, and a scrollback view that holds a hundred thousand lines without complaint.

┌─ Caravan ─────────────────────────────────────────────────────────────────────┐
│                         │ #caravan - a native macOS IRC client                │
│                         ├──────────────────────────────────────────────┬──────┤
│ * Libera.Chat           │ [12:04:17] *** Joins: alice (~a@example.net) │ @ops │
│   |- #caravan         * │ [12:04:22] <bob>   parser passes the corpus  │ @bob │
│   |- #swift             │ [12:04:31] <alice> all 66 cases?             │ +eve │
│   `- >NickServ          │ [12:04:36] * bob nods                        │  ann │
│                         │ [12:04:41] -NickServ- You are now identified │  joe │
│ * soju (bouncer)        │ [12:05:02] *** eve is now known as evelyn    │      │
│   |- #ops             o │                                              │      │
│   `- #dev               │                                              │      │
├─────────────────────────┴──────────────────────────────────────────────┴──────┤
│ [#caravan] > /msg alice thanks!                                               │
└───────────────────────────────────────────────────────────────────────────────┘
    * highlight    o activity

A mockup, not a screenshot. The window is currently empty — see Progress.

Architecture

Four modules, one app. Dependencies point one way only.

                      ┌───────────────────────────┐
                      │            App            │  SwiftUI shell
                      │   NSTextView scrollback   │  AppKit where it counts
                      └─────────────┬─────────────┘
                                    │
                      ┌─────────────▼─────────────┐
                      │        IRCSession         │  registration, ISUPPORT,
                      │   actor - event stream    │  state machine, events
                      └─────────────┬─────────────┘
                                    │
                      ┌─────────────▼─────────────┐
                      │       IRCTransport        │  NWConnection, TLS,
                      │   actor - line framing    │  send queue, reconnect
                      └─────────────┬─────────────┘
                                    │
                  ┌─────────────────┴───────────────────────┐
                  │                                         │
        ┌─────────▼─────────┐                   ┌───────────▼───────────┐
        │    Diagnostics    │                   │      IRCProtocol      │
        │ os.Logger         │                   │ parse - serialize     │
        │ Redactor          │                   │ IRCv3 tags - masks    │
        │ TraceBuffer       │                   │ casemapping           │
        │ Signposts         │                   │                       │
        └───────────────────┘                   └───────────────────────┘
             Darwin-only                         pure · builds on Linux

IRCProtocol has no I/O, no Foundation, no Darwin APIs — nothing but the standard library. CI builds and runs its test suite on Linux, so the moment someone reaches for AppKit or os.Logger in there, a job fails. The rule is mechanical, not a comment in a file.

Progress

Stage 2 — a mIRC daily driver

Seventeen prompts. STAGE2-PROMPTS.md is authoritative; every roadmap item below is attached to one of them.

# Prompt Status
1 Formatting codes: rendering ✅ done
2 The input field grows up ✅ done
3 Capabilities and authentication ✅ done
4 Multi-network, and the bouncer ✅ done
5 Queries and CTCP ✅ done
6 Activity and navigation at scale ✅ done
7 Windows and chrome ✅ done
8 Commands and modes ✅ done
9 Things you can do to what is in the buffer ✅ done
10 Options ✅ done
11 The Dashboard and the server list ✅ done
12 Logging ✅ done
13a What deserves none ✅ done
13b What deserves your attention ✅ done
14 Presence ✅ done
15 Channel list ✅ done
16 Flood protection ✅ done
17 Buffer utilities ✅ done

Stage 2 is done when you would use this instead of your current client.

Stage 1 — connect, join, chat · complete

You can idle in a channel on Libera and hold a conversation. STAGE1-PROMPTS.md is the finished queue, kept for the record.

Eleven prompts, all landed
# Prompt Status
1 Scaffold — package, Xcode project, CI ✅ done
2 Diagnostics — logging, redaction, wire tracing ✅ done
3 Message parser — IRCv3 tags, casemapping, masks ✅ done
4 Transport — line framing, TLS, NWConnection ✅ done
5 Registration and connection state machine ✅ done
6 Typed event model ✅ done
7 Minimal UI and the scrollback view ✅ done
8 Channel and user state ✅ done
9 Command line ✅ done
10 Status window, timestamps, line rendering ✅ done
11 Debug & Settings canvas ✅ done

Roadmap

Full detail in PLAN.md.

Stage 1 — Basic · connect, join, chat
  • TLS connection, registration, ISUPPORT, PING/PONG, reconnect with backoff
  • Full IRCv3 message parsing: tags with escaping, sources, numerics
  • Casemapping (ascii / rfc1459 / strict-rfc1459) and wildcard mask matching
  • Channel and user state, nick list ordered by PREFIX rank
  • /join /part /msg /me /nick /quit /raw, unknown commands passed straight through
  • Status window with raw traffic, timestamps, mIRC-style event lines
  • Debug & Settings canvas — /debug and the live wire trace, not shaped like a chat window
  • Settings in a plain-text config you can edit by hand without the app clobbering it
Stage 2 — Intermediate · a daily driver
  • Formatting — bold, italic, underline, strikethrough, monospace, reverse, and the full 99-colour ^C palette including the extended 16–98 range
  • Multi-window — four activity states and highlight badges in the tree, next-unread and next-highlight keys, MRU Ctrl+Tab, a ⌘K quick-switcher, ⌘1–9 bindings, detachable windows
  • Multi-network — direct connections and soju's bouncer-networks, behind one sidebar model that does not care which is in play
  • Bouncer supportchathistory backfill, de-duplicated against local logs
  • Queries and CTCP (VERSION PING TIME USERINFO CLIENTINFO FINGER ACTION)
  • Full command set — /whois /whowas /who /mode /op /kick /ban /kickban /topic /invite /notice /away /list /names /ignore /oper /amsg /ame /ctcp /clear
  • Tab completion, mIRC-style cycling with a configurable suffix
  • Mode tracking, ban/quiet/invex list dialogs, channel modes sheet
  • Nick-list and channel context menus
  • Options dialog — Connect, IRC, Display, Colors, Sounds, Logging, Mouse
  • Server list with groups, autojoin, perform-on-connect, connect-on-startup
  • Logging in mIRC's layout, log viewer, reload-last-N-lines on join
  • Highlights, keyword and regex lists, per-event sounds, notifications, Dock badge
  • Ignore list with wildcard masks and mIRC-style level flags
  • Notify list via MONITOR, with ISON polling as fallback
  • /list channel browser with filters, URL catcher, away system, flood protection
  • SASLPLAIN, EXTERNAL (CertFP), SCRAM-SHA-256; NickServ fallback
  • IRCv3cap-notify multi-prefix away-notify account-notify extended-join userhost-in-names server-time message-tags echo-message batch chghost invite-notify setname standard-replies labeled-response
Stage 3 — Advanced · mIRC parity
  • DCC — CHAT, SEND, GET with resume, passive/reverse DCC for NAT, a transfer manager, drag-and-drop onto a nick to send
  • Scripting in JavaScript, not mIRC's language. Declarative aliases and popups for the one-liner cases, JavaScriptCore for real logic. A bare JSContext has no require, fetch or filesystem at all, so capabilities are granted rather than restricted — and Safari Web Inspector attaches for actual breakpoints.
  • Identd, SOCKS5 and HTTP proxies, Tor
  • Themes over stage 1's one format-table seam, F-key bindings, arbitrary shortcuts
  • User levels and access lists, paste protection, spell check
  • soju extras — filehost, metadata, search, webpush; ZNC compatibility quirks
  • Full-text search across all logged history
Stage 4 — Polish and release
  • Accessibility, localization, performance work on the scrollback pipeline
  • Distribution as a Homebrew cask from our own tap; Developer ID signing and notarization
  • mIRC settings importer — read mirc.ini, servers.ini, remote.ini
  • Optional iCloud sync, optional iOS companion sharing the core modules

Building

Requires Xcode 26+ on macOS 15+.

git clone https://github.com/Lacuna-Research/caravan.git
cd caravan
make hooks          # install the pre-commit hook — once, after cloning
make all            # build, test, lint, docs check, app
Target What it does
make build Compile the package
make test Run the test suite
make app Build the macOS app with xcodebuild (Debug)
make install Build Release and put it in /Applications
make fmt Format with the toolchain's swift format
make lint Format check, strict
make check Documentation discipline
make hooks Install the pre-commit hook

make install is the one to use if you actually want to run Caravan: it builds Release, where make app builds Debug for acceptance runs, and it derives the built product's path rather than assuming one, so it installs the checkout you ran it from.

swift-testing ships with Xcode rather than Command Line Tools, so make test and make app need full Xcode. make build, make lint and make check work with CLT alone.

Where your data lives

Nothing is ever written inside the source tree — not even under a gitignored path.

What Where
Settings caravan.conf in $XDG_CONFIG_HOME/caravan/, default ~/.config/caravan/
Logs, scrollback $XDG_DATA_HOME/caravan/, default ~/.local/share/caravan/
Caches $XDG_CACHE_HOME/caravan/, default ~/.cache/caravan/
Passwords, client certs macOS Keychain — never a file

Config files are plain text and user-editable; treat their paths as public API. caravan.conf is key = value, one per line, # for a comment. Your comments and any keys the app does not recognise survive being written back, and a key that is not there takes its default — so the file only ever holds what you actually changed.

How this gets built

One prompt per branch, one PR, squash-merged once CI is green. main is protected: four required checks, no force-pushes, admins not exempt.

The interesting part is that the project's own rules are machine-checked. Scripts/check-docs.sh runs as a pre-commit hook and in CI, and fails on:

  • CLAUDE.md over 100 lines — the cap forces pruning instead of accretion
  • any edit to an existing BUILD-LOG.md line — it is append-only
  • a Sources/ change with no build-log entry
  • a missing or malformed status line, or a progress badge that disagrees with it
  • carry-forward notes that outlived the prompt they were addressed to
  • undeclared SwiftPM dependencies

Two git hooks and a Stop hook cover what a diff can't see: no commits to main, no pushing a worktree-* branch before renaming it, and no worktree left behind after its PR merged.

File Purpose
PLAN.md Living roadmap across four stages
STAGE1-PROMPTS.md The ten stage-1 work units, authoritative
BUILD-LOG.md Append-only: decisions, deviations, surprises, measurements
CLAUDE.md Build standards and working method

BUILD-LOG.md is the one worth reading. It records what was decided and what was rejected and why — including the mistakes, which stay in the log alongside their corrections rather than being quietly edited away.

Licence

BSD 3-Clause. Copyright (c) 2026, Lacuna Research.

The vendored ircdocs/parser-tests corpus is CC0-1.0 and imposes no conditions.


Swift 6 · zero dependencies · every decision written down

About

Caravan — a native macOS IRC client loosely inspired by the good ol' days of mIRC. Swift 6, no external dependencies.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages