tincan-cli README

The project's README from github.com/bilalyazicioglu/tincan-cli. As one markdown file: README.md.

tincan logo

tincan

Serverless peer-to-peer voice and text chat for your terminal.

Website Build Status crates.io License Wiki Developer Blog (EN) Developer Blog (TR)

Built With Ratatui

Website · Install · Usage · Shortcuts · How it works · Security · Design notes · Blog (EN) · Blog (TR)

tincan does what Discord does, without needing anyone's server: the first person to open the app creates the room, sends the invite code it prints to their friends, and they connect with that code from anywhere in the world. No VPN, no port forwarding, no accounts.

tincan terminal demo: Screen Studio style animated overview showing real-time audio meters, travelling pulse latency, chat messaging, and audio settings transition.

The palette is metal: a brown tin ground, brass for the string and the invite code, and the two greens copper actually turns — bright patina for what is live, deeper verdigris for a link that is holding.

The line down the middle is the connection. It runs taut when everyone is reached directly, sags into a dashed line when someone is coming through a relay, and frays when audio starts dropping. While anyone is talking a pulse travels down it, at the speed of the round trip — so a slow link is something you see rather than read. The meters beside each name move with that person's voice.

Before anyone has joined, the chat pane draws the same thing full size: your can, the string, their can, with the latency written on it and your invite code underneath. It stays up there while the conversation is short enough to leave the room for it, so a full-screen terminal is never a wall of nothing.

Everything underneath that — the audio screen and its noise floor, turning one person down without deafening the room, and the sounds the interface makes — is written up in the interface design notes, with the background story in the developer blog (English / Türkçe).

Why tincan

Discord runs on Discord's servers, and TeamSpeak or Mumble need someone to run one. tincan needs neither: whoever opens a room hosts it, and the invite code is its address. There is no account to make, nothing to rent and nothing to keep running.

That is not the same as having no infrastructure. To find each other and get through routers, peers use public relays and a lookup service run by Number Zero, and a network that blocks those can stop new connections. How it works says what they do and what they can see, and Known limits says what happens without them.

Install

Homebrew (macOS & Linux):

brew tap bilalyazicioglu/tap && brew install tincan

NPM:

# Run directly without installing:
npx tincan-cli host

# Or install globally:
npm install -g tincan-cli

Shell script (macOS & Linux):

curl -fsSL https://raw.githubusercontent.com/bilalyazicioglu/tincan-cli/main/install.sh | sh

This downloads a prebuilt binary for your platform into ~/.local/bin and verifies its checksum. Anywhere without one it falls back to building from source. If you would rather read the script before running it — always a reasonable instinct with curl | sh — it lives at install.sh in this repo.

Windows: use npx tincan-cli host, or download tincan-x86_64-pc-windows-msvc.zip from the latest release and put tincan.exe somewhere on your PATH. The shell script above is POSIX and will not install it for you.

Prebuilt binaries are published for these five targets:

Platform Target
macOS, Apple Silicon aarch64-apple-darwin
macOS, Intel x86_64-apple-darwin
Linux, x86_64 x86_64-unknown-linux-gnu
Linux, arm64 aarch64-unknown-linux-gnu
Windows, x64 x86_64-pc-windows-msvc

A word on the last row: the Windows binary compiles and the test suite passes on Windows in CI, which is not the same as the application having been used there. The terminal UI, WASAPI device enumeration and the microphone permission prompt have not been exercised on a real Windows machine. If you try it, say how it went.

Cargo, if you have a Rust toolchain:

cargo install tincan-chat     # the command it installs is still `tincan`

The crate is tincan-chat because tincan and tincan-cli were both taken on crates.io by unrelated projects before this one existed. Only the published name differs; the binary, this repository and the npm package are all tincan.

From git, to build whatever is on main:

cargo install --git https://github.com/bilalyazicioglu/tincan-cli

That needs Rust 1.91+ and a way to get Opus. The easiest is to install it from your package manager — cargo picks it up through pkg-config and links it in:

brew install opus pkg-config                             # macOS
sudo apt install libopus-dev pkg-config libasound2-dev   # Debian/Ubuntu

Without a system Opus, the build compiles the vendored C source instead, which needs autotools (autoconf, automake, libtool). Either way it takes a few minutes.

However you install it, you need a microphone and a speaker; any sample rate will do, since tincan resamples to and from the 48 kHz Opus works at. On the first run your operating system will ask for microphone permission — on macOS the prompt comes from the terminal app running tincan (Terminal, iTerm, VS Code…), not from tincan itself.

Usage

Open a room:

tincan host lobby --name alice
room:        lobby
passphrase:  chestnut-ferry-lens-moss

The room's address is derived from its name and passphrase, so that is all your friends need — two things you can say down the phone or write on a whiteboard. The passphrase is generated for you: it is what stops strangers from guessing their way into the room, so bring your own with -p only if it is as strong (tincan warns when it is not).

Open a room behind an invite code instead:

tincan host --name alice --password secret

Without a room name tincan prints an invite code, puts it on your clipboard, and waits for you to press Enter before taking over the screen. Send it to your friends — copy and paste, 63 characters. The code is the host's public key, so this is the way in whose security does not rest on the passphrase.

Once the interface is up the footer only has room for the first group of the code. Press F1 to print the whole thing into the chat pane and copy it again, so you can invite someone without restarting the room.

Join a room:

tincan join lobby --name bob                            # asks for the passphrase
tincan join n73w-kuqc-uog2-... --name bob --password secret

join takes either a room name or an invite code. Joining by name without -p asks for the passphrase, which keeps it out of the process list. Case, spaces, dashes and underscores are forgiven in both room names and passphrases: Chestnut Ferry Lens Moss works.

A join makes one attempt. When the host may not be up yet — a script that starts both, or a host restarting under the same name — --retry keeps trying for that many seconds:

tincan join lobby --name bob --retry 60

See your audio devices:

tincan devices          # what the device picker offers
tincan devices --all    # on Linux, ALSA's plugins and each card's raw PCMs too

Shell auto-completions:

tincan completions zsh > ~/.zfunc/_tincan        # zsh
source <(tincan completions bash)               # bash
tincan completions fish > ~/.config/fish/completions/tincan.fish # fish

Options

Option Description
--name, -n Your nickname in the room (default: your system username)
ROOM host: room name (optional). join: room name or invite code
--password, -p Passphrase. Generated for a named room when left out
--channels Comma-separated channel list (default: general,gaming,music)
--no-voice Skip audio entirely; text chat only
--input Microphone to use (a distinctive part of the name is enough)
--output Speaker to use
--ptt Push-to-talk: the microphone only opens with F4
--retry join: keep trying to reach the room for this many seconds

Shortcuts

Key Action
Tab / Shift+Tab Move between channels
F2 (or Ctrl+G) Join / leave the voice of the channel you see
F3 (or Ctrl+T) Mute / unmute your microphone
F4 Push-to-talk (only in --ptt mode)
F5 Deafen: hear nobody (also closes your mic)
F6 (or Ctrl+,) Open audio settings
n (on that screen) Noise suppression on / off
F1 Show the full invite code (and copy it)
↑ / ↓ Pick someone out of the roster
← / → Turn that person down or up
Ctrl+K Silence selected person (or Vim scroll up)
Ctrl+J / Ctrl+K Vim scroll down / up (also Alt+J / Alt+K)
PageUp / PageDn Scroll chat history up or down (15 lines)
Shift+↑ / ↓ Scroll chat history smoothly (also Ctrl+↑/↓)
Scroll wheel / Trackpad Scroll chat history smoothly (3 lines)
End Jump to latest message
Esc Let go of the roster, or jump to latest chat
Enter Send the message
Ctrl+C Quit

To pick a device, list them with tincan devices first, then pass part of a name:

tincan join <code> --input "MacBook Pro Mic" --output "AirPods"

The channel you are looking at and the channel you are connected to by voice are independent: you can read the chat in "general" while talking in "gaming". In the channel list, > marks the one you are viewing and 🔊 the one you are in.

The audio shortcuts are F-keys on purpose: in a terminal Ctrl+M (0x0D) and Ctrl+J (0x0A) are Enter and cannot be told apart from it — had those been used, the "mute" key would have quietly sent a message.

The footer shows link status: how many peers you reach directly, how many flow through a relay, the worst latency, and whether you have had audio dropouts. When everything is fine it shows shortcut hints instead — technical detail only surfaces when there is a problem.

Environment

Set NO_COLOR for a colourless interface, TINCAN_ASCII=1 if your terminal has no box-drawing, TINCAN_NO_MOTION=1 to hold the string still, and TINCAN_THEME=light for the same palette on a light background.

How it works

Two planes, kept apart:

Control plane (star). Whoever opens the room is the coordinator: the roster, the channels and the chat flow through them. The traffic is tiny, a few hundred bytes per second.

Voice plane (mesh). Peers in the same channel connect directly to each other and send Opus packets as QUIC datagrams. Voice never passes through the coordinator — the host's connection is not a bottleneck, and a six-person room needs about 160 kbps of upload each.

        [alice: coordinator]
         /      |      \          ── control (reliable stream)
      bob     carol    dave
         \______|______/          ── voice (mesh, direct datagrams)

Connectivity comes from iroh: the invite code is the peer's public key. Most of the time a direct P2P connection is established; if hole punching fails, traffic flows through a relay — which cannot decrypt anything, it only forwards. QUIC encrypts every connection end to end and verifies the other side's identity by public key.

What tincan depends on

Serverless here means there is no tincan server: no account, no room registry, nothing this project runs, and no copy of your conversation anywhere but on the machines having it. It does not mean no infrastructure at all. tincan uses iroh's N0 preset, which brings in three services operated by Number Zero, the company behind iroh:

So, put plainly: no server holds your room and no server can hear it, but two people cannot currently find each other without n0's. If those services went away, new connections would stop working. Pointing tincan at a relay and a DNS server you run yourself is something iroh supports and tincan does not expose yet — that is issue #136.

What the microphone sends

Before anything leaves the machine, the frame goes through RNNoise — a small recurrent network trained to tell speech from everything else. It takes out what a noise gate cannot: a fan, an air conditioner, the rain, the person typing while they talk. The gate decides whether you are speaking; this decides what you sound like when you are.

It is on unless you turn it off, because a feature nobody finds is a feature nobody has. The switch is n on the audio settings screen (F6), and the choice is remembered.

The cost is honest and worth stating: RNNoise is an overlap-add design, so it hands back the frame before the one just given to it. That is 10 ms of added latency on the capture path, always, whether or not there is any noise to remove. Against tincan's 20 ms frames and three-frame jitter buffer it is a small share of the total, and it buys a call where the other person can hear you over your own keyboard. Turn it off and the 10 ms goes away along with the suppression.

Only your own microphone is cleaned. Voices arriving from other people are played as they were sent, so the work is one stream no matter how many people are in the room.

Security

The password never travels over the wire. Both sides stretch it once with Argon2id into an admission key; the coordinator sends a random nonce and the client returns a keyed BLAKE2b MAC of it. Because the nonce is fresh on every connection, a captured proof cannot be replayed, and the coordinator spends no Argon2 work on connection attempts.

The password is not for encryption but for admission control — QUIC already handles the encryption.

What a relay can see is worth being exact about. It cannot read anything: the QUIC session runs between the two peers and the relay holds no key to it. It does see the shape of the traffic — which two public keys are talking, when, and how much — which is more than nothing if that pattern is the part you were hoping to keep to yourself.

Your address is published, not only your key. For a public key to be enough to find you, iroh announces the endpoint's reachable addresses — its IP addresses and its relay URL — as a record signed under that key, and those records are public. Anyone holding the invite code can resolve it to an address, and so can anyone who derives the same key from a room name and passphrase. Peers then connect to each other directly whenever hole punching works, so everyone in a room learns everyone else's IP address.

This is the part of the two-cans-and-a-string picture that is true in a way you might not want: on a real string, the other end knows where you are. A service with servers in the middle can stand between you and hide it. tincan has no such middle, and that cuts both ways — nobody is keeping your conversation, and nobody is keeping your address out of it either.

A room opened by name goes further: its coordinator key is Argon2id(passphrase, room name), so the passphrase is the room's address as well as its lock. That is what makes the invite speakable, and it has costs, listed under Known limits.

--password is visible on the command line, so other users on the same machine can read it with ps. tincan join <room> without -p asks for the passphrase instead, and tincan host <room> without -p generates one.

Development

cargo test              # 93 tests: unit + control plane + voice mesh
cargo clippy --all-targets
RUST_LOG=tincan=debug cargo run -- host 2>tincan.log   # logs to a file, so they don't scramble the UI

The tests never touch the internet: the control-plane and voice-mesh tests use two real iroh endpoints over a real QUIC connection, but with relays and discovery disabled and addresses introduced by hand. The audio tests use no audio hardware either — they attach directly to the ends of the mesh.

Source layout

src/
  proto.rs        On-the-wire types (control messages + voice packet header)
  room.rs         The room's authoritative state — the coordinator's single source
                  of truth, pure and tested
  auth.rs         Admission (Argon2id-stretched key, MAC over a nonce) and the
                  identity of a room opened by name
  passphrase.rs   Generated four-word passphrases, weak-password warning
  invite.rs       The invite code: base32, grouped, tolerant of pasting
  net/
    endpoint.rs   iroh endpoint setup, identity conversions
    control.rs    Coordinator server + joining client
    voice.rs      Voice mesh: connection management, datagram transport, channel filter
  audio/
    device.rs     cpal ↔ lock-free ring buffer bridge
    codec.rs      Opus encode/decode + loss concealment
    jitter.rs     Per-peer jitter buffer
    mixer.rs      Multi-source mixing + limiter
    vad.rs        Voice activity detection (indicator + DTX)
    denoise.rs    RNNoise noise suppression on the capture path
  ui/
    state.rs      Interface state (independent of network and terminal, tested)
    view.rs       Screen layout
examples/         Phase 0 probes — throwaway measurement tools

examples/ping.rs measures connectivity and latency between two machines; examples/loopback.rs measures the audio chain. Both were written to validate design decisions and are not used in the product.

Known limits

Contributing

Bug reports and feature requests are welcome! Please use the issue templates when opening an issue, and review the pull request checklist before submitting a PR.

License

MIT — see LICENSE.

tincan-cli (bilalyazicioglu/tincan-cli)

Searching Database…

这是什么?tincan 是一个在终端里运行的点对点语音和文字聊天应用,无需注册账号、无需搭建中央服务器,就像一个小巧的 Discord 替代品。
这是需要我集成的库,还是直接运行的独立应用?它是用 ratatui 构建的独立终端界面(TUI)应用,不是库——安装二进制文件后直接在 shell 里运行即可。
怎么让它跑起来?在 macOS/Linux 上通过 Homebrew、npm 或提供的 shell 脚本安装(Windows 使用 `npx tincan-cli host` 或下载的 .exe),然后运行 host 命令开启房间,或用邀请码加入。
tincan 真的是无服务器、完全不需要基础设施吗?不完全是:tincan 自己不运营任何服务器,但节点之间仍要依赖 Number Zero(iroh 的开发者)运营的公共中继和 DNS 查询服务来互相发现并穿透 NAT;如果这些服务宕机,新连接将无法建立。
它实际能在哪些平台上运行?预编译二进制覆盖 macOS(Apple Silicon 和 Intel)和 Linux(x86_64 和 arm64);也有 Windows x64 构建,但只通过了 CI 验证,还没在真实 Windows 机器上测试过。
没有互联网连接也能用 tincan 吗?不能——节点需要联网才能访问 n0 的中继和 DNS 查询服务,把邀请码和房间名解析为可连接的地址。
中继服务器能看到或听到我的对话吗?不能——QUIC 对每个连接进行端到端加密,当流量必须经过中继时,中继也只是转发它无法解密的数据包。
Windows 支持现在就能直接用吗?Windows 二进制可以编译并通过 CI 测试,但界面、WASAPI 音频设备枚举和麦克风权限提示还没有在真实 Windows 机器上实际验证过。
噪声抑制功能是做什么的?RNNoise 滤波器只清理你自己的麦克风输入,滤除风扇声、空调嗡嗡声和键盘敲击声,代价是增加约 10ms 延迟;在音频设置界面按“n”键切换。
用邀请码加入和用房间名加入有什么不同?邀请码实际上就是房主的公钥,用它加入不依赖口令强度;而用房间名加入则需要口令,该口令经 Argon2id 拉伸,且从不在网络上传输。
如果我想加入时房主还没上线怎么办?给 --retry 传一个秒数,tincan 就会持续尝试连接房间,这对几乎同时启动房主和加入者的脚本很有用。
Size
22.4 MB
Stars
215
Age
2mo
Commits · 90 Days