From 4766b6aa9bae7f8b843d90dd45a1d17d815f9949 Mon Sep 17 00:00:00 2001 From: frapank Date: Sat, 13 Jun 2026 18:34:09 +0200 Subject: [PATCH 01/28] remove: cpp old files --- CMakeLists.txt | 21 ------ include/lagger/gui.hpp | 28 -------- include/lagger/utils.hpp | 12 ---- src/gui.cpp | 148 --------------------------------------- src/main.cpp | 32 --------- src/utils.cpp | 49 ------------- 6 files changed, 290 deletions(-) delete mode 100644 CMakeLists.txt delete mode 100644 include/lagger/gui.hpp delete mode 100644 include/lagger/utils.hpp delete mode 100644 src/gui.cpp delete mode 100644 src/main.cpp delete mode 100644 src/utils.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index ae22efc..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -cmake_minimum_required(VERSION 3.28) -project(lagger) - -set(CMAKE_CXX_STANDARD 23) -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - -add_executable(lagger) - -target_sources(lagger PRIVATE - ${CMAKE_SOURCE_DIR}/src/gui.cpp - ${CMAKE_SOURCE_DIR}/src/main.cpp - ${CMAKE_SOURCE_DIR}/src/utils.cpp -) - -target_include_directories(lagger PRIVATE - ${CMAKE_SOURCE_DIR}/src - ${CMAKE_SOURCE_DIR}/include - ${CMAKE_SOURCE_DIR}/include/lagger -) - - diff --git a/include/lagger/gui.hpp b/include/lagger/gui.hpp deleted file mode 100644 index 8f2e3b4..0000000 --- a/include/lagger/gui.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include -#include - -namespace color { -constexpr const char* red = "\033[38;2;255;105;97m"; -constexpr const char* green = "\033[38;2;119;221;119m"; -constexpr const char* yellow = "\033[38;2;253;253;150m"; -constexpr const char* blue = "\033[38;2;174;198;207m"; -constexpr const char* reset = "\033[0m"; -} // namespace color - -class Gui { - public: - void start(); - - private: - std::string getDelay(); - std::expected setupInterface(); - void removeDelay(); - void setDelay(); - void clearGui(); - void drawGui(); - - std::string interface_{}; - bool running = false; -}; diff --git a/include/lagger/utils.hpp b/include/lagger/utils.hpp deleted file mode 100644 index a5f579d..0000000 --- a/include/lagger/utils.hpp +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include -#include - -namespace utils { - -std::expected checkTcInstalled(); -std::expected executeCommand(const char* command); -bool checkIfSudo(); - -} // namespace utils diff --git a/src/gui.cpp b/src/gui.cpp deleted file mode 100644 index 7a067cd..0000000 --- a/src/gui.cpp +++ /dev/null @@ -1,148 +0,0 @@ -#include "lagger/gui.hpp" - -#include - -#include -#include -#include -#include -#include -#include - -#include "lagger/utils.hpp" - -void Gui::clearGui() { std::cout << "\033[2J\033[H" << std::flush; } - -void Gui::removeDelay() { - auto cmd = "sudo tc qdisc del dev " + interface_ + " root"; - auto res = utils::executeCommand(cmd.c_str()); - if (!res) { - clearGui(); - std::cerr << color::red << res.error() << color::reset << "\n"; - } -} - -std::string Gui::getDelay() { - auto cmd = - "tc qdisc show dev " + interface_ + " | grep -oP 'delay \\K[0-9.]+ms'"; - auto res = utils::executeCommand(cmd.c_str()); - if (!res || res->empty()) { - return std::string(color::red) + "No delay" + color::reset; - } - return res.value(); -} - -void Gui::setDelay() { - clearGui(); - - int time; - std::cout << "Delay: "; - std::cin >> time; - const auto cmd = "sudo tc qdisc add dev " + interface_ + - " root netem delay " + std::to_string(time) + "ms"; - - auto res = utils::executeCommand(cmd.c_str()); - if (!res) { - removeDelay(); - std::cerr << color::red << res.error() << color::reset << "\n"; - } else { - std::cout << color::yellow << "New delay set\n" << color::reset; - } -} - -std::expected Gui::setupInterface() { - clearGui(); - - FILE* pipe{popen("ip -o link show | awk -F': ' '{print $2}'", "r")}; - if (!pipe) { - return std::unexpected("Failed to list interfaces"); - } - - std::vector interfaces; - char buffer[256]; - while (fgets(buffer, sizeof(buffer), pipe)) { - std::string iface(buffer); - iface.erase(iface.find_last_not_of(" \n\r\t") + 1); - if (iface != "lo") { - interfaces.push_back(iface); - } - } - pclose(pipe); - - if (interfaces.empty()) { - return std::unexpected("No network interfaces foudn"); - } - - std::cout << color::yellow << "\nAvailable network interfaces:\n" - << color::reset; - for (size_t i = 0; i < interfaces.size(); ++i) { - std::cout << color::green << " " << i + 1 << ") " << interfaces[i] - << "\n" - << color::reset; - } - - int choice = 0; - while (true) { - std::cout << color::yellow << "\nChoose interface (1-" - << interfaces.size() << "): " << color::reset; - std::cin >> choice; - if (std::cin.fail() || choice < 1 || - (size_t)choice > interfaces.size()) { - std::cin.clear(); - std::cin.ignore(10000, '\n'); - std::cout << "Invalid choice. Try again.\n"; - } else - break; - } - - interface_ = interfaces[choice - 1]; - return {}; -} - -void Gui::start() { - if (auto check = setupInterface(); !check) { - throw std::runtime_error(check.error()); - return; - } - - running = true; - while (running) { - drawGui(); - } -} - -void Gui::drawGui() { - // Draw gui - clearGui(); - std::cout << color::yellow << "Lagger settings: \n \n" << color::reset; - std::cout << color::blue << " Current delay: " << color::reset << getDelay() - << "\n" - << color::reset; - std::cout << color::green << " 1) Set new delay \n" << color::reset; - std::cout << color::green << " 2) Remove current delay \n" << color::reset; - std::cout << color::red << " 3) Exit\n" << color::reset; - - // Select option - int option; - std::cout << "\n\n" << color::reset << "Input: " << color::reset; - if (!(std::cin >> option)) { - std::cin.clear(); - std::cin.ignore(1000, '\n'); - return; - } - - // Elaborat - switch (option) { - case 1: - setDelay(); - break; - case 2: - removeDelay(); - break; - case 3: - clearGui(); - running = false; - break; - } -} - diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index 194f11d..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include - -#include -#include - -#include "lagger/gui.hpp" -#include "lagger/utils.hpp" - -int main() { - if (!utils::checkIfSudo()) { - std::cerr << "The program should run as sudo\n"; - return EXIT_FAILURE; - } - - if (auto check = utils::checkTcInstalled(); !check) { - std::cerr << check.error() << "\n"; - return EXIT_FAILURE; - } - - try { - Gui gui; - gui.start(); - } catch (const std::runtime_error& e) { - std::cerr << color::red << "Critical Error: " << e.what() - << color::reset << "\n"; - return EXIT_FAILURE; - } catch (const std::exception& e) { - std::cerr << color::red << "Unexpected Error: " << e.what() - << color::reset << "\n"; - return EXIT_FAILURE; - } -} diff --git a/src/utils.cpp b/src/utils.cpp deleted file mode 100644 index 9317262..0000000 --- a/src/utils.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#include "lagger/utils.hpp" - -#include - -#include -#include -#include -#include - -namespace utils { - -std::expected checkTcInstalled() { - const std::vector paths = { - "/usr/bin/", "/usr/sbin/", "/bin/", - "/sbin/", "/usr/local/bin/", "/usr/local/sbin/"}; - - for (const auto& dir : paths) { - auto candidate = std::filesystem::path(dir) / "tc"; - if (std::filesystem::exists(candidate) && - std::filesystem::is_regular_file(candidate)) { - return {}; - } - } - return std::unexpected("iproute-tc not installed"); -} - -std::expected executeCommand(const char* command) { - auto pipeCloser = [](FILE* f) { - if (f) pclose(f); - }; - - std::unique_ptr pipe(popen(command, "r"), - pipeCloser); - - if (!pipe) { - return std::unexpected( - std::string{"Can't open pipe for: " + std::string(command) + '\n'}); - } - - std::string output; - char buffer[256]; - while (fgets(buffer, sizeof(buffer), pipe.get())) output += buffer; - - return output; -} - -bool checkIfSudo() { return getuid() == 0; } - -} // namespace utils From 90b9561d9ca74be50cf88269c4dcdab6d59eaa0a Mon Sep 17 00:00:00 2001 From: frapank Date: Sat, 13 Jun 2026 18:34:44 +0200 Subject: [PATCH 02/28] refactor: change .gitignore for Rust --- .gitignore | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 6ffd2d9..a76c814 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,37 @@ -.cache/ -/build/ +/target/ +**/target/ + +Cargo.lock + +*.rlib +*.rmeta +*.rmeta.d +*.d + +.vscode/ +.idea/ +*.iml + +.idea/ + +.DS_Store +Thumbs.db + +*~ + +*.prof +*.perf +flamegraph.svg + +*.log + +.env +.env.* + +coverage/ +tarpaulin-report.html + +*.wasm + +*.pid +*.tmp From 041981c3915e349f13233abd4b7b08994a9d53e0 Mon Sep 17 00:00:00 2001 From: frapank Date: Sat, 13 Jun 2026 18:36:16 +0200 Subject: [PATCH 03/28] add: setup base rust project --- Cargo.toml | 10 ++++++++++ src/gui/mod.rs | 0 src/main.rs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 Cargo.toml create mode 100644 src/gui/mod.rs create mode 100644 src/main.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..366109e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "MemMapExplorer" +version = "0.1.0" +edition = "2024" + +[dependencies] +crossterm = "0.29.0" +env_logger = "0.11.10" +log = "0.4.32" +ratatui = "0.30.1" diff --git a/src/gui/mod.rs b/src/gui/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e13bdbe --- /dev/null +++ b/src/main.rs @@ -0,0 +1,30 @@ +use crossterm::event; +use log::error; +use std::error::Error; +use std::path::Path; +use std::process::ExitCode; + +fn run() -> Result<(), Box> { + ratatui::run(|terminal| { + loop { + terminal.draw(|frame| frame.render_widget("Hello World!", frame.area()))?; + if event::read()?.is_key_press() { + break Ok::<(), Box>(()); + } + } + })?; + + Ok(()) +} + +fn main() -> ExitCode { + env_logger::init(); + + match run() { + Ok(()) => ExitCode::SUCCESS, + Err(e) => { + error!("{e}"); + ExitCode::FAILURE + } + } +} From 59cba285f3a26899ef0b9d2331c442e738980507 Mon Sep 17 00:00:00 2001 From: frapank Date: Sat, 13 Jun 2026 19:06:38 +0200 Subject: [PATCH 04/28] add: list_interfaces() utils function --- src/main.rs | 10 ++++++++-- src/utils.rs | 11 +++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 src/utils.rs diff --git a/src/main.rs b/src/main.rs index e13bdbe..f5128b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,19 @@ +mod utils; + +use utils::list_interfaces; use crossterm::event; use log::error; use std::error::Error; -use std::path::Path; use std::process::ExitCode; fn run() -> Result<(), Box> { + ratatui::run(|terminal| { + let list = list_interfaces().unwrap(); + for l in list { + terminal.draw(|frame| frame.render_widget(l, frame.area()))?; + } loop { - terminal.draw(|frame| frame.render_widget("Hello World!", frame.area()))?; if event::read()?.is_key_press() { break Ok::<(), Box>(()); } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..b8dbd4a --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,11 @@ +pub fn list_interfaces() -> std::io::Result> { + let mut ifaces: Vec = std::fs::read_dir("/sys/class/net/")? + .filter_map(|e| e.ok()) + .map(|e| e.file_name().to_string_lossy().into_owned()) + .filter(|name| name != "lo") + .collect(); + ifaces.sort(); + Ok(ifaces) +} + + From ad345f828828e94d3410a8fe20b4ccc2159922b8 Mon Sep 17 00:00:00 2001 From: frapank Date: Sun, 14 Jun 2026 21:21:35 +0200 Subject: [PATCH 05/28] add: base ratatui gui and basic interf struct --- src/main.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index f5128b5..a312dce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,40 @@ mod utils; -use utils::list_interfaces; -use crossterm::event; +use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use log::error; +use ratatui::layout::{Constraint, Layout}; +use ratatui::style::{Color, Stylize}; +use ratatui::widgets::{Block, Borders, Paragraph}; use std::error::Error; use std::process::ExitCode; +use utils::list_interfaces; + +struct Interface { + name: String, + //curr_delay: u32, +} + +struct ProgramContext { + interf_vec: Vec, +} fn run() -> Result<(), Box> { + let mut ctx: ProgramContext = ProgramContext { + interf_vec: list_interfaces()? + .into_iter() + .map(|name| Interface { name }) + .collect(), + }; + + ctx.interf_vec.push(Interface { + name: "aaa".to_string(), + }); ratatui::run(|terminal| { - let list = list_interfaces().unwrap(); - for l in list { - terminal.draw(|frame| frame.render_widget(l, frame.area()))?; - } loop { - if event::read()?.is_key_press() { + terminal.draw(|frame| render(frame, &ctx))?; + + if handle_events()? { break Ok::<(), Box>(()); } } @@ -23,6 +43,44 @@ fn run() -> Result<(), Box> { Ok(()) } +fn render(frame: &mut ratatui::Frame, ctx: &ProgramContext) { + let general = Layout::default() + .direction(ratatui::layout::Direction::Vertical) + .margin(1) + .constraints(vec![Constraint::Length(3), + Constraint::Length(1), // Padding + Constraint::Percentage(100)]) + .split(frame.area()); + + frame.render_widget( + Paragraph::new("Commands: [q]uit").block(Block::new().borders(Borders::ALL)), + general[0], + ); + + let aval_interf = Layout::default() + .direction(ratatui::layout::Direction::Vertical) + .constraints(vec![Constraint::Length(3); ctx.interf_vec.len()]) + .split(general[2]); + + for (interf, area) in ctx.interf_vec.iter().zip(aval_interf.iter()) { + frame.render_widget( + Paragraph::new(interf.name.as_str()).block(Block::new().borders(Borders::ALL)), + *area, + ); + } +} + +fn handle_events() -> std::io::Result { + match event::read()? { + Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { + KeyCode::Char('q') => return Ok(true), + _ => {} + }, + _ => {} + } + Ok(false) +} + fn main() -> ExitCode { env_logger::init(); From 92e558edc46deecb440e7cb301c8766edacfe8f4 Mon Sep 17 00:00:00 2001 From: frapank Date: Mon, 15 Jun 2026 02:18:54 +0200 Subject: [PATCH 06/28] add: functional gui --- src/main.rs | 83 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/src/main.rs b/src/main.rs index a312dce..0f28e18 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,16 @@ mod utils; -use crossterm::event::{self, Event, KeyCode, KeyEventKind}; -use log::error; -use ratatui::layout::{Constraint, Layout}; -use ratatui::style::{Color, Stylize}; -use ratatui::widgets::{Block, Borders, Paragraph}; use std::error::Error; use std::process::ExitCode; + +use crossterm::event::{self, Event, KeyCode, KeyEventKind}; +use log::error; + +use ratatui::layout::{Alignment, Constraint, Layout}; +use ratatui::style::{Color, Style}; +use ratatui::text::Line; +use ratatui::widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}; + use utils::list_interfaces; struct Interface { @@ -16,6 +20,7 @@ struct Interface { struct ProgramContext { interf_vec: Vec, + interf_sel: usize, } fn run() -> Result<(), Box> { @@ -24,6 +29,7 @@ fn run() -> Result<(), Box> { .into_iter() .map(|name| Interface { name }) .collect(), + interf_sel: 0 }; ctx.interf_vec.push(Interface { @@ -34,7 +40,7 @@ fn run() -> Result<(), Box> { loop { terminal.draw(|frame| render(frame, &ctx))?; - if handle_events()? { + if handle_events(&mut ctx)? { break Ok::<(), Box>(()); } } @@ -47,34 +53,65 @@ fn render(frame: &mut ratatui::Frame, ctx: &ProgramContext) { let general = Layout::default() .direction(ratatui::layout::Direction::Vertical) .margin(1) - .constraints(vec![Constraint::Length(3), - Constraint::Length(1), // Padding - Constraint::Percentage(100)]) + .constraints(vec![Constraint::Length(3), Constraint::Min(0)]) .split(frame.area()); + let rounded = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded); + frame.render_widget( - Paragraph::new("Commands: [q]uit").block(Block::new().borders(Borders::ALL)), + Paragraph::new("NetLatencyInjector").alignment(Alignment::Center).block(rounded.clone()), general[0], ); - let aval_interf = Layout::default() - .direction(ratatui::layout::Direction::Vertical) - .constraints(vec![Constraint::Length(3); ctx.interf_vec.len()]) - .split(general[2]); - - for (interf, area) in ctx.interf_vec.iter().zip(aval_interf.iter()) { - frame.render_widget( - Paragraph::new(interf.name.as_str()).block(Block::new().borders(Borders::ALL)), - *area, - ); - } + let items: Vec = ctx + .interf_vec + .iter() + .enumerate() + .map(|(idx, interf)| { + if idx == ctx.interf_sel { + ListItem::new( + Line::from(format!("> {}", interf.name)) + ) + .style( + Style::default() + .fg(Color::Black) + .bg(Color::LightBlue) + ) + } else { + ListItem::new( + Line::from(format!(" {}", interf.name)) + ) + } + }) + .collect(); + + let list = List::new(items).block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title(" Interfaces "), + ); + + frame.render_widget(list, general[1]); } -fn handle_events() -> std::io::Result { +fn handle_events(ctx: &mut ProgramContext) -> std::io::Result { match event::read()? { Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { KeyCode::Char('q') => return Ok(true), - _ => {} + KeyCode::Char('j') => { + ctx.interf_sel = (ctx.interf_sel + 1) % ctx.interf_vec.len(); + }, + KeyCode::Char('k') => { + if ctx.interf_sel == 0 { + ctx.interf_sel = ctx.interf_vec.len() - 1; + } else { + ctx.interf_sel -= 1; + } + }, + _ => {} }, _ => {} } From 21c631e886abd947865e33fbc748bdcf4057c400 Mon Sep 17 00:00:00 2001 From: frapank Date: Mon, 15 Jun 2026 02:57:28 +0200 Subject: [PATCH 07/28] change: move gui definition from main.rs to gui.rs --- src/gui/gui.rs | 109 +++++++++++++++++++++++++++++++++++++++++++++++++ src/gui/mod.rs | 1 + src/main.rs | 107 ++---------------------------------------------- 3 files changed, 113 insertions(+), 104 deletions(-) create mode 100644 src/gui/gui.rs diff --git a/src/gui/gui.rs b/src/gui/gui.rs new file mode 100644 index 0000000..94816ae --- /dev/null +++ b/src/gui/gui.rs @@ -0,0 +1,109 @@ +use ratatui::layout::{Alignment, Constraint, Layout}; +use ratatui::style::{Color, Style}; +use ratatui::text::Line; +use ratatui::widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}; + +use crate::Interface; +use crate::ProgramContext; +use crate::list_interfaces; +use crossterm::event::{self, Event, KeyCode, KeyEventKind}; + +use std::error::Error; + +pub struct Gui { + ctx: ProgramContext, +} + +impl Gui { + pub fn run() -> Result<(), Box> { + let mut gui = Self::new()?; + + ratatui::run(|terminal| { + loop { + terminal.draw(|frame| gui.render(frame))?; + + if gui.handle_events()? { + break Ok::<(), Box>(()); + } + } + })?; + + Ok(()) + } + + pub fn render(&self, frame: &mut ratatui::Frame) { + let general = Layout::default() + .direction(ratatui::layout::Direction::Vertical) + .margin(1) + .constraints(vec![Constraint::Length(3), Constraint::Min(0)]) + .split(frame.area()); + + let rounded = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded); + + frame.render_widget( + Paragraph::new("NetLatencyInjector") + .alignment(Alignment::Center) + .block(rounded.clone()), + general[0], + ); + + let items: Vec = self + .ctx + .interf_vec + .iter() + .enumerate() + .map(|(idx, interf)| { + if idx == self.ctx.interf_sel { + ListItem::new(Line::from(format!("> {}", interf.name))) + .style(Style::default().fg(Color::Black).bg(Color::LightBlue)) + } else { + ListItem::new(Line::from(format!(" {}", interf.name))) + } + }) + .collect(); + + let list = List::new(items).block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title(" Interfaces "), + ); + + frame.render_widget(list, general[1]); + } + + pub fn handle_events(&mut self) -> std::io::Result { + match event::read()? { + Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { + KeyCode::Char('q') => return Ok(true), + KeyCode::Char('j') => { + self.ctx.interf_sel = (self.ctx.interf_sel + 1) % self.ctx.interf_vec.len(); + } + KeyCode::Char('k') => { + if self.ctx.interf_sel == 0 { + self.ctx.interf_sel = self.ctx.interf_vec.len() - 1; + } else { + self.ctx.interf_sel -= 1; + } + } + _ => {} + }, + _ => {} + } + Ok(false) + } + + fn new() -> Result> { + let ctx = ProgramContext { + interf_vec: list_interfaces()? + .into_iter() + .map(|name| Interface { name }) + .collect(), + interf_sel: 0, + }; + + Ok(Self { ctx }) + } +} diff --git a/src/gui/mod.rs b/src/gui/mod.rs index e69de29..14c39ab 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -0,0 +1 @@ +pub mod gui; diff --git a/src/main.rs b/src/main.rs index 0f28e18..ed55e2a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,15 @@ +mod gui; mod utils; -use std::error::Error; use std::process::ExitCode; -use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use log::error; -use ratatui::layout::{Alignment, Constraint, Layout}; -use ratatui::style::{Color, Style}; -use ratatui::text::Line; -use ratatui::widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}; - +use crate::gui::gui::Gui; use utils::list_interfaces; struct Interface { name: String, - //curr_delay: u32, } struct ProgramContext { @@ -23,105 +17,10 @@ struct ProgramContext { interf_sel: usize, } -fn run() -> Result<(), Box> { - let mut ctx: ProgramContext = ProgramContext { - interf_vec: list_interfaces()? - .into_iter() - .map(|name| Interface { name }) - .collect(), - interf_sel: 0 - }; - - ctx.interf_vec.push(Interface { - name: "aaa".to_string(), - }); - - ratatui::run(|terminal| { - loop { - terminal.draw(|frame| render(frame, &ctx))?; - - if handle_events(&mut ctx)? { - break Ok::<(), Box>(()); - } - } - })?; - - Ok(()) -} - -fn render(frame: &mut ratatui::Frame, ctx: &ProgramContext) { - let general = Layout::default() - .direction(ratatui::layout::Direction::Vertical) - .margin(1) - .constraints(vec![Constraint::Length(3), Constraint::Min(0)]) - .split(frame.area()); - - let rounded = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded); - - frame.render_widget( - Paragraph::new("NetLatencyInjector").alignment(Alignment::Center).block(rounded.clone()), - general[0], - ); - - let items: Vec = ctx - .interf_vec - .iter() - .enumerate() - .map(|(idx, interf)| { - if idx == ctx.interf_sel { - ListItem::new( - Line::from(format!("> {}", interf.name)) - ) - .style( - Style::default() - .fg(Color::Black) - .bg(Color::LightBlue) - ) - } else { - ListItem::new( - Line::from(format!(" {}", interf.name)) - ) - } - }) - .collect(); - - let list = List::new(items).block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .title(" Interfaces "), - ); - - frame.render_widget(list, general[1]); -} - -fn handle_events(ctx: &mut ProgramContext) -> std::io::Result { - match event::read()? { - Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { - KeyCode::Char('q') => return Ok(true), - KeyCode::Char('j') => { - ctx.interf_sel = (ctx.interf_sel + 1) % ctx.interf_vec.len(); - }, - KeyCode::Char('k') => { - if ctx.interf_sel == 0 { - ctx.interf_sel = ctx.interf_vec.len() - 1; - } else { - ctx.interf_sel -= 1; - } - }, - _ => {} - }, - _ => {} - } - Ok(false) -} - fn main() -> ExitCode { env_logger::init(); - match run() { + match Gui::run() { Ok(()) => ExitCode::SUCCESS, Err(e) => { error!("{e}"); From 2859822406b9990af5a9856e93eb558e2551d999 Mon Sep 17 00:00:00 2001 From: frapank Date: Mon, 15 Jun 2026 12:40:10 +0200 Subject: [PATCH 08/28] move: ProgramContext logic to main.c --- src/gui/gui.rs | 17 ++++------------- src/main.rs | 14 +++++++++++++- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/gui/gui.rs b/src/gui/gui.rs index 94816ae..3a1dc26 100644 --- a/src/gui/gui.rs +++ b/src/gui/gui.rs @@ -3,9 +3,7 @@ use ratatui::style::{Color, Style}; use ratatui::text::Line; use ratatui::widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}; -use crate::Interface; use crate::ProgramContext; -use crate::list_interfaces; use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use std::error::Error; @@ -15,8 +13,8 @@ pub struct Gui { } impl Gui { - pub fn run() -> Result<(), Box> { - let mut gui = Self::new()?; + pub fn run(ctx: ProgramContext) -> Result<(), Box> { + let mut gui = Self::new(ctx)?; ratatui::run(|terminal| { loop { @@ -95,15 +93,8 @@ impl Gui { Ok(false) } - fn new() -> Result> { - let ctx = ProgramContext { - interf_vec: list_interfaces()? - .into_iter() - .map(|name| Interface { name }) - .collect(), - interf_sel: 0, - }; - + fn new(ctx: ProgramContext) -> Result> { + Ok(Self { ctx }) } } diff --git a/src/main.rs b/src/main.rs index ed55e2a..6da1ce3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,10 +17,22 @@ struct ProgramContext { interf_sel: usize, } +fn run_program() -> Result<(), Box> { + let ctx = ProgramContext { + interf_vec: list_interfaces()? + .into_iter() + .map(|name| Interface { name }) + .collect(), + interf_sel: 0, + }; + + Gui::run(ctx) +} + fn main() -> ExitCode { env_logger::init(); - match Gui::run() { + match run_program() { Ok(()) => ExitCode::SUCCESS, Err(e) => { error!("{e}"); From 9acb9f2fcaec014fbe62488c47f87216d0ab5894 Mon Sep 17 00:00:00 2001 From: frapank Date: Mon, 15 Jun 2026 12:47:31 +0200 Subject: [PATCH 09/28] move: ProgramContext struct from main.rs to app.rs --- src/app.rs | 8 ++++++++ src/gui/gui.rs | 5 ++--- src/main.rs | 15 ++++----------- 3 files changed, 14 insertions(+), 14 deletions(-) create mode 100644 src/app.rs diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..2426b17 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,8 @@ +pub struct Interface { + pub name: String, +} + +pub struct ProgramContext { + pub interf_vec: Vec, + pub interf_sel: usize, +} diff --git a/src/gui/gui.rs b/src/gui/gui.rs index 3a1dc26..4887acb 100644 --- a/src/gui/gui.rs +++ b/src/gui/gui.rs @@ -3,11 +3,11 @@ use ratatui::style::{Color, Style}; use ratatui::text::Line; use ratatui::widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}; -use crate::ProgramContext; use crossterm::event::{self, Event, KeyCode, KeyEventKind}; - use std::error::Error; +use crate::app::ProgramContext; + pub struct Gui { ctx: ProgramContext, } @@ -94,7 +94,6 @@ impl Gui { } fn new(ctx: ProgramContext) -> Result> { - Ok(Self { ctx }) } } diff --git a/src/main.rs b/src/main.rs index 6da1ce3..609160d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod app; mod gui; mod utils; @@ -5,25 +6,17 @@ use std::process::ExitCode; use log::error; -use crate::gui::gui::Gui; +use app::{Interface, ProgramContext}; +use gui::gui::Gui; use utils::list_interfaces; -struct Interface { - name: String, -} - -struct ProgramContext { - interf_vec: Vec, - interf_sel: usize, -} - fn run_program() -> Result<(), Box> { let ctx = ProgramContext { interf_vec: list_interfaces()? .into_iter() .map(|name| Interface { name }) .collect(), - interf_sel: 0, + interf_sel: 0, }; Gui::run(ctx) From 93e4946ad3eaaa3d028db283392e32be1752e4a7 Mon Sep 17 00:00:00 2001 From: frapank Date: Mon, 15 Jun 2026 12:50:10 +0200 Subject: [PATCH 10/28] rename: gui.rs to tui.rs --- src/gui/mod.rs | 2 +- src/gui/{gui.rs => tui.rs} | 0 src/main.rs | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/gui/{gui.rs => tui.rs} (100%) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 14c39ab..10925a2 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1 +1 @@ -pub mod gui; +pub mod tui; diff --git a/src/gui/gui.rs b/src/gui/tui.rs similarity index 100% rename from src/gui/gui.rs rename to src/gui/tui.rs diff --git a/src/main.rs b/src/main.rs index 609160d..4e5adf9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use std::process::ExitCode; use log::error; use app::{Interface, ProgramContext}; -use gui::gui::Gui; +use gui::tui::Gui; use utils::list_interfaces; fn run_program() -> Result<(), Box> { From 3e144569934fbd299d524419b3b67aed52ee50f9 Mon Sep 17 00:00:00 2001 From: frapank Date: Mon, 15 Jun 2026 13:14:50 +0200 Subject: [PATCH 11/28] add: clippy check --- src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.rs b/src/main.rs index 4e5adf9..91fa738 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +#![warn(clippy::pedantic)] + mod app; mod gui; mod utils; From a0c1f1ba76e995470bcd0aa44de5d1d0ba4907f4 Mon Sep 17 00:00:00 2001 From: frapank Date: Mon, 15 Jun 2026 13:15:21 +0200 Subject: [PATCH 12/28] add: constant for TITLE_HEIGHT, ControlFlow insted of bool and safe check for the handler --- src/gui/tui.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/gui/tui.rs b/src/gui/tui.rs index 4887acb..837922e 100644 --- a/src/gui/tui.rs +++ b/src/gui/tui.rs @@ -5,9 +5,12 @@ use ratatui::widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}; use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use std::error::Error; +use std::ops::ControlFlow; use crate::app::ProgramContext; +const TITLE_HEIGHT: u16 = 3; + pub struct Gui { ctx: ProgramContext, } @@ -20,7 +23,7 @@ impl Gui { loop { terminal.draw(|frame| gui.render(frame))?; - if gui.handle_events()? { + if gui.handle_events()?.is_break() { break Ok::<(), Box>(()); } } @@ -33,7 +36,7 @@ impl Gui { let general = Layout::default() .direction(ratatui::layout::Direction::Vertical) .margin(1) - .constraints(vec![Constraint::Length(3), Constraint::Min(0)]) + .constraints(vec![Constraint::Length(TITLE_HEIGHT), Constraint::Min(0)]) .split(frame.area()); let rounded = Block::default() @@ -72,11 +75,11 @@ impl Gui { frame.render_widget(list, general[1]); } - pub fn handle_events(&mut self) -> std::io::Result { + pub fn handle_events(&mut self) -> std::io::Result> { match event::read()? { Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { - KeyCode::Char('q') => return Ok(true), - KeyCode::Char('j') => { + KeyCode::Char('q') => return Ok(ControlFlow::Break(())), + KeyCode::Char('j') if !self.ctx.interf_vec.is_empty() => { self.ctx.interf_sel = (self.ctx.interf_sel + 1) % self.ctx.interf_vec.len(); } KeyCode::Char('k') => { @@ -90,7 +93,7 @@ impl Gui { }, _ => {} } - Ok(false) + Ok(ControlFlow::Continue(())) } fn new(ctx: ProgramContext) -> Result> { From ed2ddb54126d85772666e935a49379320ee64c3e Mon Sep 17 00:00:00 2001 From: frapank Date: Mon, 15 Jun 2026 13:17:41 +0200 Subject: [PATCH 13/28] remove: Result from gui::new() --- src/gui/tui.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gui/tui.rs b/src/gui/tui.rs index 837922e..9610f57 100644 --- a/src/gui/tui.rs +++ b/src/gui/tui.rs @@ -17,7 +17,7 @@ pub struct Gui { impl Gui { pub fn run(ctx: ProgramContext) -> Result<(), Box> { - let mut gui = Self::new(ctx)?; + let mut gui = Self::new(ctx); ratatui::run(|terminal| { loop { @@ -96,7 +96,7 @@ impl Gui { Ok(ControlFlow::Continue(())) } - fn new(ctx: ProgramContext) -> Result> { - Ok(Self { ctx }) + fn new(ctx: ProgramContext) -> Self { + Self { ctx } } } From a83e3795495c3ca1e8b2c8c1f041f0db74252fc6 Mon Sep 17 00:00:00 2001 From: frapank Date: Mon, 15 Jun 2026 13:18:55 +0200 Subject: [PATCH 14/28] fix: redundant closure --- src/utils.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index b8dbd4a..076feaa 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,11 +1,9 @@ pub fn list_interfaces() -> std::io::Result> { let mut ifaces: Vec = std::fs::read_dir("/sys/class/net/")? - .filter_map(|e| e.ok()) + .filter_map(std::io::Result::ok) .map(|e| e.file_name().to_string_lossy().into_owned()) .filter(|name| name != "lo") .collect(); ifaces.sort(); Ok(ifaces) } - - From e61fb61eead9282bcaab32c2b0112e7d6a64516f Mon Sep 17 00:00:00 2001 From: frapank Date: Tue, 16 Jun 2026 10:21:27 +0200 Subject: [PATCH 15/28] add: new impl for PorgramContext, and new delay field --- src/app.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/app.rs b/src/app.rs index 2426b17..0fdfb83 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,8 +1,29 @@ +use crate::utils::list_interfaces; +use crate::utils::get_delay; + pub struct Interface { pub name: String, + pub delay: u32 } pub struct ProgramContext { pub interf_vec: Vec, pub interf_sel: usize, } + +impl ProgramContext { + pub fn new() -> Result { + let mut interfaces = Vec::new(); + + for name in list_interfaces()? { + let delay = get_delay(&name)?.unwrap_or(0); + + interfaces.push(Interface { name, delay }); + } + + Ok(Self { + interf_vec: interfaces, + interf_sel: 0, + }) + } +} From d71a1fbeb6944dff1ef5d480fb3adebba7dfc0d4 Mon Sep 17 00:00:00 2001 From: frapank Date: Tue, 16 Jun 2026 10:21:42 +0200 Subject: [PATCH 16/28] add: delay print to ListItem --- src/gui/tui.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/tui.rs b/src/gui/tui.rs index 9610f57..aaa8a1c 100644 --- a/src/gui/tui.rs +++ b/src/gui/tui.rs @@ -57,7 +57,7 @@ impl Gui { .enumerate() .map(|(idx, interf)| { if idx == self.ctx.interf_sel { - ListItem::new(Line::from(format!("> {}", interf.name))) + ListItem::new(Line::from(format!(" > {} [{}ms]", interf.name, interf.delay))) .style(Style::default().fg(Color::Black).bg(Color::LightBlue)) } else { ListItem::new(Line::from(format!(" {}", interf.name))) From 3b0fd19d79ea19a2879145e0920d746ae6f61cfe Mon Sep 17 00:00:00 2001 From: frapank Date: Tue, 16 Jun 2026 10:22:04 +0200 Subject: [PATCH 17/28] change: use of ProgramContext::new() insted of manual definition --- src/main.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index 91fa738..68b25c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,18 +8,11 @@ use std::process::ExitCode; use log::error; -use app::{Interface, ProgramContext}; +use app::ProgramContext; use gui::tui::Gui; -use utils::list_interfaces; fn run_program() -> Result<(), Box> { - let ctx = ProgramContext { - interf_vec: list_interfaces()? - .into_iter() - .map(|name| Interface { name }) - .collect(), - interf_sel: 0, - }; + let ctx = ProgramContext::new()?; Gui::run(ctx) } From 4f17083cba8dba8f493ce63141e65c591c26c89c Mon Sep 17 00:00:00 2001 From: frapank Date: Tue, 16 Jun 2026 10:23:00 +0200 Subject: [PATCH 18/28] add: thiserror and key functions --- Cargo.toml | 1 + src/utils.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 366109e..4eec8d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,4 @@ crossterm = "0.29.0" env_logger = "0.11.10" log = "0.4.32" ratatui = "0.30.1" +thiserror = "2.0.18" diff --git a/src/utils.rs b/src/utils.rs index 076feaa..a7687d3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,15 @@ +use thiserror::Error; +use std::process::Command; + +#[derive(Debug, Error)] +pub enum NetemError { + #[error("tc returned non-zero exit status: {0}")] + TcExitError(String), + + #[error("io error: {0}")] + Io(#[from] std::io::Error), +} + pub fn list_interfaces() -> std::io::Result> { let mut ifaces: Vec = std::fs::read_dir("/sys/class/net/")? .filter_map(std::io::Result::ok) @@ -7,3 +19,64 @@ pub fn list_interfaces() -> std::io::Result> { ifaces.sort(); Ok(ifaces) } + +pub fn get_delay(iface: &str) -> Result, NetemError> { + let output = Command::new("tc") + .args(["qdisc", "show", "dev", iface]) + .output()?; + + if !output.status.success() { + return Err(NetemError::TcExitError( + String::from_utf8_lossy(&output.stderr).into_owned(), + )); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + + for line in stdout.lines() { + if let Some(pos) = line.find("delay ") { + let part = &line[pos + 6..]; + + if let Some(end) = part.find("ms") { + let num = &part[..end]; + + if let Ok(val) = num.trim().parse::() { + return Ok(Some(val.round() as u32)); + } + } + } + } + + Ok(None) + +} + +pub fn set_delay(iface: &str, ms: u32) -> Result<(), NetemError> { + let delay = format!("{ms}ms"); + + let output = Command::new("tc") + .args(["qdisc", "replace", "dev", iface, "root", "netem", "delay", delay.as_str()]) + .output()?; + + if !output.status.success() { + return Err(NetemError::TcExitError( + String::from_utf8_lossy(&output.stderr).into_owned(), + )); + } + + Ok(()) +} + +pub fn clear_delay(iface: &str) -> Result<(), NetemError> { + let output = Command::new("tc") + .args(["qdisc", "del", "dev", iface, "root"]) + .output()?; + + if !output.status.success() { + return Err(NetemError::TcExitError( + String::from_utf8_lossy(&output.stderr).into_owned(), + )); + } + + Ok(()) +} From 82eeef394843a5fb6778a949728dda32a1ed84be Mon Sep 17 00:00:00 2001 From: frapank Date: Tue, 16 Jun 2026 10:32:10 +0200 Subject: [PATCH 19/28] refactor: fmt --- src/app.rs | 4 ++-- src/gui/tui.rs | 7 +++++-- src/main.rs | 2 +- src/utils.rs | 18 +++++++++++++----- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/app.rs b/src/app.rs index 0fdfb83..a080ce2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,9 +1,9 @@ -use crate::utils::list_interfaces; use crate::utils::get_delay; +use crate::utils::list_interfaces; pub struct Interface { pub name: String, - pub delay: u32 + pub delay: u32, } pub struct ProgramContext { diff --git a/src/gui/tui.rs b/src/gui/tui.rs index aaa8a1c..6c4cb68 100644 --- a/src/gui/tui.rs +++ b/src/gui/tui.rs @@ -57,8 +57,11 @@ impl Gui { .enumerate() .map(|(idx, interf)| { if idx == self.ctx.interf_sel { - ListItem::new(Line::from(format!(" > {} [{}ms]", interf.name, interf.delay))) - .style(Style::default().fg(Color::Black).bg(Color::LightBlue)) + ListItem::new(Line::from(format!( + " > {} [{}ms]", + interf.name, interf.delay + ))) + .style(Style::default().fg(Color::Black).bg(Color::LightBlue)) } else { ListItem::new(Line::from(format!(" {}", interf.name))) } diff --git a/src/main.rs b/src/main.rs index 68b25c2..fa42dc6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ use app::ProgramContext; use gui::tui::Gui; fn run_program() -> Result<(), Box> { - let ctx = ProgramContext::new()?; + let ctx = ProgramContext::new()?; Gui::run(ctx) } diff --git a/src/utils.rs b/src/utils.rs index a7687d3..5e75729 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,5 @@ -use thiserror::Error; use std::process::Command; +use thiserror::Error; #[derive(Debug, Error)] pub enum NetemError { @@ -27,7 +27,7 @@ pub fn get_delay(iface: &str) -> Result, NetemError> { if !output.status.success() { return Err(NetemError::TcExitError( - String::from_utf8_lossy(&output.stderr).into_owned(), + String::from_utf8_lossy(&output.stderr).into_owned(), )); } @@ -48,19 +48,27 @@ pub fn get_delay(iface: &str) -> Result, NetemError> { } Ok(None) - } pub fn set_delay(iface: &str, ms: u32) -> Result<(), NetemError> { let delay = format!("{ms}ms"); let output = Command::new("tc") - .args(["qdisc", "replace", "dev", iface, "root", "netem", "delay", delay.as_str()]) + .args([ + "qdisc", + "replace", + "dev", + iface, + "root", + "netem", + "delay", + delay.as_str(), + ]) .output()?; if !output.status.success() { return Err(NetemError::TcExitError( - String::from_utf8_lossy(&output.stderr).into_owned(), + String::from_utf8_lossy(&output.stderr).into_owned(), )); } From 16ac5006468dd15fdfb3774db7577829f87c0864 Mon Sep 17 00:00:00 2001 From: frapank Date: Tue, 16 Jun 2026 10:34:47 +0200 Subject: [PATCH 20/28] add: github action --- .github/workflows/ci.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c4c41e8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +env: + CARGO_TERM_COLOR: always + +jobs: + check: + name: Fmt · Clippy · Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - uses: Swatinem/rust-cache@v2 + + - name: Formato + run: cargo fmt --all -- --check + + - name: Test + run: cargo test --verbose From cd613a890b0dd185a6d5b452128e5e2b0b33e11d Mon Sep 17 00:00:00 2001 From: frapank Date: Wed, 17 Jun 2026 20:43:10 +0200 Subject: [PATCH 21/28] add: popup to insert ms --- src/gui/tui.rs | 140 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 108 insertions(+), 32 deletions(-) diff --git a/src/gui/tui.rs b/src/gui/tui.rs index 6c4cb68..61523f9 100644 --- a/src/gui/tui.rs +++ b/src/gui/tui.rs @@ -1,34 +1,32 @@ -use ratatui::layout::{Alignment, Constraint, Layout}; +use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::style::{Color, Style}; use ratatui::text::Line; -use ratatui::widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}; - +use ratatui::widgets::{Block, BorderType, Borders, Clear, List, ListItem, Paragraph}; use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use std::error::Error; use std::ops::ControlFlow; - use crate::app::ProgramContext; +use crate::utils::{set_delay, NetemError}; const TITLE_HEIGHT: u16 = 3; pub struct Gui { ctx: ProgramContext, + popup_open: bool, + popup_input: String, } impl Gui { pub fn run(ctx: ProgramContext) -> Result<(), Box> { let mut gui = Self::new(ctx); - ratatui::run(|terminal| { loop { terminal.draw(|frame| gui.render(frame))?; - if gui.handle_events()?.is_break() { break Ok::<(), Box>(()); } } })?; - Ok(()) } @@ -57,49 +55,127 @@ impl Gui { .enumerate() .map(|(idx, interf)| { if idx == self.ctx.interf_sel { - ListItem::new(Line::from(format!( - " > {} [{}ms]", - interf.name, interf.delay - ))) - .style(Style::default().fg(Color::Black).bg(Color::LightBlue)) + ListItem::new(Line::from(format!(" > {} [{}ms]", interf.name, interf.delay))) + .style(Style::default().fg(Color::Black).bg(Color::LightBlue)) } else { ListItem::new(Line::from(format!(" {}", interf.name))) } }) .collect(); - let list = List::new(items).block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .title(" Interfaces "), + frame.render_widget( + List::new(items).block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title(" Interfaces "), + ), + general[1], ); - frame.render_widget(list, general[1]); + if self.popup_open { + let full = frame.area(); + + let area = Self::centered_rect(40, 3, full); + + frame.render_widget(Clear, area); + + frame.render_widget( + Paragraph::new(self.popup_input.as_str()).block( + Block::default() + .title(" Delay in ms — ESC close ") + .title_alignment(Alignment::Center) + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(Color::Yellow)), + ), + area, + ); + } } - pub fn handle_events(&mut self) -> std::io::Result> { + fn centered_rect(percent_x: u16, height_lines: u16, r: Rect) -> Rect { + let vertical = Layout::vertical([ + Constraint::Fill(1), + Constraint::Length(height_lines), + Constraint::Fill(1), + ]) + .split(r); + + Layout::horizontal([ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ]) + .split(vertical[1])[1] + } + + pub fn handle_events(&mut self) -> Result, NetemError> { match event::read()? { - Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { - KeyCode::Char('q') => return Ok(ControlFlow::Break(())), - KeyCode::Char('j') if !self.ctx.interf_vec.is_empty() => { - self.ctx.interf_sel = (self.ctx.interf_sel + 1) % self.ctx.interf_vec.len(); - } - KeyCode::Char('k') => { - if self.ctx.interf_sel == 0 { - self.ctx.interf_sel = self.ctx.interf_vec.len() - 1; - } else { - self.ctx.interf_sel -= 1; + Event::Key(key) if key.kind == KeyEventKind::Press => { + if self.popup_open { + match key.code { + KeyCode::Enter => { + if let Ok(delay) = self.popup_input.trim().parse::() { + if let Some(iface) = + self.ctx.interf_vec.get_mut(self.ctx.interf_sel) + { + iface.delay = delay; + if let Err(error) = set_delay(&iface.name, delay) { + return Err(error); + } + } + } + self.close_popup(); + } + KeyCode::Esc => self.close_popup(), + KeyCode::Backspace => { + self.popup_input.pop(); + } + KeyCode::Char(c) if c.is_ascii_digit() => { + self.popup_input.push(c); + } + _ => {} + } + } else { + match key.code { + KeyCode::Char('q') => return Ok(ControlFlow::Break(())), + KeyCode::Enter => self.open_sel(), + KeyCode::Char('j') if !self.ctx.interf_vec.is_empty() => { + self.ctx.interf_sel = + (self.ctx.interf_sel + 1) % self.ctx.interf_vec.len(); + } + KeyCode::Char('k') if !self.ctx.interf_vec.is_empty() => { + if self.ctx.interf_sel == 0 { + self.ctx.interf_sel = self.ctx.interf_vec.len() - 1; + } else { + self.ctx.interf_sel -= 1; + } + } + _ => {} } } - _ => {} - }, + } _ => {} } Ok(ControlFlow::Continue(())) } + fn open_sel(&mut self) { + self.popup_input.clear(); + self.popup_open = true; + } + + fn close_popup(&mut self) { + self.popup_input.clear(); + self.popup_open = false; + } + fn new(ctx: ProgramContext) -> Self { - Self { ctx } + Self { + ctx, + popup_open: false, + popup_input: String::new(), + } } } From 279751afdd5b4696816a0b92486796762db5fe67 Mon Sep 17 00:00:00 2001 From: frapank Date: Wed, 17 Jun 2026 20:43:19 +0200 Subject: [PATCH 22/28] remove: unused function --- src/utils.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 5e75729..a50e5cf 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -74,17 +74,3 @@ pub fn set_delay(iface: &str, ms: u32) -> Result<(), NetemError> { Ok(()) } - -pub fn clear_delay(iface: &str) -> Result<(), NetemError> { - let output = Command::new("tc") - .args(["qdisc", "del", "dev", iface, "root"]) - .output()?; - - if !output.status.success() { - return Err(NetemError::TcExitError( - String::from_utf8_lossy(&output.stderr).into_owned(), - )); - } - - Ok(()) -} From c443bc4f518a3b3970e93e477549ec38de8fd57f Mon Sep 17 00:00:00 2001 From: frapank Date: Wed, 17 Jun 2026 20:43:33 +0200 Subject: [PATCH 23/28] add: check if the program is runned with root privilege --- src/main.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main.rs b/src/main.rs index fa42dc6..df73804 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod gui; mod utils; use std::process::ExitCode; +use std::env; use log::error; @@ -20,6 +21,11 @@ fn run_program() -> Result<(), Box> { fn main() -> ExitCode { env_logger::init(); + if env::var("SUDO_USER").is_err() { + error!("Program require root privilege"); + return ExitCode::FAILURE; + } + match run_program() { Ok(()) => ExitCode::SUCCESS, Err(e) => { From 6f83818c07a56eba31e1ca272cef19449aff7dd4 Mon Sep 17 00:00:00 2001 From: frapank Date: Wed, 17 Jun 2026 20:43:41 +0200 Subject: [PATCH 24/28] change: project info --- Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4eec8d8..cb154b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "MemMapExplorer" -version = "0.1.0" -edition = "2024" +name = "NetLatencyInjector" +version = "1.0" +edition = "2026" [dependencies] crossterm = "0.29.0" +ratatui = "0.30.1" env_logger = "0.11.10" log = "0.4.32" -ratatui = "0.30.1" thiserror = "2.0.18" From 6fc74303cc016a29c22609d22e9a9fe6e843b8cd Mon Sep 17 00:00:00 2001 From: frapank Date: Wed, 17 Jun 2026 20:44:37 +0200 Subject: [PATCH 25/28] fix: minor definition --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cb154b7..4ed538a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "NetLatencyInjector" -version = "1.0" -edition = "2026" +version = "1.0.0" +edition = "2024" [dependencies] crossterm = "0.29.0" From f9261c2cb468e8ead098bfbb52d6af066e5cd25f Mon Sep 17 00:00:00 2001 From: frapank Date: Wed, 17 Jun 2026 20:44:43 +0200 Subject: [PATCH 26/28] refactor: cargo fmt --- src/gui/tui.rs | 13 ++++++++----- src/main.rs | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/gui/tui.rs b/src/gui/tui.rs index 61523f9..7fa5ea3 100644 --- a/src/gui/tui.rs +++ b/src/gui/tui.rs @@ -1,12 +1,12 @@ +use crate::app::ProgramContext; +use crate::utils::{NetemError, set_delay}; +use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::style::{Color, Style}; use ratatui::text::Line; use ratatui::widgets::{Block, BorderType, Borders, Clear, List, ListItem, Paragraph}; -use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use std::error::Error; use std::ops::ControlFlow; -use crate::app::ProgramContext; -use crate::utils::{set_delay, NetemError}; const TITLE_HEIGHT: u16 = 3; @@ -55,8 +55,11 @@ impl Gui { .enumerate() .map(|(idx, interf)| { if idx == self.ctx.interf_sel { - ListItem::new(Line::from(format!(" > {} [{}ms]", interf.name, interf.delay))) - .style(Style::default().fg(Color::Black).bg(Color::LightBlue)) + ListItem::new(Line::from(format!( + " > {} [{}ms]", + interf.name, interf.delay + ))) + .style(Style::default().fg(Color::Black).bg(Color::LightBlue)) } else { ListItem::new(Line::from(format!(" {}", interf.name))) } diff --git a/src/main.rs b/src/main.rs index df73804..1e00dd2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,8 +4,8 @@ mod app; mod gui; mod utils; -use std::process::ExitCode; use std::env; +use std::process::ExitCode; use log::error; From 899018a5ce6c630137c9b75d1c94528126c9bdbe Mon Sep 17 00:00:00 2001 From: frapank Date: Wed, 17 Jun 2026 20:47:35 +0200 Subject: [PATCH 27/28] remove: temporarily remove ci.yml --- .github/workflows/ci.yml | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index c4c41e8..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: CI - -on: - push: - branches: ["main"] - pull_request: - branches: ["main"] - -env: - CARGO_TERM_COLOR: always - -jobs: - check: - name: Fmt · Clippy · Test - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - - uses: Swatinem/rust-cache@v2 - - - name: Formato - run: cargo fmt --all -- --check - - - name: Test - run: cargo test --verbose From 63d1aa868f66e223fea2edca4aa3213776cfa394 Mon Sep 17 00:00:00 2001 From: Francesco <110063472+frapank@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:46:43 +0000 Subject: [PATCH 28/28] add: new github action --- .github/workflows/main.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..c4c41e8 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +env: + CARGO_TERM_COLOR: always + +jobs: + check: + name: Fmt · Clippy · Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - uses: Swatinem/rust-cache@v2 + + - name: Formato + run: cargo fmt --all -- --check + + - name: Test + run: cargo test --verbose