# How IRC-Style Commands Are Processed and Routed in BitChat: A Complete Technical Breakdown

> Discover how BitChat processes and routes IRC-style commands through its three-layer pipeline: UI detection, parsing, and execution. Learn the technical breakdown.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: deep-dive
- Published: 2026-08-02

---

**BitChat routes IRC-style commands through a three-layer pipeline: UI detection in `ChatViewModel`, parsing in `CommandProcessor`, and execution via the `CommandContextProvider` protocol that bridges commands to the conversation store and mesh transport.**

BitChat implements a classic IRC-style command system where users type `/` followed by a command name to trigger actions like private messaging, peer discovery, or mesh diagnostics. This architecture is tightly integrated with SwiftUI's **MVVM pattern** and designed for decentralized mesh networking. Understanding how IRC-style commands are processed and routed in BitChat reveals a clean separation between parsing logic, business rules, and UI state management.

## Command Pipeline Architecture

The command system operates across three distinct layers with well-defined responsibilities:

| Layer | Components | Responsibility |
|-------|-----------|--------------|
| **UI → ViewModel** | `ChatViewModel` | Detects leading `/` and forwards raw text to the command engine |
| **Command Engine** | `CommandProcessor` | Parses tokens, validates arguments, dispatches handlers |
| **Routing / Execution** | `CommandContextProvider` protocol | Executes concrete actions via `ChatViewModel` methods |

## Detecting Commands in the View Layer

When a user sends a message, `ChatViewModel.sendMessage(_:)` at [`bitchat/ViewModels/ChatViewModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/ViewModels/ChatViewModel.swift) checks for the command prefix:

```swift
@MainActor
func sendMessage(_ text: String) {
    if text.hasPrefix("/") {
        let result = commandProcessor.process(text)
        handleCommandResult(result)
        return
    }
    // Normal public message handling proceeds here
}

```

This early return prevents commands from being broadcast as plaintext to the mesh. The `commandProcessor` property is injected during bootstrap, ensuring testability.

## Parsing and Dispatching in CommandProcessor

The `CommandProcessor.process(_:)` method in [`bitchat/Services/CommandProcessor.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/CommandProcessor.swift) implements a classic token-based parser:

```swift
func process(_ command: String) -> CommandResult {
    let parts = command.split(separator: " ", maxSplits: 1, omittingEmptySubsequences: false)
    guard let cmd = parts.first else { 
        return .error(message: "Invalid command") 
    }
    let args = parts.count > 1 ? String(parts[1]) : ""
    
    switch cmd {
    case "/msg", "/m":      return handleMessage(args)
    case "/who", "/w":      return handleWho()
    case "/clear":          return handleClear()
    case "/ping":           return handlePing(args)
    case "/trace":          return handleTrace(args)
    // Additional commands...
    default:                return .error(message: "Unknown command: \(cmd)")
    }
}

```

**Key design decisions:**

- **Multiple aliases** (`/msg` and `/m`) map to identical handlers
- **Argument preservation** uses `maxSplits: 1` to keep quoted strings intact
- **Exhaustive switch** ensures every path returns a `CommandResult`

## The CommandContextProvider Protocol: Router Interface

Commands don't execute directly—they delegate through the `CommandContextProvider` protocol defined in [`ChatViewModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ChatViewModel.swift). This protocol abstracts all side effects:

```swift
protocol CommandContextProvider: AnyObject {
    var nickname: String { get }
    var activeChannel: ChannelID { get }
    var selectedPrivateChatPeer: PeerID? { get }
    
    // Peer lookup and chat management
    func getPeerIDForNickname(_ nickname: String) -> PeerID?
    func startPrivateChat(with peerID: PeerID)
    
    // Message operations
    func sendPrivateMessage(_ content: String, to peerID: PeerID)
    func sendPublicMessage(_ content: String)
    func addPublicSystemMessage(_ content: String)
    func addCommandOutput(_ content: String, to destination: CommandOutputDestination)
    
    // Current context for async replies
    func currentCommandDestination() -> CommandOutputDestination
}

```

`ChatViewModel` is the sole implementer, making it the **single source of truth** for command routing. This design enables unit testing of `CommandProcessor` with mock providers.

## Concrete Routing Examples

### Private Chat Commands

The `/msg` handler demonstrates nickname resolution and chat initiation:

```swift
private func handleMessage(_ args: String) -> CommandResult {
    let argParts = args.split(separator: " ", maxSplits: 1)
    guard let nickname = argParts.first.map(String.init) else {
        return .error(message: "Usage: /msg <nickname> [message]")
    }
    
    guard let peerID = contextProvider?.getPeerIDForNickname(nickname) else {
        return .error(message: "'\(nickname)' not found")
    }
    
    contextProvider?.startPrivateChat(with: peerID)
    
    if argParts.count > 1 {
        let message = String(argParts[1])
        contextProvider?.sendPrivateMessage(message, to: peerID)
    }
    
    return .success(message: "Started private chat with \(nickname)")
}

```

### Asynchronous Mesh Diagnostics

Commands like `/ping` must survive **chat context switches**. The solution captures the destination before launching async work:

```swift
private func handlePing(_ args: String) -> CommandResult {
    guard let targetNickname = args.split(separator: " ").first.map(String.init),
          let target = contextProvider?.getPeerIDForNickname(targetNickname) else {
        return .error(message: "Usage: /ping <nickname>")
    }
    
    // Capture destination synchronously
    let destination = contextProvider?.currentCommandDestination() ?? .meshTimeline
    
    meshDiagnostics?.sendMeshPing(to: target) { [weak contextProvider] result in
        guard let rtt = result else {
            contextProvider?.addCommandOutput(
                "No reply from \(targetNickname)", 
                to: destination
            )
            return
        }
        contextProvider?.addCommandOutput(
            "Pong from \(targetNickname): \(rtt.ms) ms",
            to: destination
        )
    }
    
    return .success(message: "Pinging \(targetNickname)...")
}

```

The `currentCommandDestination()` call is critical—if the user switches channels after sending `/ping`, the reply still appears in the original chat.

## UI Consistency: CommandInfo Enum

The autocomplete suggestions in the composer are driven by `CommandInfo` in [`bitchat/Models/CommandInfo.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Models/CommandInfo.swift):

```swift
enum CommandInfo: String, Identifiable {
    case block, clear, group, help, hug
    case message = "msg"
    case slap, pay, unblock, who
    case favorite = "fav"
    case unfavorite = "unfav"
    case ping, trace, drop
    
    var id: String { rawValue }
    
    static var allVisible: [CommandInfo] {
        [.message, .who, .ping, .clear, .help]
    }
}

```

**This enum guarantees UI-engine consistency**—raw values must match the strings accepted by `CommandProcessor`. The compiler enforces this through shared string literals or team conventions.

## Visual Styling: IRCToggleStyle

The "IRC-style pill" visual toggle is implemented in [`bitchat/Views/Components/IRCToggleStyle.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Views/Components/IRCToggleStyle.swift). This component renders command states as inline pills in settings screens and the composer.

## Summary

- **Detection**: `ChatViewModel.sendMessage(_:)` intercepts `/`-prefixed input before mesh broadcast
- **Parsing**: `CommandProcessor` tokenizes commands and dispatches to typed handlers
- **Routing**: All side effects flow through `CommandContextProvider`, with `ChatViewModel` as the sole implementer
- **Async safety**: Commands like `/ping` capture `currentCommandDestination()` before launching asynchronous mesh operations
- **UI parity**: `CommandInfo` enum ensures autocomplete suggestions align with parser capabilities

## Frequently Asked Questions

### How does BitChat prevent commands from being sent as plaintext?

`ChatViewModel` checks `text.hasPrefix("/")` before any message transmission. Commands are consumed entirely by `commandProcessor.process(_:)` and never reach the mesh transport layer unless explicitly routed through `sendPublicMessage(_:)` or `sendPrivateMessage(_:to:)` by a handler.

### Can I add custom IRC-style commands to BitChat?

Yes. Extend `CommandProcessor.process(_:)` with new switch cases, implement the handler method, and add corresponding methods to `CommandContextProvider`. For UI autocomplete, add entries to `CommandInfo`. The protocol-based design isolates changes to three files.

### Why use a protocol instead of direct ViewModel calls?

The `CommandContextProvider` protocol enables **unit testing** of `CommandProcessor` without instantiating the full `ChatViewModel`. Mocks can verify command behavior without triggering actual network operations or database mutations.

### How does `/ping` know where to display results if I switch chats?

The handler captures `currentCommandDestination()` **synchronously** before invoking the async mesh diagnostic. The closure retains this destination value, ensuring `addCommandOutput(_:to:)` targets the correct conversation regardless of subsequent UI navigation.