(API Docs)
Termionix is an Ansi Enabled Telnet Library for Tokio.
- RFC 854 Compliant Telnet Protocol - Full implementation of the Telnet protocol
- ANSI Escape Sequence Handling - Parse and generate ANSI codes for terminal control
- MUD Protocol Extensions - Support for GMCP, MSDP, MSSP, MCCP, NAWS, and more
- Async-First Design - Built on Tokio for high-performance async I/O
- Split Read/Write Architecture - Independent read and write operations prevent blocking
- Configurable Flush Strategies - Optimize for latency or throughput
- Connection Metadata - Type-safe storage for per-connection data
- Observability - Integrated tracing and metrics support
- Ergonomic API - Easy-to-use high-level abstractions
Add Termionix to your Cargo.toml:
[dependencies]
termionix-service = "0.1"
termionix-terminal = "0.1"
tokio = { version = "1", features = ["full"] }Create a simple telnet server:
use std::sync::Arc;
use termionix_server::{
ConnectionManager, TelnetConnection, TelnetHandler,
TelnetServer, TelnetServerConfig,
};
use termionix_terminal::{TerminalCommand, TerminalEvent};
struct MyHandler;
#[async_trait::async_trait]
impl TelnetHandler for MyHandler {
async fn on_connect(&self, conn: &TelnetConnection) {
conn.send("Welcome to my server!\r\n").await.ok();
}
async fn on_data(&self, conn: &TelnetConnection, data: &str) {
// Echo back to client
conn.send(&format!("You said: {}\r\n", data)).await.ok();
}
async fn on_event(&self, conn: &TelnetConnection, event: TerminalEvent) {
match event {
TerminalEvent::WindowSize { width, height } => {
conn.send(&format!("Window: {}x{}\r\n", width, height)).await.ok();
}
_ => {}
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = TelnetServerConfig {
address: "127.0.0.1:4000".parse()?,
..Default::default()
};
let manager = Arc::new(ConnectionManager::new());
let handler = Arc::new(MyHandler);
let server = TelnetServer::new(config, handler, manager);
server.run().await?;
Ok(())
}Store typed data per connection:
#[derive(Clone)]
struct PlayerData {
name: String,
level: u32,
}
// Store data
let player = PlayerData { name: "Alice".to_string(), level: 5 };
conn.set_data("player", player);
// Retrieve data
if let Some(player) = conn.get_data::<PlayerData>("player") {
println!("Player: {} (Level {})", player.name, player.level);
}
// Check existence
if conn.has_data("player") {
// ...
}
// Remove data
conn.remove_data("player");Query telnet option negotiation state:
// Get window size (NAWS)
if let Some((width, height)) = conn.window_size().await {
println!("Terminal size: {}x{}", width, height);
}
// Get terminal type
if let Some(term_type) = conn.terminal_type().await {
println!("Terminal: {}", term_type);
}
// Check if option is enabled
use termionix_telnetcodec::TelnetOption;
if conn.is_option_enabled(TelnetOption::Echo).await {
println!("Echo is enabled");
}Send messages to multiple connections:
// Broadcast to all connections
manager.broadcast("Server announcement\r\n").await;
// Broadcast except specific connections
manager.broadcast_except("Player joined\r\n", &[conn.id()]).await;
// Broadcast with custom filter
manager.broadcast_filtered("Room message\r\n", |conn| {
// Only send to connections in the same room
conn.get_data::<RoomData>("room")
.map(|r| r.id == target_room_id)
.unwrap_or(false)
}).await;Enable structured logging:
use tracing_subscriber;
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();Run with: RUST_LOG=debug cargo run
Termionix automatically tracks:
- Connection counts (total, active)
- Message throughput (sent, received)
- Character counts
- Operation latency
- Error rates
Integrate with your metrics backend:
use metrics_exporter_prometheus::PrometheusBuilder;
PrometheusBuilder::new()
.install()
.expect("failed to install Prometheus recorder");Termionix uses a split read/write architecture that solves the blocking issue where buffered writes would wait for read timeouts:
use termionix_server::{SplitConnection, FlushStrategy};
// Create connection with independent read/write workers
let conn = SplitConnection::from_stream(stream, codec_read, codec_write);
// Configure flush strategy
conn.set_flush_strategy(FlushStrategy::OnNewline).await;
// Send data - doesn't block on reads!
conn.send("Hello\n", false).await?;
// Receive data - doesn't block on writes!
if let Some(event) = conn.next().await? {
println!("Received: {:?}", event);
}Benefits:
- ✅ Reads never block writes
- ✅ Writes never block reads
- ✅ Configurable flush strategies (Manual, Immediate, OnNewline, OnThreshold)
- ✅ Independent background workers
- ✅ Zero mutex contention
See examples/split_connection_demo.rs for a complete demonstration.
See the examples/ directory for complete examples:
simple_server.rs- Basic telnet echo serverecho_server.rs- Echo server with connection managementsplit_connection_demo.rs- NEW: Demonstrates split read/write architectureadvanced_features.rs- Demonstrates all advanced featuresansi_demo.rs- ANSI escape sequence handling
Run an example:
cargo run --example split_connection_demoThen connect with a telnet client:
telnet localhost 4000.github- GitHub Actions Workflows and Issue Templatesansicodec- ANSI String Handling Librarytelnetcodec- Telnet Framed Codec for Tokioterminal- ANSI Enabled Telnet Terminalservice- NEW: Unified connection layer with split read/write architectureserver- High-Level Telnet Server Frameworkclient- High-Level Telnet Client Frameworkcompress- MCCP Compression Supportdoc- Documentation, Specifications, and RFCsexamples- Usage Examples
- API Documentation
- CHANGELOG - Version history and changes
- Connection Architecture Analysis - Split read/write architecture details
- Service Crate README - Unified connection layer documentation
- Fix the myriad of Doctests
- Add Support for GMCP as described here
- Add Support for Compression and MCCP2 as described here
- Add support for MMCP as described here
- Add support for MNES as described here
- Add support for MTTS as described here
- Add support for MSLP as described here
- Add Support for MSP as described here
This project is licensed under Apache License, Version 2.0.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions.