A multiplexer for live chat. chatmux reads Twitch, YouTube and Kick
into your terminal and puts them in one view — side by side in a full-screen TUI,
or as plain lines on stdout when you want to pipe it somewhere.
- Three platforms, one view — Twitch, YouTube and Kick side by side, or merged into a single column.
- No accounts, no tokens for Twitch and Kick. YouTube needs a free API key.
- Survives bad networks — every source reconnects on its own with exponential backoff and jitter; one dead stream never takes the others with it.
- Filters and highlights — hide bot commands and known bots, highlight your own name or anything matching a regexp.
- Optional logging to dated text or JSONL files, so you can grep or
jqthe chat afterwards. - Pipe-friendly —
-plainwrites one line per message to stdout and keeps status noise on stderr.
go install github.com/andreas-bylund/chatmux@latestgit clone https://github.com/andreas-bylund/chatmux.git
cd chatmux
make build # produces ./chatmuxRequires Go 1.25 or newer.
Grab an archive for your platform from the releases page.
Each argument is either a source in the form <platform>:<target>, or the name of
a preset from the config file.
chatmux twitch:xqc
chatmux kick:trainwreckstv
chatmux youtube:dQw4w9WgXcQ
chatmux twitch:xqc kick:trainwreckstv youtube:dQw4w9WgXcQ
chatmux evening # a preset from the config fileu merges every source into one time-ordered column, which is the readable
option once you are following more than two or three channels. Each line keeps
its platform badge, and a source that drops says so in the stream rather than
just going quiet:
? shows this list without leaving the interface:
| Key | Action |
|---|---|
q / ctrl+c |
quit |
↑ ↓ / k j |
scroll one line |
pgup / pgdn |
scroll one page |
g / G |
jump to top / bottom |
tab / shift+tab |
change the focused pane |
u |
toggle between split and unified view |
t |
show/hide timestamps |
c |
show/hide the platform tag |
f |
turn the hide-filters on/off |
? |
help |
Scrolling up pauses autoscroll (the pane header shows ↑paused) so new messages
do not yank the text away from under you. G resumes.
| Flag | Action |
|---|---|
-config <path> |
use a different config file |
-init-config |
write a commented starter config and exit |
-list-presets |
list the configured presets |
-plain |
plain lines on stdout instead of the TUI |
-unified |
merge all sources into one column |
-log / -no-log |
force logging on/off for this run |
-version |
print the version |
-plain writes one line per message to stdout and status changes to stderr, so a
redirected stdout contains chat and nothing else:
chatmux -plain twitch:xqc | grep -i my-name
chatmux -plain twitch:xqc > chat.txtPlain mode turns on automatically when stdout is not a terminal — a TUI cannot be drawn there anyway.
chatmux -init-config # writes ~/.config/chatmux/config.tomlThe generated file is commented. In short:
[ui]
mode = "split" # "split" = one column per source, "unified" = all in one
timestamps = true
scrollback = 2000 # messages kept per pane
compact = false # true hides the per-line platform tag
[log]
enabled = false
dir = "~/.local/share/chatmux"
format = "text" # "text", "jsonl" or "both"
split_by_source = false
[filter]
hide_commands = true # hides anything starting with "!"
hide_users = ["nightbot", "streamelements"]
hide_patterns = [] # regexps matched against the text
min_length = 0
[highlight]
words = ["andreas"] # case-insensitive substrings
users = []
patterns = [] # regexps, for when a substring is too blunt
[retry]
initial = "2s"
max = "60s"
factor = 2.0
reset_after = "2m" # a connection that held this long resets the backoff
max_attempts = 0 # 0 = never give up
[presets]
evening = ["twitch:xqc", "kick:trainwreckstv"]The config file lives in $XDG_CONFIG_HOME/chatmux/config.toml, falling back
to ~/.config/chatmux/config.toml. Unknown keys produce an error that points
at the typo instead of being silently ignored.
Hidden messages reach neither the screen nor the log — the log matches what you
saw. f in the TUI turns the hide-filters off, but only going forward: messages
already filtered out are gone. Highlights are unaffected by f; they are a
marking, not a filter.
With log.enabled = true the chat is written to date-stamped files
(2026-08-09.log / .jsonl) that roll over at midnight and are appended to
between runs. JSONL lines are chat.Message verbatim, so they go straight into
jq:
jq -r 'select(.platform=="twitch") | "\(.user): \(.text)"' ~/.local/share/chatmux/2026-08-09.jsonlAnonymous IRC over TLS (irc.chat.twitch.tv:6697). No token required. The client
pings every 60 seconds and treats 150 seconds of silence as a dead connection, so
dropped links are noticed instead of hanging quietly.
chatmux twitch:<channel>YouTube Data API v3. Requires a free API key:
- Go to the Google Cloud Console
- Create a project and enable YouTube Data API v3
- Create an API key under Credentials
export YOUTUBE_API_KEY="your-key"
The target can be a video ID or a URL (watch?v=, youtu.be/, /live/,
/embed/):
chatmux youtube:dQw4w9WgXcQ
chatmux "youtube:https://youtu.be/dQw4w9WgXcQ"Each poll costs 5 quota units; the default allowance of 10,000/day is roughly 2-3 hours of continuous reading. Exhausted quota, an invalid key and disabled chat are reported as permanent errors and not retried. If the stream has not started yet the client keeps trying, so you can start it in advance.
Pusher WebSocket, the same one the web client uses. No auth.
chatmux kick:<channel-slug>Unofficial — Kick publishes no chat API, so the app key and event names are lifted from the web client and can stop working without notice.
Every source is supervised separately. A dropped connection is retried with exponential backoff (2s → 60s) plus ±20% jitter, so several sources lost to the same network blip do not come back in lockstep. A connection that held for two minutes counts as healthy and resets the backoff.
Errors that retrying cannot fix — a missing API key, a suspended channel, exhausted quota — are marked permanent and stop that source instead of looping. The other sources carry on.
Connection trouble shows up both in the pane header (● connected, ◌ retry 8s,
✕ failed) and as a line in the stream, so afterwards you can see where in the
chat the gap was.
chat/ Message, Sink, Source + the supervisor with backoff
twitch/ IRC client (irc.go = IRCv3 parser)
youtube/ Data API v3 polling
kick/ Pusher WebSocket
config/ TOML config and presets
filter/ hide and highlight rules
recorder/ file logging
ui/ Bubble Tea TUI (panes, scrollback, status bar)
main.go CLI and wiring
plain.go stdout mode
Add a platform by implementing chat.Source (Run, Label, Platform) and
registering it in parseSource in main.go. See
CONTRIBUTING.md for the details.
make test # go test ./...
make race # go test -race ./...
make check # vet + tests
make lint # golangci-lint (install it first)coder/websocket— Kick's WebSocketcharmbracelet/bubbletea+lipgloss+x/ansi— the TUIBurntSushi/toml— configmattn/go-isatty— TTY detection
Twitch and YouTube use only the standard library.
Bug reports, ideas and pull requests are welcome. Start with CONTRIBUTING.md; participants are expected to follow the Code of Conduct.
MIT © Andreas Bylund
Not affiliated with, endorsed by, or sponsored by Twitch, YouTube or Kick.


