diff --git a/Cargo.lock b/Cargo.lock index 3052e7f1..f097cf2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -485,7 +485,7 @@ dependencies = [ "directories", "serde", "thiserror", - "toml", + "toml 0.5.11", ] [[package]] @@ -2500,7 +2500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.14", ] [[package]] @@ -3032,6 +3032,24 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_test" +version = "1.0.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a2f49ace1498612d14f7e0b8245519584db8299541dfe31a06374a828d620ab" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3142,6 +3160,8 @@ dependencies = [ "rodio", "rstest", "serde", + "serde_test", + "toml 0.8.0", "winres", ] @@ -3505,11 +3525,26 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c226a7bba6d859b63c92c4b4fe69c5b6b72d0cb897dbc8e6012298e6154cb56e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.20.0", +] + [[package]] name = "toml_datetime" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -3522,6 +3557,19 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ff63e60a958cefbb518ae1fd6566af80d9d4be430a33f3723dfc47d1d411d95" +dependencies = [ + "indexmap 2.0.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -4421,7 +4469,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" dependencies = [ - "toml", + "toml 0.5.11", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9011cb40..bc8b95f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ confy = "0.5.1" serde = { version = "1.0.188", default_features = false, features = ["derive"] } rodio = { version = "0.17.1", default_features = false, features = ["mp3"] } dns-lookup = "2.0.2" +toml = "0.8.0" [target.'cfg(target_arch = "powerpc64")'.dependencies] reqwest = { version = "0.11.20", features = ["json", "blocking"] } @@ -55,6 +56,7 @@ reqwest = { version = "0.11.20", default-features = false, features = ["json", " #─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── [dev-dependencies] +serde_test = "1.0.176" rstest = "0.18.2" #─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── diff --git a/README.md b/README.md index 171d45ab..ca107c11 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@

-

+

 

-

+

Application to comfortably monitor your Internet traffic
Multithreaded, cross-platform, reliable
🌐 www.sniffnet.net @@ -78,17 +78,17 @@ You can install Sniffnet in one of the following ways: [32-bit](https://github.com/GyulyVGC/sniffnet/releases/latest/download/Sniffnet_Windows_32-bit.msi) ### macOS - + - [Intel](https://github.com/GyulyVGC/sniffnet/releases/latest/download/Sniffnet_macOS_Intel.dmg) | [Apple silicon](https://github.com/GyulyVGC/sniffnet/releases/latest/download/Sniffnet_macOS_AppleSilicon.dmg) ### Linux - deb: [amd64](https://github.com/GyulyVGC/sniffnet/releases/latest/download/Sniffnet_LinuxDEB_amd64.deb) | -[arm64](https://github.com/GyulyVGC/sniffnet/releases/latest/download/Sniffnet_LinuxDEB_arm64.deb) | +[arm64](https://github.com/GyulyVGC/sniffnet/releases/latest/download/Sniffnet_LinuxDEB_arm64.deb) | [i386](https://github.com/GyulyVGC/sniffnet/releases/latest/download/Sniffnet_LinuxDEB_i386.deb) | [armhf](https://github.com/GyulyVGC/sniffnet/releases/latest/download/Sniffnet_LinuxDEB_armhf.deb) - + - rpm: [x86_64](https://github.com/GyulyVGC/sniffnet/releases/latest/download/Sniffnet_LinuxRPM_x86_64.rpm) | [aarch64](https://github.com/GyulyVGC/sniffnet/releases/latest/download/Sniffnet_LinuxRPM_aarch64.rpm) @@ -245,7 +245,7 @@ sudo sniffnet - 🌍 get information about the country of the remote hosts (IP geolocation) - ⭐ save your favorite network hosts - 🔉 set custom notifications to inform you when defined network events occur -- 🎨 choose the style that fits you the most from 4 different available themes +- 🎨 choose the style that fits you the most from 12 different available themes, plus custom theme support - 🕵️ inspect each of your network connections in real time - 📁 save complete textual reports with detailed information for each network connection: * source and destination IP addresses @@ -267,13 +267,13 @@ sudo sniffnet Geolocation and network providers (ASN) refer to the remote IP address of each connection. They are retrieved performing lookups against [MMDB files](https://maxmind.github.io/MaxMind-DB/): > **Note** - > + > > The MMDB (MaxMind database) format has been developed especially for IP lookup.
> It is optimized to perform lookups on data indexed by IP network ranges quickly and efficiently.
> It permits the best performance on IP lookups, and it's suitable for use in a production environment. - > + > > This product includes GeoLite2 data created by MaxMind, available from https://www.maxmind.com - + This file format potentially allows Sniffnet to execute hundreds of different IP lookups in a matter of a few milliseconds. @@ -284,23 +284,23 @@ sudo sniffnet

See details - +
- + Application layer protocols are inferred from the transport port numbers, following the convention maintained by [IANA](https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml). Please, remember that this is just a convention: > **Warning** - > + > > The Internet Assigned Numbers Authority (IANA) is responsible for maintaining > the official assignments of port numbers for specific uses.
> However, many unofficial uses of well-known port numbers occur in practice. The following table reports the port-to-service mappings used by Sniffnet, chosen from the most common assignments by IANA. - +
|Port number(s)|Application protocol | Description | @@ -372,6 +372,35 @@ The currently usable hotkeys are reported in the following.
+## Custom themes +
+ + See details + + Custom themes are specified as a TOML file. + + The TOML must follow this format: + ```toml + # Colors are in RGB/RGBA hexadecimal. + primary = "#1e1e2e" # Background + secondary = "#89b4fa" # Headers / incoming connections + buttons = "#313244" # Buttons + outgoing = "#f5c2e7" # Outgoing connections + text_headers = "#11111b" # Text headers + text_body = "#cdd6f4" # Text body + starred = "#f9e2afaa" # Favorites + + # The following parameters are in the range [0.0, 1.0]. + round_borders_alpha = 0.3 # Borders opacity + round_containers_alpha = 0.15 # Containers opacity + chart_badge_alpha = 0.2 # Chart opacity + + # Set to true if the theme is dark, false if it's light. + nightly = true + ``` + + The example theme above uses colors from [Catppuccin Mocha](https://github.com/catppuccin/catppuccin). +
## Troubleshooting @@ -388,9 +417,9 @@ Check the [required dependencies](#required-dependencies) section for instructio ### Rendering problems In some circumstances, especially if you are running on an old architecture or your graphical drivers are not up-to-date, -the `wgpu` default renderer used by [iced](https://github.com/iced-rs/iced) +the `wgpu` default renderer used by [iced](https://github.com/iced-rs/iced) may cause problems (country icons are completely black, or the interface glitches).
-In these cases you can download an alternative version of the application, +In these cases you can download an alternative version of the application, which is based on `tiny-skia`, a CPU-only software renderer that should work properly on every environment:
[Windows](https://github.com/GyulyVGC/sniffnet/suites/14909529200/artifacts/849640695) | [macOS](https://github.com/GyulyVGC/sniffnet/suites/14909529200/artifacts/849640694) | diff --git a/resources/themes/catppuccin_mocha.toml b/resources/themes/catppuccin_mocha.toml new file mode 100644 index 00000000..1b7cc74f --- /dev/null +++ b/resources/themes/catppuccin_mocha.toml @@ -0,0 +1,16 @@ +# Colors are in RGB/RGBA hexadecimal. +primary = "#1e1e2e" # Background +secondary = "#89b4fa" # Headers / incoming connections +buttons = "#313244" # Buttons +outgoing = "#f5c2e7" # Outgoing connections +text_headers = "#11111b" # Text headers +text_body = "#cdd6f4" # Text body +starred = "#f9e2afaa" # Favorites + +# The following parameters are in the range [0.0, 1.0]. +round_borders_alpha = 0.3 # Borders opacity +round_containers_alpha = 0.15 # Containers opacity +chart_badge_alpha = 0.2 # Chart opacity + +# Set to true if the theme is dark, false if it's light. +nightly = true \ No newline at end of file diff --git a/src/configs/types/config_advanced_settings.rs b/src/configs/types/config_advanced_settings.rs index a71e2e5f..fd175bcc 100644 --- a/src/configs/types/config_advanced_settings.rs +++ b/src/configs/types/config_advanced_settings.rs @@ -1,16 +1,19 @@ //! Module defining the `ConfigAdvancedSettings` struct, which allows to save and reload //! the application advanced settings. -use crate::utils::formatted_strings::get_default_report_directory; -use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use serde::{Deserialize, Serialize}; + +use crate::utils::formatted_strings::get_default_report_directory; + #[derive(Serialize, Deserialize, Clone, PartialEq)] pub struct ConfigAdvancedSettings { pub scale_factor: f64, pub mmdb_country: String, pub mmdb_asn: String, pub output_path: PathBuf, + pub style_path: PathBuf, } impl ConfigAdvancedSettings { @@ -42,6 +45,7 @@ impl Default for ConfigAdvancedSettings { mmdb_country: String::new(), mmdb_asn: String::new(), output_path: get_default_report_directory(), + style_path: PathBuf::new(), } } } diff --git a/src/gui/pages/inspect_page.rs b/src/gui/pages/inspect_page.rs index e152ba81..5687a122 100644 --- a/src/gui/pages/inspect_page.rs +++ b/src/gui/pages/inspect_page.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use iced::alignment::{Horizontal, Vertical}; use iced::widget::scrollable::Direction; use iced::widget::tooltip::Position; @@ -6,7 +8,6 @@ use iced::widget::{ lazy, Button, Checkbox, Column, Container, PickList, Row, Scrollable, Text, TextInput, Tooltip, }; use iced::{alignment, Alignment, Font, Length, Renderer}; -use std::path::Path; use crate::gui::components::tab::get_pages_tabs; use crate::gui::components::types::my_modal::MyModal; diff --git a/src/gui/pages/settings_advanced_page.rs b/src/gui/pages/settings_advanced_page.rs index 36dc44d9..2e5940bb 100644 --- a/src/gui/pages/settings_advanced_page.rs +++ b/src/gui/pages/settings_advanced_page.rs @@ -1,3 +1,15 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use iced::advanced::widget::Text; +use iced::alignment::{Horizontal, Vertical}; +use iced::widget::tooltip::Position; +use iced::widget::{ + button, horizontal_space, vertical_space, Column, Container, Row, Slider, TextInput, Tooltip, +}; +use iced::Length::Fixed; +use iced::{Alignment, Font, Length, Renderer}; + use crate::gui::components::tab::get_settings_tabs; use crate::gui::pages::settings_notifications_page::settings_header; use crate::gui::pages::types::settings_page::SettingsPage; @@ -5,27 +17,18 @@ use crate::gui::styles::container::ContainerType; use crate::gui::styles::style_constants::{get_font, get_font_headers, FONT_SIZE_SUBTITLE}; use crate::gui::styles::text::TextType; use crate::gui::styles::text_input::TextInputType; +use crate::gui::styles::types::custom_palette::CustomPalette; use crate::gui::types::message::Message; use crate::translations::translations_2::country_translation; use crate::translations::translations_3::{ - advanced_settings_translation, file_path_translation, info_mmdb_paths_translation, - mmdb_paths_translation, params_not_editable_translation, restore_defaults_translation, - scale_factor_translation, + advanced_settings_translation, custom_style_translation, file_path_translation, + info_mmdb_paths_translation, mmdb_paths_translation, params_not_editable_translation, + restore_defaults_translation, scale_factor_translation, }; use crate::utils::asn::MmdbReader; use crate::utils::formatted_strings::get_default_report_directory; use crate::utils::types::icon::Icon; use crate::{ConfigAdvancedSettings, Language, Sniffer, Status, StyleType}; -use iced::advanced::widget::Text; -use iced::alignment::{Horizontal, Vertical}; -use iced::widget::tooltip::Position; -use iced::widget::{ - button, horizontal_space, vertical_space, Column, Container, Row, Slider, TextInput, Tooltip, -}; -use iced::Length::Fixed; -use iced::{Alignment, Font, Length, Renderer}; -use std::path::PathBuf; -use std::sync::Arc; pub fn settings_advanced_page(sniffer: &Sniffer) -> Container> { let font = get_font(sniffer.style); @@ -58,6 +61,11 @@ pub fn settings_advanced_page(sniffer: &Sniffer) -> Container Row<'static, Message, Renderer> { + let path_str = &custom_path.to_string_lossy().to_string(); + + let is_error = if path_str.is_empty() { + false + } else { + CustomPalette::from_file(custom_path).is_err() + }; + + let input = TextInput::new("-", path_str) + .on_input(Message::LoadStyle) + .on_submit(Message::LoadStyle(path_str.clone())) + .padding([0, 5]) + .font(font) + .width(Length::Fixed(200.0)) + .style(if is_error { + TextInputType::Error + } else { + TextInputType::Standard + }); + + Row::new() + .spacing(5) + .push(Text::new(format!("{}:", custom_style_translation(language))).font(font)) + .push(input) +} diff --git a/src/gui/styles/custom_themes/dracula.rs b/src/gui/styles/custom_themes/dracula.rs index 58fcdf4b..58a9cef6 100644 --- a/src/gui/styles/custom_themes/dracula.rs +++ b/src/gui/styles/custom_themes/dracula.rs @@ -23,6 +23,7 @@ pub(in crate::gui::styles) fn dracula_dark() -> CustomPalette { round_borders_alpha: 0.1, round_containers_alpha: 0.04, chart_badge_alpha: 0.15, + nightly: true, }, } } @@ -43,6 +44,7 @@ pub(in crate::gui::styles) fn dracula_light() -> CustomPalette { chart_badge_alpha: 0.75, round_borders_alpha: 0.45, round_containers_alpha: 0.25, + nightly: false, }, } } diff --git a/src/gui/styles/custom_themes/gruvbox.rs b/src/gui/styles/custom_themes/gruvbox.rs index 2cd7b9fc..7f37cc90 100644 --- a/src/gui/styles/custom_themes/gruvbox.rs +++ b/src/gui/styles/custom_themes/gruvbox.rs @@ -24,6 +24,7 @@ pub(in crate::gui::styles) fn gruvbox_dark() -> CustomPalette { chart_badge_alpha: 0.15, round_borders_alpha: 0.12, round_containers_alpha: 0.05, + nightly: true, }, } } @@ -44,6 +45,7 @@ pub(in crate::gui::styles) fn gruvbox_light() -> CustomPalette { chart_badge_alpha: 0.75, round_borders_alpha: 0.45, round_containers_alpha: 0.2, + nightly: false, }, } } diff --git a/src/gui/styles/custom_themes/nord.rs b/src/gui/styles/custom_themes/nord.rs index e6016595..3386f78c 100644 --- a/src/gui/styles/custom_themes/nord.rs +++ b/src/gui/styles/custom_themes/nord.rs @@ -22,6 +22,7 @@ pub(in crate::gui::styles) fn nord_dark() -> CustomPalette { chart_badge_alpha: 0.2, round_borders_alpha: 0.35, round_containers_alpha: 0.15, + nightly: true, }, } } @@ -41,6 +42,7 @@ pub(in crate::gui::styles) fn nord_light() -> CustomPalette { chart_badge_alpha: 0.6, round_borders_alpha: 0.35, round_containers_alpha: 0.15, + nightly: false, }, } } diff --git a/src/gui/styles/custom_themes/solarized.rs b/src/gui/styles/custom_themes/solarized.rs index 8d09fa7f..d877a7b8 100644 --- a/src/gui/styles/custom_themes/solarized.rs +++ b/src/gui/styles/custom_themes/solarized.rs @@ -23,6 +23,7 @@ pub(in crate::gui::styles) fn solarized_light() -> CustomPalette { chart_badge_alpha: 0.75, round_borders_alpha: 0.35, round_containers_alpha: 0.15, + nightly: false, }, } } @@ -43,6 +44,7 @@ pub(in crate::gui::styles) fn solarized_dark() -> CustomPalette { chart_badge_alpha: 0.25, round_borders_alpha: 0.15, round_containers_alpha: 0.08, + nightly: true, }, } } diff --git a/src/gui/styles/types/color_remote.rs b/src/gui/styles/types/color_remote.rs new file mode 100644 index 00000000..32073d3d --- /dev/null +++ b/src/gui/styles/types/color_remote.rs @@ -0,0 +1,211 @@ +//! Remote implemention of [`serde::Deserialize`] and [`serde::Serialize`] for [`iced::Color`]. +//! +//! This implementation deserializes hexadecimal RGB(A) as string to float RGB(A) and back. +//! NOTE: The alpha channel is optional and defaults to #ff or 1.0. +//! `#ffffffff` deserializes to `1.0`, `1.0`, `1.0`, `1.0`. +//! `1.0`, `1.0`, `1.0`, `1.0` serializes to #ffffffff + +use std::hash::{Hash, Hasher}; + +use iced::Color; +use serde::{ + de::{Error as DeErrorTrait, Unexpected}, + Deserialize, Deserializer, Serializer, +}; + +// #aabbcc is seven bytes long +const HEX_STR_BASE_LEN: usize = 7; +// #aabbccdd is nine bytes long +const HEX_STR_ALPHA_LEN: usize = 9; + +pub(super) fn deserialize_color<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + // Field should be a hex string i.e. #aabbcc + let hex = String::deserialize(deserializer)?; + + // The string should be seven bytes long (octothorpe + six hex chars). + // Safety: Hexadecimal is ASCII so bytes are okay here. + let hex_len = hex.len(); + if hex_len == HEX_STR_BASE_LEN || hex_len == HEX_STR_ALPHA_LEN { + let color = hex + .strip_prefix('#') // Remove the octothorpe or fail + .ok_or_else(|| { + DeErrorTrait::invalid_value( + Unexpected::Char(hex.chars().next().unwrap_or_default()), + &"#", + ) + })? + // Iterating over bytes is safe because hex is ASCII. + // If the hex is not ASCII or invalid hex, then the iterator will short circuit and fail on `from_str_radix` + // TODO: This can be cleaned up when `iter_array_chunks` is stablized (https://github.com/rust-lang/rust/issues/100450) + .bytes() + .step_by(2) // Step by every first hex char of the two char sequence + .zip(hex.bytes().skip(2).step_by(2)) // Step by every second hex char + .map(|(first, second)| { + // Parse hex strings + let maybe_hex = [first, second]; + std::str::from_utf8(&maybe_hex) + .map_err(|_| { + DeErrorTrait::invalid_value(Unexpected::Str(&hex), &"valid hexadecimal") + }) + .and_then(|s| { + u8::from_str_radix(s, 16) + .map_err(DeErrorTrait::custom) + .map(|rgb| f32::from(rgb) / 255.0) + }) + }) + .collect::, _>>()?; + + // Alpha isn't always part of the color scheme. The resulting Vec should always have at least three elements. + // Accessing the first three elements without [slice::get] is okay because I checked the length of the hex string earlier. + Ok(Color { + r: color[0], + g: color[1], + b: color[2], + a: *color.get(3).unwrap_or(&1.0), + }) + } else { + Err(DeErrorTrait::invalid_length( + hex_len, + &&*format!("{HEX_STR_BASE_LEN} or {HEX_STR_ALPHA_LEN}"), + )) + } +} + +/// Hash delegate for [`iced::Color`] that hashes RGBA in lieu of floats. +#[inline] +pub(super) fn color_hash(color: Color, state: &mut H) { + // Hash isn't implemented for floats, so I hash the color as RGBA instead. + let color = color.into_rgba8(); + color.hash(state); +} + +/// Serialize [`iced::Color`] as a hex string. +#[inline] +pub(super) fn serialize_color(color: &Color, serializer: S) -> Result +where + S: Serializer, +{ + // iced::Color to [u8; 4] + let color = color.into_rgba8(); + + // [u8; 4] to hex string, precluding the alpha if it's 0xff. + let hex_color = if color[3] == 255 { + format!("#{:02x}{:02x}{:02x}", color[0], color[1], color[2]) + } else { + format!( + "#{:02x}{:02x}{:02x}{:02x}", + color[0], color[1], color[2], color[3] + ) + }; + + // Serialize the hex string + serializer.serialize_str(&hex_color) +} + +#[cfg(test)] +mod tests { + use iced::Color; + use serde::{Deserialize, Serialize}; + use serde_test::{assert_de_tokens_error, assert_tokens, Token}; + + use super::{deserialize_color, serialize_color}; + + // https://github.com/catppuccin/catppuccin + const CATPPUCCIN_PINK_HEX: &str = "#f5c2e7"; + const CATPPUCCIN_PINK: Color = Color { + r: 245.0 / 255.0, + g: 194.0 / 255.0, + b: 231.0 / 255.0, + a: 1.0, + }; + + const CATPPUCCIN_PINK_HEX_ALPHA: &str = "#f5c2e780"; + const CATPPUCCIN_PINK_ALPHA: Color = Color { + r: 245.0 / 255.0, + g: 194.0 / 255.0, + b: 231.0 / 255.0, + a: 128.0 / 255.0, + }; + + #[derive(Debug, PartialEq, Deserialize, Serialize)] + #[serde(transparent)] + struct DelegateTest { + #[serde( + flatten, + deserialize_with = "deserialize_color", + serialize_with = "serialize_color" + )] + color: Color, + } + + const CATPPUCCIN_PINK_DELEGATE: DelegateTest = DelegateTest { + color: CATPPUCCIN_PINK, + }; + + const CATPPUCCIN_PINK_ALPHA_DELEGATE: DelegateTest = DelegateTest { + color: CATPPUCCIN_PINK_ALPHA, + }; + + // Invalid hex colors + const CATPPUCCIN_PINK_NO_OCTO: &str = "%f5c2e7"; + const CATPPUCCIN_PINK_TRUNCATED: &str = "#c2e7"; + const CATPPUCCIN_PINK_TOO_LONG: &str = "#f5c2e7f5c2e7f5"; + const INVALID_COLOR: &str = "#ca🐈"; + + // Test if deserializing and serializing a color works. + #[test] + fn test_working_color_round_trip() { + assert_tokens( + &CATPPUCCIN_PINK_DELEGATE, + &[Token::Str(CATPPUCCIN_PINK_HEX)], + ); + } + + // Test if deserializing and serializing a color with an alpha channel works. + #[test] + fn test_working_color_with_alpha_round_trip() { + assert_tokens( + &CATPPUCCIN_PINK_ALPHA_DELEGATE, + &[Token::Str(CATPPUCCIN_PINK_HEX_ALPHA)], + ); + } + + // Missing octothorpe should fail. + #[test] + fn test_no_octothrope_color_rt() { + assert_de_tokens_error::( + &[Token::Str(CATPPUCCIN_PINK_NO_OCTO)], + "invalid value: character `%`, expected #", + ); + } + + // A hex color that is missing components should panic. + #[test] + fn test_len_too_small_color_de() { + assert_de_tokens_error::( + &[Token::Str(CATPPUCCIN_PINK_TRUNCATED)], + "invalid length 5, expected 7 or 9", + ); + } + + // A hex string that is too long shouldn't deserialize + #[test] + fn test_len_too_large_color_de() { + assert_de_tokens_error::( + &[Token::Str(CATPPUCCIN_PINK_TOO_LONG)], + "invalid length 15, expected 7 or 9", + ); + } + + // Invalid hexadecimal should panic + #[test] + fn test_invalid_hex_color_de() { + assert_de_tokens_error::( + &[Token::Str(INVALID_COLOR)], + "invalid value: string \"#ca🐈\", expected valid hexadecimal", + ); + } +} diff --git a/src/gui/styles/types/custom_palette.rs b/src/gui/styles/types/custom_palette.rs index 6a2d7f0a..f3ea4fb5 100644 --- a/src/gui/styles/types/custom_palette.rs +++ b/src/gui/styles/types/custom_palette.rs @@ -1,22 +1,42 @@ use std::fmt; +use std::fs::File; +use std::hash::{Hash, Hasher}; +use std::io::{BufReader, Read}; +use std::path::Path; use iced::Color; -use serde::{Deserialize, Serialize}; +use serde::{de::Error as DeErrorTrait, Deserialize, Serialize}; use crate::gui::styles::custom_themes::{dracula, gruvbox, nord, solarized}; use crate::gui::styles::types::palette::Palette; +use super::color_remote::{color_hash, deserialize_color, serialize_color}; + +const FLOAT_PRECISION: f32 = 10000.0; + /// Custom style with any relevant metadata +// NOTE: This is flattened for ergonomics. With flatten, both [Palette] and [PaletteExtension] can be +// defined in the TOML as a single entity rather than two separate tables. This is intentional because +// the separation between palette and its extension is an implementation detail that shouldn't be exposed +// to custom theme designers. +#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)] pub struct CustomPalette { - /// Color scheme's palette + /// Base colors for the theme + #[serde(flatten)] pub(crate) palette: Palette, /// Extra colors such as the favorites star + #[serde(flatten)] pub(crate) extension: PaletteExtension, } /// Extension color for themes. +#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)] pub struct PaletteExtension { /// Color of favorites star + #[serde( + deserialize_with = "deserialize_color", + serialize_with = "serialize_color" + )] pub starred: Color, /// Badge/logo alpha pub chart_badge_alpha: f32, @@ -24,10 +44,80 @@ pub struct PaletteExtension { pub round_borders_alpha: f32, /// Round containers alpha pub round_containers_alpha: f32, + /// Nightly (dark) style + pub nightly: bool, +} + +impl CustomPalette { + /// Deserialize [`CustomPalette`] from `path`. + /// + /// # Arguments + /// * `path` - Path to a UTF-8 encoded file containing a custom style as TOML. + pub fn from_file

(path: P) -> Result + where + P: AsRef, + { + // Try to open the file at `path` + let mut toml_reader = File::open(path) + .map_err(DeErrorTrait::custom) + .map(BufReader::new)?; + + // Read the ostensible TOML + let mut style_toml = String::new(); + toml_reader + .read_to_string(&mut style_toml) + .map_err(DeErrorTrait::custom)?; + + toml::de::from_str(&style_toml) + } +} + +impl Hash for CustomPalette { + fn hash(&self, state: &mut H) { + let Self { palette, extension } = self; + + let Palette { + primary, + secondary, + outgoing, + buttons, + text_headers, + text_body, + } = palette; + + color_hash(*primary, state); + color_hash(*secondary, state); + color_hash(*outgoing, state); + color_hash(*buttons, state); + color_hash(*text_headers, state); + color_hash(*text_body, state); + + extension.hash(state); + } +} + +impl Hash for PaletteExtension { + fn hash(&self, state: &mut H) { + let Self { + starred, + chart_badge_alpha, + round_borders_alpha, + round_containers_alpha, + .. + } = self; + + color_hash(*starred, state); + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + ((*chart_badge_alpha * FLOAT_PRECISION).trunc() as u32).hash(state); + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + ((*round_borders_alpha * FLOAT_PRECISION).trunc() as u32).hash(state); + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + ((*round_containers_alpha * FLOAT_PRECISION).trunc() as u32).hash(state); + } } /// Built in extra styles -#[derive(Clone, Copy, Serialize, Deserialize, Debug, Hash, PartialEq)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Serialize, Deserialize)] #[serde(tag = "custom")] pub enum ExtraStyles { DraculaDark, @@ -38,6 +128,7 @@ pub enum ExtraStyles { NordLight, SolarizedDark, SolarizedLight, + CustomToml(CustomPalette), } impl ExtraStyles { @@ -52,6 +143,7 @@ impl ExtraStyles { ExtraStyles::NordDark => nord::nord_dark().palette, ExtraStyles::SolarizedDark => solarized::solarized_dark().palette, ExtraStyles::SolarizedLight => solarized::solarized_light().palette, + ExtraStyles::CustomToml(user) => user.palette, } } @@ -66,21 +158,13 @@ impl ExtraStyles { ExtraStyles::NordDark => nord::nord_dark().extension, ExtraStyles::SolarizedDark => solarized::solarized_dark().extension, ExtraStyles::SolarizedLight => solarized::solarized_light().extension, + ExtraStyles::CustomToml(user) => user.extension, } } /// Theme is a night/dark style - pub const fn is_nightly(self) -> bool { - match self { - ExtraStyles::DraculaDark - | ExtraStyles::GruvboxDark - | ExtraStyles::NordDark - | ExtraStyles::SolarizedDark => true, - ExtraStyles::DraculaLight - | ExtraStyles::GruvboxLight - | ExtraStyles::NordLight - | ExtraStyles::SolarizedLight => false, - } + pub fn is_nightly(self) -> bool { + self.to_ext().nightly } /// Slice of all implemented custom styles @@ -109,6 +193,53 @@ impl fmt::Display for ExtraStyles { ExtraStyles::NordDark => write!(f, "Nord (Night)"), ExtraStyles::SolarizedLight => write!(f, "Solarized (Day)"), ExtraStyles::SolarizedDark => write!(f, "Solarized (Night)"), + // Custom style names aren't used anywhere so this shouldn't be reached + ExtraStyles::CustomToml(_) => unreachable!(), } } } + +#[cfg(test)] +mod tests { + use iced::color; + + use super::{CustomPalette, Palette, PaletteExtension}; + + fn style_path(name: &str) -> String { + format!( + "{}/resources/themes/{}.toml", + env!("CARGO_MANIFEST_DIR"), + name + ) + } + + // NOTE: This has to be updated if `resources/themes/catppuccin_mocha.toml` changes + fn catppuccin_style() -> CustomPalette { + CustomPalette { + palette: Palette { + primary: color!(30, 30, 46), + secondary: color!(137, 180, 250), + buttons: color!(49, 50, 68), + outgoing: color!(245, 194, 231), + text_headers: color!(17, 17, 27), + text_body: color!(205, 214, 244), + }, + extension: PaletteExtension { + starred: color!(249, 226, 175, 0.6666667), + round_borders_alpha: 0.3, + round_containers_alpha: 0.15, + chart_badge_alpha: 0.2, + nightly: true, + }, + } + } + + #[test] + fn custompalette_from_file_de() -> Result<(), toml::de::Error> { + let style = catppuccin_style(); + let style_de = CustomPalette::from_file(style_path("catppuccin_mocha"))?; + + assert_eq!(style, style_de); + Ok(()) + } +} diff --git a/src/gui/styles/types/mod.rs b/src/gui/styles/types/mod.rs index 4613b2da..87437804 100644 --- a/src/gui/styles/types/mod.rs +++ b/src/gui/styles/types/mod.rs @@ -1,3 +1,4 @@ +pub(super) mod color_remote; pub mod custom_palette; pub mod gradient_type; pub mod palette; diff --git a/src/gui/styles/types/palette.rs b/src/gui/styles/types/palette.rs index 5328a1a8..f78ba677 100644 --- a/src/gui/styles/types/palette.rs +++ b/src/gui/styles/types/palette.rs @@ -2,12 +2,15 @@ use iced::Color; use plotters::style::RGBColor; +use serde::{Deserialize, Serialize}; use crate::gui::styles::style_constants::{ DAY_STYLE, DEEP_SEA_STYLE, MON_AMOUR_STYLE, NIGHT_STYLE, }; use crate::StyleType; +use super::color_remote::{deserialize_color, serialize_color}; + /// Set of colors to apply to GUI /// /// Best practices: @@ -16,18 +19,43 @@ use crate::StyleType; /// - `secondary` and `outgoing` should be complementary colors if possible /// - `text_headers` should be black or white and must have a strong contrast with `secondary` /// - `text_body` should be black or white and must have a strong contrast with `primary` +#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)] pub struct Palette { /// Main color of the GUI (background, hovered buttons, active tab) + #[serde( + deserialize_with = "deserialize_color", + serialize_with = "serialize_color" + )] pub primary: Color, /// Secondary color of the GUI (incoming connections, header, footer, buttons' borders, radio selection) + #[serde( + deserialize_with = "deserialize_color", + serialize_with = "serialize_color" + )] pub secondary: Color, /// Color of outgoing connections + #[serde( + deserialize_with = "deserialize_color", + serialize_with = "serialize_color" + )] pub outgoing: Color, /// Color of active buttons (when not hovered) and inactive tabs + #[serde( + deserialize_with = "deserialize_color", + serialize_with = "serialize_color" + )] pub buttons: Color, /// Color of header and footer text + #[serde( + deserialize_with = "deserialize_color", + serialize_with = "serialize_color" + )] pub text_headers: Color, /// Color of body and buttons text + #[serde( + deserialize_with = "deserialize_color", + serialize_with = "serialize_color" + )] pub text_body: Color, } diff --git a/src/gui/types/message.rs b/src/gui/types/message.rs index b18d5493..1f575578 100644 --- a/src/gui/types/message.rs +++ b/src/gui/types/message.rs @@ -43,6 +43,8 @@ pub enum Message { Reset, /// Change application style Style(StyleType), + /// Deserialize a style from a path + LoadStyle(String), /// Manage waiting time Waiting, /// Displays a modal diff --git a/src/gui/types/sniffer.rs b/src/gui/types/sniffer.rs index 89752890..5711a221 100644 --- a/src/gui/types/sniffer.rs +++ b/src/gui/types/sniffer.rs @@ -16,6 +16,7 @@ use crate::countries::country_utils::COUNTRY_MMDB; use crate::gui::components::types::my_modal::MyModal; use crate::gui::pages::types::running_page::RunningPage; use crate::gui::pages::types::settings_page::SettingsPage; +use crate::gui::styles::types::custom_palette::{CustomPalette, ExtraStyles}; use crate::gui::styles::types::gradient_type::GradientType; use crate::gui::types::message::Message; use crate::gui::types::status::Status; @@ -177,6 +178,13 @@ impl Sniffer { self.style = style; self.traffic_chart.change_style(self.style); } + Message::LoadStyle(path) => { + self.advanced_settings.style_path = path.clone().into(); + if let Ok(palette) = CustomPalette::from_file(path) { + self.style = StyleType::Custom(ExtraStyles::CustomToml(palette)); + self.traffic_chart.change_style(self.style); + } + } Message::Waiting => self.update_waiting_dots(), Message::AddOrRemoveFavorite(host, add) => self.add_or_remove_favorite(&host, add), Message::ShowModal(modal) => { diff --git a/src/main.rs b/src/main.rs index 2cb8bd53..37aff457 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,9 +9,6 @@ use std::{panic, process, thread}; use iced::window::PlatformSpecific; use iced::{window, Application, Font, Settings}; -use crate::configs::types::config_advanced_settings::ConfigAdvancedSettings; -use crate::configs::types::config_window::{ConfigWindow, ToPosition}; -use crate::configs::types::configs::Configs; use chart::types::chart_type::ChartType; use chart::types::traffic_chart::TrafficChart; use cli::parse_cli_args; @@ -33,6 +30,9 @@ use report::types::report_sort_type::ReportSortType; use translations::types::language::Language; use utils::formatted_strings::print_cli_welcome_message; +use crate::configs::types::config_advanced_settings::ConfigAdvancedSettings; +use crate::configs::types::config_window::{ConfigWindow, ToPosition}; +use crate::configs::types::configs::Configs; use crate::secondary_threads::check_updates::set_newer_release_status; mod chart; diff --git a/src/networking/manage_packets.rs b/src/networking/manage_packets.rs index 5fdf002a..20cc5a95 100644 --- a/src/networking/manage_packets.rs +++ b/src/networking/manage_packets.rs @@ -9,7 +9,6 @@ use pcap::{Active, Address, Capture, Device}; use crate::countries::country_utils::get_country; use crate::networking::types::address_port_pair::AddressPortPair; use crate::networking::types::app_protocol::from_port_to_application_protocol; -use crate::networking::types::data_info::DataInfo; use crate::networking::types::data_info_host::DataInfoHost; use crate::networking::types::filters::Filters; use crate::networking::types::host::Host; @@ -284,7 +283,7 @@ pub fn reverse_dns_lookup( let other_data = info_traffic_lock .addresses_waiting_resolution .remove(&address_to_lookup) - .unwrap_or(DataInfo::default()); + .unwrap_or_default(); // insert the newly resolved host in the collections, with the data it exchanged so far info_traffic_lock .addresses_resolved diff --git a/src/notifications/types/notifications.rs b/src/notifications/types/notifications.rs index e3eff629..6d6f6b41 100644 --- a/src/notifications/types/notifications.rs +++ b/src/notifications/types/notifications.rs @@ -58,7 +58,7 @@ impl Default for PacketsNotification { impl PacketsNotification { /// Arbitrary string constructor. Will fallback values to existing notification if set, default() otherwise pub fn from(value: &str, existing: Option) -> Self { - let default = existing.unwrap_or(Self::default()); + let default = existing.unwrap_or_default(); let new_threshold = if value.is_empty() { 0 @@ -99,7 +99,7 @@ impl Default for BytesNotification { impl BytesNotification { /// Arbitrary string constructor. Will fallback values to existing notification if set, default() otherwise pub fn from(value: &str, existing: Option) -> Self { - let default = existing.unwrap_or(Self::default()); + let default = existing.unwrap_or_default(); let mut byte_multiple_inserted = ByteMultiple::B; let new_threshold = if value.is_empty() { diff --git a/src/secondary_threads/write_report_file.rs b/src/secondary_threads/write_report_file.rs index ad23e979..e65f9bbf 100644 --- a/src/secondary_threads/write_report_file.rs +++ b/src/secondary_threads/write_report_file.rs @@ -1,77 +1,77 @@ -//! Module containing functions executed by the thread in charge of updating the output report every 1 second - -use std::collections::HashSet; -use std::fs::File; -use std::io::{BufWriter, Seek, SeekFrom, Write}; -use std::sync::{Arc, Condvar, Mutex}; -use std::thread; -use std::time::Duration; - -use crate::gui::types::status::Status; -use crate::utils::formatted_strings::get_default_report_directory; -use crate::InfoTraffic; - -/// The calling thread enters in a loop in which it sleeps for 1 second and then -/// updates the output report containing detailed traffic information -pub fn sleep_and_write_report_loop( - current_capture_id: &Arc>, - info_traffic_mutex: &Arc>, - status_pair: &Arc<(Mutex, Condvar)>, -) { - let cvar = &status_pair.1; - - let path_report = get_default_report_directory(); - - let mut capture_id = *current_capture_id.lock().unwrap(); - - let mut output = - BufWriter::new(File::create(path_report.clone()).expect("Error creating output file\n\r")); - writeln!(output, "---------------------------------------------------------------------------------------------------------------------------------------------------------------------").expect("Error writing output file\n\r"); - writeln!(output, "| Src IP address | Src port | Dst IP address | Dst port | Layer 4 | Layer 7 | Packets | Bytes | Initial timestamp | Final timestamp |").expect("Error writing output file\n\r"); - writeln!(output, "---------------------------------------------------------------------------------------------------------------------------------------------------------------------").expect("Error writing output file\n\r"); - - loop { - // sleep 1 second - thread::sleep(Duration::from_secs(1)); - - let current_capture_id_lock = current_capture_id.lock().unwrap(); - if *current_capture_id_lock != capture_id { - capture_id = *current_capture_id_lock; - output = BufWriter::new( - File::create(path_report.clone()).expect("Error creating output file\n\r"), - ); - writeln!(output, "---------------------------------------------------------------------------------------------------------------------------------------------------------------------").expect("Error writing output file\n\r"); - writeln!(output, "| Src IP address | Src port | Dst IP address | Dst port | Layer 4 | Layer 7 | Packets | Bytes | Initial timestamp | Final timestamp |").expect("Error writing output file\n\r"); - writeln!(output, "---------------------------------------------------------------------------------------------------------------------------------------------------------------------").expect("Error writing output file\n\r"); - } - drop(current_capture_id_lock); - - let mut status = status_pair.0.lock().expect("Error acquiring mutex\n\r"); - - if *status == Status::Running { - drop(status); - - let mut info_traffic = info_traffic_mutex - .lock() - .expect("Error acquiring mutex\n\r"); - - for index in &info_traffic.addresses_last_interval { - let key_val = info_traffic.map.get_index(*index).unwrap(); - let seek_pos = 166 * 3 + 206 * (*index) as u64; - output.seek(SeekFrom::Start(seek_pos)).unwrap(); - writeln!(output, "{}{}", key_val.0, key_val.1) - .expect("Error writing output file\n\r"); - } - info_traffic.addresses_last_interval = HashSet::new(); // empty set - - drop(info_traffic); - - output.flush().expect("Error writing output file\n\r"); - } else { - //status is Init - while *status == Status::Init { - status = cvar.wait(status).expect("Error acquiring mutex\n\r"); - } - } - } -} +// //! Module containing functions executed by the thread in charge of updating the output report every 1 second +// +// use std::collections::HashSet; +// use std::fs::File; +// use std::io::{BufWriter, Seek, SeekFrom, Write}; +// use std::sync::{Arc, Condvar, Mutex}; +// use std::thread; +// use std::time::Duration; +// +// use crate::gui::types::status::Status; +// use crate::utils::formatted_strings::get_default_report_directory; +// use crate::InfoTraffic; +// +// /// The calling thread enters in a loop in which it sleeps for 1 second and then +// /// updates the output report containing detailed traffic information +// pub fn sleep_and_write_report_loop( +// current_capture_id: &Arc>, +// info_traffic_mutex: &Arc>, +// status_pair: &Arc<(Mutex, Condvar)>, +// ) { +// let cvar = &status_pair.1; +// +// let path_report = get_default_report_directory(); +// +// let mut capture_id = *current_capture_id.lock().unwrap(); +// +// let mut output = +// BufWriter::new(File::create(path_report.clone()).expect("Error creating output file\n\r")); +// writeln!(output, "---------------------------------------------------------------------------------------------------------------------------------------------------------------------").expect("Error writing output file\n\r"); +// writeln!(output, "| Src IP address | Src port | Dst IP address | Dst port | Layer 4 | Layer 7 | Packets | Bytes | Initial timestamp | Final timestamp |").expect("Error writing output file\n\r"); +// writeln!(output, "---------------------------------------------------------------------------------------------------------------------------------------------------------------------").expect("Error writing output file\n\r"); +// +// loop { +// // sleep 1 second +// thread::sleep(Duration::from_secs(1)); +// +// let current_capture_id_lock = current_capture_id.lock().unwrap(); +// if *current_capture_id_lock != capture_id { +// capture_id = *current_capture_id_lock; +// output = BufWriter::new( +// File::create(path_report.clone()).expect("Error creating output file\n\r"), +// ); +// writeln!(output, "---------------------------------------------------------------------------------------------------------------------------------------------------------------------").expect("Error writing output file\n\r"); +// writeln!(output, "| Src IP address | Src port | Dst IP address | Dst port | Layer 4 | Layer 7 | Packets | Bytes | Initial timestamp | Final timestamp |").expect("Error writing output file\n\r"); +// writeln!(output, "---------------------------------------------------------------------------------------------------------------------------------------------------------------------").expect("Error writing output file\n\r"); +// } +// drop(current_capture_id_lock); +// +// let mut status = status_pair.0.lock().expect("Error acquiring mutex\n\r"); +// +// if *status == Status::Running { +// drop(status); +// +// let mut info_traffic = info_traffic_mutex +// .lock() +// .expect("Error acquiring mutex\n\r"); +// +// for index in &info_traffic.addresses_last_interval { +// let key_val = info_traffic.map.get_index(*index).unwrap(); +// let seek_pos = 166 * 3 + 206 * (*index) as u64; +// output.seek(SeekFrom::Start(seek_pos)).unwrap(); +// writeln!(output, "{}{}", key_val.0, key_val.1) +// .expect("Error writing output file\n\r"); +// } +// info_traffic.addresses_last_interval = HashSet::new(); // empty set +// +// drop(info_traffic); +// +// output.flush().expect("Error writing output file\n\r"); +// } else { +// //status is Init +// while *status == Status::Init { +// status = cvar.wait(status).expect("Error acquiring mutex\n\r"); +// } +// } +// } +// } diff --git a/src/translations/translations_3.rs b/src/translations/translations_3.rs index 870444a6..99e753fc 100644 --- a/src/translations/translations_3.rs +++ b/src/translations/translations_3.rs @@ -66,3 +66,11 @@ pub fn file_path_translation(language: Language) -> &'static str { _ => "File path", } } + +pub fn custom_style_translation(language: Language) -> &'static str { + match language { + Language::EN => "Custom style", + Language::IT => "Stile personalizzato", + _ => "Custom style", + } +}