ts-cli is an experimental TeamSpeak client written in C++20, targeting Linux natively, Windows via MinGW-w64 cross-compilation, and Windows natively via MSYS2's UCRT64 environment. Keep changes small, testable, and focused on the client features the project actually intends to support.
Full environment setup for all three targets (Linux native, Windows cross-compiled via MinGW-w64, Windows native via MSYS2) is documented in BUILDING.md.
Before submitting changes, run:
make linuxand, if you touched anything below the CLI layer (net, protocol, log, audio), also cross-compile and test the Windows target:
make windows-crossChanges should compile cleanly and keep all tests passing on both targets. Protocol changes should include focused regression tests where practical.
Releases are cut with make release, which requires a clean working tree and only plain git — no GitHub-specific tooling. It builds linux and windows-cross (both format, build, and run their full test suite first — nothing untested gets released), then:
- shows the current version (read from the
VERSIONfile at the repo root) and prompts for the new one — it never bumps the version automatically, and rejects anything that isn't a validMAJOR.MINOR.PATCHorMAJOR.MINOR.PATCH-prereleasesemantic version (or is identical to the current one; a version that doesn't look like an increase gets a warning, not a hard block); - packages the two binaries as
ts-cli-v<version>-linux-x86_64andts-cli-v<version>-windows-x86_64.exeintobuild/release/; - asks for a final confirmation, showing exactly what it's about to do;
- on confirmation: commits the
VERSIONbump, creates an annotatedv<version>tag, and pushes both toorigin.
make release stops there — it does not create anything on GitHub itself. A maintainer creates the actual GitHub release from the pushed tag by hand and attaches the two binaries left behind in build/release/. Answering anything other than y at the confirmation prompt aborts before anything is committed, tagged, or pushed.
Keep responsibilities separated:
client/— CLI, configuration, application runtime and later UI/audio coordinationlog/— logging and sinksnet/— networking primitivesprotocol/— TeamSpeak wire protocol, crypto, transport, typed messages and protocol state
The protocol layer must not read configuration files, inspect XDG paths, or depend on terminal/UI behavior.
Prefer typed messages and state objects over raw command strings in higher layers. Avoid generic Utils classes and large procedural grab-bags.
ts-cli targets Linux natively and Windows via MinGW-w64 cross-compilation (see BUILDING.md), so public headers must not leak OS-specific types (sockaddr_storage, pollfd, file descriptors, HANDLE, ...). net, log, and audio follow the same shape for this:
- a public, platform-agnostic interface (
net::UdpSocket,log::TerminalColor,audio::AudioEngine) that the rest of the codebase links against; - an abstract backend interface (
net::SocketBackend,audio::AudioBackend) or a set of free functions, plus aCreate*Backend()factory declared next to it where a stateful backend is needed; - one concrete backend implementation per build target, kept in its own source subdirectory (
net/src/posix/+net/src/win32/,log/src/posix/+log/src/win32/,audio/src/posix/pipewire_backend.cpp+audio/src/win32/wasapi_backend.cpp) and selected byCMakeLists.txtbased on the target platform — never by#ifdefscattered through shared source.
client/platform/console_io.hpp and client/platform/terminal_control.hpp follow the same pattern for interactive-terminal/stdin handling and cursor control (client/src/platform/posix/, client/src/platform/win32/), since those are also inherently OS-specific. audio ships a real backend on both targets now (PipeWire/Opus on Linux, WASAPI/Opus on Windows); Opus and RNNoise (audio/src/opus_codec.cpp, audio/src/rnnoise_filter.cpp) are platform-agnostic and stay outside the posix//win32/ split entirely, wired in once regardless of which backend gets picked.
When adding a backend for a target that doesn't have one yet, this is the checklist: implement the missing backend against the existing interface headers, add the corresponding CMakeLists.txt branch, and leave every consumer above the backend layer untouched. If a new primitive needs OS-specific types at all, give it the same treatment rather than including platform headers from a public header.
- C++20
- Traditional include guards; do not use
#pragma once - Lowercase snake_case filenames
- PascalCase types and functions
- Private fields use
m_followed by PascalCase, for examplem_ClientId - Do not write
this->m_Fieldunless required - Opening braces stay on the same line
- Prefer
std::endlfor normal terminal/log output in this project
Namespaces follow the component structure, for example:
namespace ts::net {
}
namespace ts::protocol {
}In header files:
- Use quotes only for project headers in the same directory
- Use angle brackets for project headers from other directories
- Use angle brackets for standard-library and external headers
Example:
#include "bootstrap.hpp"
#include <protocol/crypto/session_crypto.hpp>
#include <cstdint>
#include <string>In .cpp files, use angle brackets for project headers:
#include <protocol/session/session.hpp>Use [[nodiscard]] for values whose result should normally be consumed:
- parsers
- factories
- serializers
- getters
- computations
- data-returning functions
Do not add it to actions or lifecycle operations such as:
Send();
Connect();
Run();An action does not become [[nodiscard]] merely because it returns an ID.
Prefer evidence in this order:
- behavior observed from real TeamSpeak clients/servers
- established protocol implementations and documentation
- inference only when necessary
Do not weaken crypto or transport validation just to make a higher-level feature work. If a protocol assumption fails against a real server, capture the actual message/packet behavior and fix the correct layer.
Keep protocol changes narrow. Avoid combining unrelated refactors with new wire behavior.
One network I/O thread should eventually own all TeamSpeak protocol state, including:
- packet sequences and generations
- ACK/retransmission state
- crypto
- fragmentation and compression
- ping/pong and timers
- session state mutations
Do not process packets concurrently on separate worker threads. The UI should communicate with the network side through queued actions/events. Audio will live on its own thread later.
Treat the server as the source of truth. For actions such as moving channels, send the command and let the resulting server notification update local state rather than mutating state optimistically.
Preserve unknown or not-yet-supported notifications when possible instead of silently dropping information required by later features.
The goal is a useful terminal TeamSpeak client, not complete implementation of every TeamSpeak feature. Current priorities are navigation, text chat, interactive runtime behavior and voice/audio.
Administrative tooling, exhaustive permission management and unrelated protocol surface should only be added when there is a concrete client need.
Contributions are made under the same license as the project: GPL-3.0-or-later.