mod collector;
mod graph;
mod i18n;
mod loopback;
mod stats;
mod ui;
use std::io;
use std::time::{Duration, Instant};
use clap::{CommandFactory, FromArgMatches, Parser};
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use i18n::{Lang, t, set_lang};
use collector::{Collector, DeviceInfo};
use loopback::{LoopbackCounters, LoopbackMode};
use stats::StatisticsEngine;
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
pub enum Unit {
Bit,
Byte,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
pub enum BarStyle {
Fill,
Color,
Plain,
}
pub fn parse_max_value(s: &str) -> Result<f64, String> {
let s = s.trim();
if s.is_empty() {
return Err("empty value".to_string());
}
let (num_str, multiplier) = if let Some(n) = s.strip_suffix('G').or_else(|| s.strip_suffix('g')) {
(n, 1024.0 * 1024.0 * 1024.0)
} else if let Some(n) = s.strip_suffix('M').or_else(|| s.strip_suffix('m')) {
(n, 1024.0 * 1024.0)
} else if let Some(n) = s.strip_suffix('K').or_else(|| s.strip_suffix('k')) {
(n, 1024.0)
} else {
(s, 1.0)
};
let num: f64 = num_str.parse().map_err(|e| format!("invalid number: {e}"))?;
Ok(num * multiplier)
}
pub fn parse_hex_color(s: &str) -> Result<ratatui::style::Color, String> {
let hex = s.trim().strip_prefix("0x").or_else(|| s.trim().strip_prefix("0X")).unwrap_or(s.trim());
if hex.len() != 6 {
return Err(format!("expected 6 hex digits (e.g. 0x3399ff), got: {s}"));
}
let r = u8::from_str_radix(&hex[0..2], 16).map_err(|e| format!("bad red: {e}"))?;
let g = u8::from_str_radix(&hex[2..4], 16).map_err(|e| format!("bad green: {e}"))?;
let b = u8::from_str_radix(&hex[4..6], 16).map_err(|e| format!("bad blue: {e}"))?;
Ok(ratatui::style::Color::Rgb(r, g, b))
}
#[derive(Parser)]
#[command(name = "winload", version = concat!(env!("CARGO_PKG_VERSION"), " (Rust edition)"))]
struct Args {
#[arg(short = 't', long = "interval", default_value = "500")]
interval: u64,
#[arg(short = 'a', long = "average", default_value = "300")]
average: u64,
#[arg(short = 'd', long = "device")]
device: Option<String>,
#[arg(long = "debug-info")]
debug_info: bool,
#[arg(short = 'e', long = "emoji")]
emoji: bool,
#[arg(short = 'U', long = "unicode")]
unicode: bool,
#[arg(short = 'u', long = "unit", value_enum, default_value = "bit")]
unit: Unit,
#[arg(short = 'b', long = "bar-style", value_enum, default_value = "fill")]
bar_style: BarStyle,
#[arg(long = "in-color", value_parser = parse_hex_color)]
in_color: Option<ratatui::style::Color>,
#[arg(long = "out-color", value_parser = parse_hex_color)]
out_color: Option<ratatui::style::Color>,
#[arg(short = 'm', long = "max", value_parser = parse_max_value, conflicts_with = "smart_max")]
max: Option<f64>,
#[arg(short = 'n', long = "no-graph")]
no_graph: bool,
#[arg(long = "hide-separator")]
hide_separator: bool,
#[arg(long = "no-color")]
no_color: bool,
#[arg(long = "npcap")]
npcap: bool,
#[arg(long = "smart-max", default_missing_value = "10", num_args = 0..=1, value_name = "SECS")]
smart_max: Option<f64>,
#[arg(long = "lang", value_enum, default_value = "en-us")]
lang: Lang,
}
pub struct DeviceView {
pub info: DeviceInfo,
pub engine: StatisticsEngine,
}
pub struct App {
pub views: Vec<DeviceView>,
pub current_idx: usize,
pub emoji: bool,
pub unicode: bool,
pub unit: Unit,
pub bar_style: BarStyle,
pub in_color: ratatui::style::Color,
pub out_color: ratatui::style::Color,
pub fixed_max: Option<f64>,
pub no_graph: bool,
pub hide_separator: bool,
pub no_color: bool,
pub smart_max_half_life: Option<f64>,
pub loopback_mode: LoopbackMode,
pub loopback_info: Option<String>,
loopback_counters: Option<LoopbackCounters>,
collector: Collector,
}
impl App {
fn new(args: &Args) -> Self {
let collector = Collector::new();
let devices = collector.devices();
let views: Vec<DeviceView> = devices
.into_iter()
.map(|info| DeviceView {
info,
engine: StatisticsEngine::new(args.interval, args.average, args.smart_max),
})
.collect();
let mut current_idx = 0;
if let Some(ref name) = args.device {
let lower = name.to_lowercase();
if let Some(idx) = views
.iter()
.position(|v| v.info.name.to_lowercase().contains(&lower))
{
current_idx = idx;
}
}
let loopback_mode = if args.npcap {
LoopbackMode::Npcap
} else {
LoopbackMode::None
};
Self {
views,
current_idx,
emoji: args.emoji,
unicode: args.unicode,
unit: args.unit,
bar_style: args.bar_style,
in_color: args.in_color.unwrap_or(ratatui::style::Color::Rgb(0x00, 0xd7, 0xff)),
out_color: args.out_color.unwrap_or(ratatui::style::Color::Rgb(0xff, 0xaf, 0x00)),
fixed_max: args.max,
no_graph: args.no_graph,
hide_separator: args.hide_separator,
no_color: args.no_color,
smart_max_half_life: args.smart_max,
loopback_mode,
loopback_info: None,
loopback_counters: None,
collector,
}
}
pub fn current_view(&self) -> Option<&DeviceView> {
self.views.get(self.current_idx)
}
fn update(&mut self) {
let mut snapshots = self.collector.collect();
if let Some(ref counters) = self.loopback_counters {
let elapsed = self.collector.elapsed_secs();
for (name, snap) in snapshots.iter_mut() {
if name.to_lowercase().contains("loopback") {
snap.bytes_recv = counters.get_recv();
snap.bytes_sent = counters.get_sent();
snap.elapsed_secs = elapsed;
}
}
}
for view in &mut self.views {
if let Some(snap) = snapshots.get(&view.info.name) {
view.engine.update(snap.clone());
}
}
}
fn next_device(&mut self) {
if !self.views.is_empty() {
self.current_idx = (self.current_idx + 1) % self.views.len();
}
}
fn prev_device(&mut self) {
if !self.views.is_empty() {
self.current_idx = (self.current_idx + self.views.len() - 1) % self.views.len();
}
}
}
fn run(terminal: &mut ratatui::DefaultTerminal, args: Args) -> io::Result<()> {
let mut app = App::new(&args);
if app.loopback_mode != LoopbackMode::None {
let counters = LoopbackCounters::new();
let result = match app.loopback_mode {
LoopbackMode::Npcap => loopback::platform::start_npcap(counters.clone()),
LoopbackMode::None => unreachable!(),
};
match result {
Ok(info_msg) => {
app.loopback_info = Some(info_msg);
app.loopback_counters = Some(counters);
}
Err(e) => {
ratatui::restore();
eprintln!("Error: Failed to start loopback capture:\n{e}");
std::process::exit(1);
}
}
}
let tick_rate = Duration::from_millis(args.interval);
let mut last_tick = Instant::now();
app.update();
loop {
terminal.draw(|frame| ui::draw(frame, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_default();
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => {
return Ok(());
}
KeyCode::Char('c')
if key.modifiers.contains(KeyModifiers::CONTROL) =>
{
return Ok(());
}
KeyCode::Char('=') => {
app.hide_separator = !app.hide_separator;
}
KeyCode::Char('c') => {
app.no_color = !app.no_color;
}
KeyCode::Right | KeyCode::Down | KeyCode::Tab | KeyCode::Enter => {
app.next_device();
}
KeyCode::Left | KeyCode::Up => {
app.prev_device();
}
_ => {}
}
}
}
}
if last_tick.elapsed() >= tick_rate {
app.update();
last_tick = Instant::now();
}
}
}
fn print_system_info() {
eprintln!("\nSystem: {} | Arch: {} | Target: {}",
std::env::consts::OS,
std::env::consts::ARCH,
env!("TARGET"));
}
fn pre_scan_lang() -> Lang {
let args: Vec<String> = std::env::args().collect();
for i in 0..args.len() {
if args[i] == "--lang" {
if let Some(val) = args.get(i + 1) {
return match val.as_str() {
"zh-cn" => Lang::ZhCn,
"zh-tw" => Lang::ZhTw,
_ => Lang::EnUs,
};
}
}
if let Some(rest) = args[i].strip_prefix("--lang=") {
return match rest {
"zh-cn" => Lang::ZhCn,
"zh-tw" => Lang::ZhTw,
_ => Lang::EnUs,
};
}
}
Lang::EnUs
}
fn build_translated_command() -> clap::Command {
let after_help = format!("\nSystem: {} | Arch: {} | Target: {}",
std::env::consts::OS,
std::env::consts::ARCH,
env!("TARGET"));
Args::command()
.about(t("description"))
.after_help(after_help)
.mut_arg("interval", |a| a.help(t("help_interval")))
.mut_arg("average", |a| a.help(t("help_average")))
.mut_arg("device", |a| a.help(t("help_device")))
.mut_arg("debug_info", |a| a.help(t("help_debug_info")))
.mut_arg("emoji", |a| a.help(t("help_emoji")))
.mut_arg("unicode", |a| a.help(t("help_unicode")))
.mut_arg("unit", |a| a.help(t("help_unit")))
.mut_arg("bar_style", |a| a.help(t("help_bar_style")))
.mut_arg("in_color", |a| a.help(t("help_in_color")))
.mut_arg("out_color", |a| a.help(t("help_out_color")))
.mut_arg("max", |a| a.help(t("help_max")))
.mut_arg("no_graph", |a| a.help(t("help_no_graph")))
.mut_arg("hide_separator", |a| a.help(t("help_hide_separator")))
.mut_arg("no_color", |a| a.help(t("help_no_color")))
.mut_arg("npcap", |a| a.help(t("help_npcap")))
.mut_arg("smart_max", |a| a.help(t("help_smart_max")))
.mut_arg("lang", |a| a.help(t("help_lang")))
}
fn main() -> io::Result<()> {
let lang = pre_scan_lang();
set_lang(lang);
{
let raw_args: Vec<String> = std::env::args().collect();
if raw_args.iter().any(|a| a == "--version" || a == "-V") {
println!("\x1b[1mwinload {} (Rust edition)\x1b[0m", env!("CARGO_PKG_VERSION"));
println!("System: {} | Arch: {} | Target: {}",
std::env::consts::OS,
std::env::consts::ARCH,
env!("TARGET"));
return Ok(());
}
}
let cmd = build_translated_command();
let matches = cmd.get_matches();
let args = Args::from_arg_matches(&matches)
.unwrap_or_else(|e| e.exit());
if args.debug_info {
let collector = Collector::new();
if args.emoji {
println!("\n🔍🌐 Network Interfaces Debug Info 🖧✨");
}
collector.print_debug_info();
if args.emoji {
println!("🏁 Done! Happy debugging! 🎉🐛");
}
return Ok(());
}
let mut terminal = ratatui::init();
let result = run(&mut terminal, args);
ratatui::restore();
print_system_info();
result
}