How IRC-Style Commands Are Processed and Routed in BitChat: A Complete Technical Breakdown
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 checks for the command prefix:
@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 implements a classic token-based parser:
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 (
/msgand/m) map to identical handlers - Argument preservation uses
maxSplits: 1to 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. This protocol abstracts all side effects:
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:
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:
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:
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. 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:
CommandProcessortokenizes commands and dispatches to typed handlers - Routing: All side effects flow through
CommandContextProvider, withChatViewModelas the sole implementer - Async safety: Commands like
/pingcapturecurrentCommandDestination()before launching asynchronous mesh operations - UI parity:
CommandInfoenum 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →