├── src ├── bin │ ├── resources.rs │ ├── resources-kill.rs │ ├── resources-processes.rs │ └── resources-adjust.rs ├── ui │ ├── dialogs │ │ └── mod.rs │ ├── widgets │ │ ├── mod.rs │ │ ├── graph_box.rs │ │ └── double_graph_box.rs │ ├── mod.rs │ └── pages │ │ ├── mod.rs │ │ ├── processes │ │ └── process_name_cell.rs │ │ └── applications │ │ └── application_name_cell.rs ├── lib.rs ├── config.rs.in ├── utils │ ├── os.rs │ ├── link │ │ ├── mod.rs │ │ ├── sata.rs │ │ └── usb.rs │ ├── npu │ │ ├── other.rs │ │ └── intel.rs │ └── gpu │ │ ├── other.rs │ │ ├── v3d.rs │ │ └── amd.rs ├── meson.build ├── gui.rs └── i18n.rs ├── data ├── resources │ ├── screenshots │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ └── 8.png │ ├── meson.build │ ├── icons │ │ ├── info-symbolic.svg │ │ ├── app-symbolic.svg │ │ ├── flash-storage-symbolic.svg │ │ ├── wwan-symbolic.svg │ │ ├── floppy-symbolic.svg │ │ ├── memory-symbolic.svg │ │ ├── system-processes-symbolic.svg │ │ ├── search-symbolic.svg │ │ ├── slip-symbolic.svg │ │ ├── ethernet-symbolic.svg │ │ ├── infiniband-symbolic.svg │ │ ├── virtual-ethernet-symbolic.svg │ │ ├── unknown-network-type-symbolic.svg │ │ ├── gpu-symbolic.svg │ │ ├── options-symbolic.svg │ │ ├── hdd-symbolic.svg │ │ ├── battery-symbolic.svg │ │ ├── unknown-drive-type-symbolic.svg │ │ ├── nvme-symbolic.svg │ │ ├── ssd-symbolic.svg │ │ ├── processor-symbolic.svg │ │ ├── bluetooth-symbolic.svg │ │ ├── shell-symbolic.svg │ │ ├── cd-dvd-bluray-symbolic.svg │ │ ├── zram-symbolic.svg │ │ ├── wlan-symbolic.svg │ │ ├── vpn-symbolic.svg │ │ ├── emmc-symbolic.svg │ │ ├── select-all-symbolic.svg │ │ ├── bridge-symbolic.svg │ │ ├── vm-bridge-symbolic.svg │ │ ├── docker-bridge-symbolic.svg │ │ ├── loop-device-symbolic.svg │ │ ├── ram-disk-symbolic.svg │ │ ├── device-settings-symbolic.svg │ │ ├── generic-process-symbolic.svg │ │ ├── raid-symbolic.svg │ │ ├── npu-symbolic.svg │ │ ├── generic-settings-symbolic.svg │ │ ├── zfs-symbolic.svg │ │ └── mapped-device-symbolic.svg │ ├── style.css │ └── ui │ │ ├── widgets │ │ ├── stack_sidebar.ui │ │ ├── process_name_cell.ui │ │ ├── application_name_cell.ui │ │ ├── graph_box.ui │ │ ├── stack_sidebar_item.ui │ │ └── double_graph_box.ui │ │ ├── shortcuts.ui │ │ └── pages │ │ ├── applications.ui │ │ ├── memory.ui │ │ ├── battery.ui │ │ ├── drive.ui │ │ ├── npu.ui │ │ ├── network.ui │ │ └── gpu.ui ├── icons │ ├── meson.build │ └── net.nokyan.Resources-symbolic.svg ├── net.nokyan.Resources.desktop.in.in ├── net.nokyan.Resources.policy.in.in └── meson.build ├── .typos.toml ├── .gitignore ├── po ├── its │ ├── polkit.loc │ └── polkit.its ├── meson.build ├── LINGUAS └── POTFILES.in ├── meson_options.txt ├── lib └── process_data │ ├── src │ └── meson.build │ └── Cargo.toml ├── .github ├── workflows │ └── flatpak.yml └── ISSUE_TEMPLATE │ ├── feature_request.yaml │ └── bug_report.yaml ├── resources.doap ├── Cargo.toml ├── hooks └── pre-commit.hook ├── meson.build └── README.md /src/bin/resources.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | resources::gui::main(); 3 | } 4 | -------------------------------------------------------------------------------- /data/resources/screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nokyan/resources/HEAD/data/resources/screenshots/1.png -------------------------------------------------------------------------------- /data/resources/screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nokyan/resources/HEAD/data/resources/screenshots/2.png -------------------------------------------------------------------------------- /data/resources/screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nokyan/resources/HEAD/data/resources/screenshots/3.png -------------------------------------------------------------------------------- /data/resources/screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nokyan/resources/HEAD/data/resources/screenshots/4.png -------------------------------------------------------------------------------- /data/resources/screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nokyan/resources/HEAD/data/resources/screenshots/5.png -------------------------------------------------------------------------------- /data/resources/screenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nokyan/resources/HEAD/data/resources/screenshots/6.png -------------------------------------------------------------------------------- /data/resources/screenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nokyan/resources/HEAD/data/resources/screenshots/7.png -------------------------------------------------------------------------------- /data/resources/screenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nokyan/resources/HEAD/data/resources/screenshots/8.png -------------------------------------------------------------------------------- /src/ui/dialogs/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod app_dialog; 2 | pub mod process_dialog; 3 | pub mod process_options_dialog; 4 | pub mod settings_dialog; 5 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod application; 2 | #[rustfmt::skip] 3 | pub mod config; 4 | pub mod gui; 5 | pub mod i18n; 6 | pub mod ui; 7 | pub mod utils; 8 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Emmanuele Bassi 2 | # SPDX-License-Identifier: CC0-1.0 3 | [files] 4 | extend-exclude = ["LICENSES/*", "po/*"] -------------------------------------------------------------------------------- /src/ui/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod double_graph_box; 2 | pub mod graph; 3 | pub mod graph_box; 4 | pub mod stack_sidebar; 5 | pub mod stack_sidebar_item; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | build/ 3 | _build/ 4 | builddir/ 5 | build-aux/app 6 | build-aux/.flatpak-builder/ 7 | src/config.rs 8 | *.ui.in~ 9 | *.ui~ 10 | .flatpak/ 11 | .flatpak-builder/ 12 | .vscode/ 13 | vendor 14 | -------------------------------------------------------------------------------- /po/its/polkit.loc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option( 2 | 'profile', 3 | type: 'combo', 4 | choices: [ 5 | 'default', 6 | 'development' 7 | ], 8 | value: 'development', 9 | description: 'The build profile for Resources. One of "default" or "development".' 10 | ) 11 | -------------------------------------------------------------------------------- /data/resources/meson.build: -------------------------------------------------------------------------------- 1 | # Resources 2 | resources = gnome.compile_resources( 3 | 'resources', 4 | 'resources.gresource.xml', 5 | gresource_bundle: true, 6 | source_dir: meson.current_build_dir(), 7 | install: true, 8 | install_dir: pkgdatadir, 9 | ) 10 | -------------------------------------------------------------------------------- /po/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext( 2 | 'resources', 3 | args: [ 4 | '--keyword=i18n', 5 | '--keyword=i18n_f', 6 | '--keyword=i18n_k', 7 | '--keyword=ni18n:1,2', 8 | '--keyword=ni18n_f:1,2', 9 | '--keyword=ni18n_k:1,2', 10 | ], 11 | preset: 'glib', 12 | ) 13 | -------------------------------------------------------------------------------- /data/resources/icons/info-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /po/LINGUAS: -------------------------------------------------------------------------------- 1 | # Please keep this list sorted alphabetically 2 | # 3 | ar 4 | bg 5 | bn 6 | ca 7 | cs 8 | de 9 | el 10 | en_GB 11 | es 12 | eu 13 | fa 14 | fi 15 | fr 16 | he 17 | hi 18 | hu 19 | it 20 | ja 21 | ka 22 | nb 23 | nl 24 | oc 25 | pl 26 | pt_BR 27 | ru 28 | sl 29 | sv 30 | tr 31 | uk 32 | uz 33 | zh_CN 34 | zh_TW 35 | -------------------------------------------------------------------------------- /data/icons/meson.build: -------------------------------------------------------------------------------- 1 | install_data( 2 | '@0@.svg'.format(application_id), 3 | install_dir: iconsdir / 'hicolor' / 'scalable' / 'apps' 4 | ) 5 | 6 | install_data( 7 | '@0@-symbolic.svg'.format(base_id), 8 | install_dir: iconsdir / 'hicolor' / 'symbolic' / 'apps', 9 | rename: '@0@-symbolic.svg'.format(application_id) 10 | ) 11 | -------------------------------------------------------------------------------- /po/its/polkit.its: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /src/config.rs.in: -------------------------------------------------------------------------------- 1 | pub const APP_ID: &str = @APP_ID@; 2 | pub const GETTEXT_PACKAGE: &str = @GETTEXT_PACKAGE@; 3 | pub const LOCALEDIR: &str = @LOCALEDIR@; 4 | pub const PKGDATADIR: &str = @PKGDATADIR@; 5 | pub const PROFILE: &str = @PROFILE@; 6 | pub const RESOURCES_FILE: &str = concat!(@PKGDATADIR@, "/resources.gresource"); 7 | pub const VERSION: &str = @VERSION@; 8 | pub const LIBEXECDIR: &str = @LIBEXECDIR@; -------------------------------------------------------------------------------- /lib/process_data/src/meson.build: -------------------------------------------------------------------------------- 1 | cargo_options = [ 2 | '--manifest-path', meson.project_source_root() / 'lib' / 'process_data' / 'Cargo.toml', 3 | ] 4 | cargo_options += [ 5 | '--target-dir', meson.project_build_root() / 'lib' / 'process_data' / 'src', 6 | ] 7 | 8 | test( 9 | 'Cargo tests (process_data)', 10 | cargo, 11 | args: ['test', cargo_options], 12 | timeout: 3600, 13 | env: cargo_env, 14 | ) -------------------------------------------------------------------------------- /data/resources/icons/app-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/icons/flash-storage-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /data/resources/icons/wwan-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/net.nokyan.Resources.desktop.in.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Resources 3 | Comment=Keep an eye on system resources 4 | Type=Application 5 | Exec=resources 6 | Terminal=false 7 | Categories=System; 8 | # Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! 9 | Keywords=System;Resources;Monitor;Processes;Usage;Task;Manager;CPU;RAM;Memory;GPU; 10 | # Translators: Do NOT translate or transliterate this text (this is an icon file name)! 11 | Icon=@icon@ 12 | StartupNotify=true 13 | X-Purism-FormFactor=Workstation;Mobile; 14 | -------------------------------------------------------------------------------- /data/resources/icons/floppy-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/style.css: -------------------------------------------------------------------------------- 1 | progressbar.slim > trough, progressbar.slim > trough > progress { 2 | min-height: 4px; 3 | } 4 | 5 | .resources-columnview { 6 | background-color: @window_bg_color; 7 | } 8 | 9 | .bubble { 10 | background-color: alpha(currentColor, 0.08); 11 | min-width: 32px; 12 | min-height: 32px; 13 | border-radius: 50%; 14 | } 15 | 16 | .big-bubble { 17 | background-color: alpha(currentColor, 0.08); 18 | min-width: 128px; 19 | min-height: 128px; 20 | border-radius: 50%; 21 | } 22 | 23 | .small-graph { 24 | border-radius: 4px; 25 | } 26 | 27 | .graph { 28 | border-radius: 8px; 29 | } 30 | -------------------------------------------------------------------------------- /data/resources/icons/memory-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/icons/system-processes-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.github/workflows/flatpak.yml: -------------------------------------------------------------------------------- 1 | name: Flatpak Build Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | flatpak: 13 | runs-on: ubuntu-latest 14 | container: 15 | image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-48 16 | options: --privileged 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 20 | with: 21 | manifest-path: build-aux/net.nokyan.Resources.Devel.json 22 | run-tests: true 23 | cache-key: flatpak-builder-${{ github.sha }} 24 | upload-artifact: false 25 | -------------------------------------------------------------------------------- /data/resources/icons/search-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/icons/slip-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/resources/icons/ethernet-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/resources/icons/infiniband-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/resources/icons/virtual-ethernet-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/resources/icons/unknown-network-type-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/resources/icons/gpu-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/icons/options-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/ui/widgets/stack_sidebar.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | -------------------------------------------------------------------------------- /data/net.nokyan.Resources.policy.in.in: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | nokyan 7 | net.nokyan.Resources 8 | 9 | Control Process 10 | Authentication is required to control superuser’s or other users’ processes 11 | 12 | no 13 | no 14 | auth_admin_keep 15 | 16 | @libexecdir@/resources-kill 17 | 18 | -------------------------------------------------------------------------------- /data/resources/icons/hdd-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/icons/battery-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/resources/icons/unknown-drive-type-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /lib/process_data/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "process-data" 3 | version = "1.9.1" 4 | authors = ["nokyan "] 5 | edition = "2024" 6 | rust-version = "1.85.0" 7 | homepage = "https://apps.gnome.org/app/net.nokyan.Resources/" 8 | license = "GPL-3.0-or-later" 9 | 10 | [profile.dev] 11 | opt-level = 1 12 | 13 | [profile.release] 14 | codegen-units = 1 15 | lto = true 16 | strip = true 17 | opt-level = 3 18 | 19 | [dependencies] 20 | anyhow = "1.0.100" 21 | glob = "0.3.3" 22 | lazy-regex = "3.4.2" 23 | libc = "0.2.177" 24 | num_cpus = "1.17.0" 25 | nutype = { version = "0.6.2", features = ["serde"] } 26 | nvml-wrapper = "0.11.0" 27 | serde = { version = "1.0.228", features = ["serde_derive"] } 28 | syscalls = { version = "0.7.0", features = ["all"] } 29 | thiserror = "2.0.17" 30 | unescape = "0.1.0" 31 | uzers = "0.12.1" 32 | 33 | [dev-dependencies] 34 | pretty_assertions = "1.4.1" 35 | -------------------------------------------------------------------------------- /data/resources/icons/nvme-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/icons/ssd-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/ui/widgets/process_name_cell.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 25 | -------------------------------------------------------------------------------- /data/resources/ui/widgets/application_name_cell.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 25 | -------------------------------------------------------------------------------- /data/resources/icons/processor-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/bin/resources-kill.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use nix::{sys::signal, unistd::Pid}; 4 | 5 | fn main() { 6 | if let Some(pid) = env::args().nth(1).and_then(|s| s.trim().parse().ok()) { 7 | if let Some(arg) = env::args().nth(2) { 8 | let signal = match arg.as_str() { 9 | "STOP" => signal::Signal::SIGSTOP, 10 | "CONT" => signal::Signal::SIGCONT, 11 | "TERM" => signal::Signal::SIGTERM, 12 | "KILL" => signal::Signal::SIGKILL, 13 | _ => std::process::exit(254), 14 | }; 15 | let result = signal::kill(Pid::from_raw(pid), Some(signal)); 16 | if let Err(errno) = result { 17 | match errno { 18 | nix::errno::Errno::UnknownErrno => std::process::exit(253), 19 | _ => std::process::exit(errno as i32), 20 | }; 21 | } 22 | std::process::exit(0); 23 | } 24 | } 25 | std::process::exit(255); 26 | } 27 | -------------------------------------------------------------------------------- /data/resources/icons/bluetooth-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/resources/icons/shell-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /resources.doap: -------------------------------------------------------------------------------- 1 | 2 | 3 | Resources 4 | Keep an eye on system resources 5 | 6 | 7 | Rust 8 | GTK 4 9 | Libadwaita 10 | 11 | 12 | nokyan 13 | 14 | 15 | 16 | 17 | nokyan 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /data/resources/icons/cd-dvd-bluray-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/utils/os.rs: -------------------------------------------------------------------------------- 1 | use lazy_regex::{Lazy, Regex, lazy_regex}; 2 | use log::trace; 3 | 4 | use crate::utils::read_parsed; 5 | 6 | use super::IS_FLATPAK; 7 | 8 | const PATH_OS_RELEASE: &str = "/etc/os-release"; 9 | const PATH_OS_RELEASE_FLATPAK: &str = "/run/host/etc/os-release"; 10 | const PATH_KERNEL_VERSION: &str = "/proc/sys/kernel/osrelease"; 11 | 12 | static RE_PRETTY_NAME: Lazy = lazy_regex!("PRETTY_NAME=\"(.*)\""); 13 | 14 | pub struct OsInfo { 15 | pub name: Option, 16 | pub kernel_version: Option, 17 | } 18 | 19 | impl OsInfo { 20 | pub fn get() -> Self { 21 | let os_path = if *IS_FLATPAK { 22 | PATH_OS_RELEASE_FLATPAK 23 | } else { 24 | PATH_OS_RELEASE 25 | }; 26 | 27 | trace!("Path for the os-release file is determined to be `{os_path}`"); 28 | 29 | let name = RE_PRETTY_NAME 30 | .captures(&read_parsed::(os_path).unwrap_or_default()) 31 | .and_then(|captures| captures.get(1)) 32 | .map(|capture| capture.as_str().trim().to_string()); 33 | 34 | let kernel_version = read_parsed(PATH_KERNEL_VERSION).ok(); 35 | 36 | OsInfo { 37 | name, 38 | kernel_version, 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /data/resources/icons/zram-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | extern crate paste; 2 | 3 | #[macro_export] 4 | macro_rules! gstring_getter_setter { 5 | ($($gstring_name:ident),*) => { 6 | $( 7 | pub fn $gstring_name(&self) -> glib::GString { 8 | let $gstring_name = self.$gstring_name.take(); 9 | self.$gstring_name.set($gstring_name.clone()); 10 | $gstring_name 11 | } 12 | 13 | paste::paste! { 14 | pub fn [](&self, $gstring_name: &str) { 15 | self.$gstring_name.set(glib::GString::from($gstring_name)); 16 | } 17 | } 18 | )* 19 | }; 20 | } 21 | 22 | #[macro_export] 23 | macro_rules! gstring_option_getter_setter { 24 | ($($gstring_name:ident),*) => { 25 | $( 26 | pub fn $gstring_name(&self) -> Option { 27 | let $gstring_name = self.$gstring_name.take(); 28 | self.$gstring_name.set($gstring_name.clone()); 29 | $gstring_name 30 | } 31 | 32 | paste::paste! { 33 | pub fn [](&self, $gstring_name: Option<&str>) { 34 | self.$gstring_name.set($gstring_name.map(glib::GString::from)); 35 | } 36 | } 37 | )* 38 | }; 39 | } 40 | 41 | pub mod dialogs; 42 | pub mod pages; 43 | pub mod widgets; 44 | pub mod window; 45 | -------------------------------------------------------------------------------- /src/bin/resources-processes.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use process_data::ProcessData; 3 | use ron::ser::PrettyConfig; 4 | use std::io::{Read, Write}; 5 | 6 | use clap::Parser; 7 | 8 | #[derive(Parser, Debug)] 9 | #[command(version, about)] 10 | struct Args { 11 | /// Output once and then exit 12 | #[arg(short, long, default_value_t = false)] 13 | once: bool, 14 | 15 | /// Use Rusty Object Notation (use this only for debugging this binary on its own, Resources won't be able to decode RON) 16 | #[arg(short, long, default_value_t = false)] 17 | ron: bool, 18 | } 19 | 20 | fn main() -> Result<()> { 21 | let args = Args::parse(); 22 | 23 | if args.once { 24 | output(args.ron)?; 25 | return Ok(()); 26 | } 27 | 28 | loop { 29 | let mut buffer = [0; 1]; 30 | 31 | std::io::stdin().read_exact(&mut buffer)?; 32 | 33 | output(args.ron)?; 34 | } 35 | } 36 | 37 | fn output(ron: bool) -> Result<()> { 38 | let data = ProcessData::all_process_data()?; 39 | 40 | let encoded = if ron { 41 | ron::ser::to_string_pretty(&data, PrettyConfig::default())? 42 | .as_bytes() 43 | .to_vec() 44 | } else { 45 | rmp_serde::to_vec(&data)? 46 | }; 47 | 48 | let len_byte_array = encoded.len().to_le_bytes(); 49 | 50 | let stdout = std::io::stdout(); 51 | let mut handle = stdout.lock(); 52 | 53 | handle.write_all(&len_byte_array)?; 54 | 55 | handle.write_all(&encoded)?; 56 | 57 | handle.flush()?; 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /po/POTFILES.in: -------------------------------------------------------------------------------- 1 | # List of source files containing translatable strings. 2 | # Please keep this file sorted alphabetically. 3 | 4 | data/net.nokyan.Resources.desktop.in.in 5 | data/net.nokyan.Resources.metainfo.xml.in.in 6 | data/net.nokyan.Resources.policy.in.in 7 | 8 | data/resources/ui/dialogs/app_dialog.ui 9 | data/resources/ui/dialogs/process_dialog.ui 10 | data/resources/ui/dialogs/process_options_dialog.ui 11 | data/resources/ui/dialogs/settings_dialog.ui 12 | data/resources/ui/pages/applications.ui 13 | data/resources/ui/pages/battery.ui 14 | data/resources/ui/pages/cpu.ui 15 | data/resources/ui/pages/drive.ui 16 | data/resources/ui/pages/gpu.ui 17 | data/resources/ui/pages/memory.ui 18 | data/resources/ui/pages/network.ui 19 | data/resources/ui/pages/npu.ui 20 | data/resources/ui/pages/processes.ui 21 | data/resources/ui/shortcuts.ui 22 | data/resources/ui/window.ui 23 | 24 | src/application.rs 25 | src/ui/dialogs/app_dialog.rs 26 | src/ui/dialogs/process_dialog.rs 27 | src/ui/dialogs/process_options_dialog.rs 28 | src/ui/pages/applications/application_entry.rs 29 | src/ui/pages/applications/mod.rs 30 | src/ui/pages/battery.rs 31 | src/ui/pages/cpu.rs 32 | src/ui/pages/drive.rs 33 | src/ui/pages/gpu.rs 34 | src/ui/pages/memory.rs 35 | src/ui/pages/mod.rs 36 | src/ui/pages/network.rs 37 | src/ui/pages/processes/mod.rs 38 | src/ui/pages/processes/process_entry.rs 39 | src/ui/window.rs 40 | src/utils/app.rs 41 | src/utils/battery.rs 42 | src/utils/units.rs 43 | src/utils/drive.rs 44 | src/utils/gpu/mod.rs 45 | src/utils/link/mod.rs 46 | src/utils/network.rs 47 | src/utils/npu/mod.rs 48 | src/utils/units.rs 49 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "resources" 3 | version = "1.9.1" 4 | authors = ["nokyan "] 5 | edition = "2024" 6 | rust-version = "1.85.0" 7 | homepage = "https://apps.gnome.org/app/net.nokyan.Resources/" 8 | license = "GPL-3.0-or-later" 9 | 10 | [profile.dev] 11 | opt-level = 1 12 | 13 | [profile.release] 14 | codegen-units = 1 15 | lto = true 16 | strip = true 17 | opt-level = 3 18 | 19 | [dependencies] 20 | adw = { version = "0.8.0", features = ["v1_8"], package = "libadwaita" } 21 | anyhow = { version = "1.0.100", features = ["backtrace"] } 22 | async-channel = "2.5.0" 23 | clap = { version = "4.5.51", features = ["derive"] } 24 | gettext-rs = { version = "0.7.7", features = ["gettext-system"] } 25 | glob = "0.3.3" 26 | gtk = { version = "0.10.2", features = ["v4_12"], package = "gtk4" } 27 | lazy-regex = "3.4.1" 28 | libc = { version = "0.2.177", features = ["extra_traits"] } 29 | log = "0.4.28" 30 | neli-wifi = { version = "0.6.1", features = ["default"] } 31 | nix = { version = "0.30.0", default-features = false, features = [ 32 | "signal", 33 | "sched", 34 | ] } 35 | num_cpus = "1.17.0" 36 | nvml-wrapper = "0.11.0" 37 | paste = "1.0.15" 38 | path-dedot = "3.1.1" 39 | plotters = { version = "0.3.7", default-features = false, features = [ 40 | "area_series", 41 | ] } 42 | plotters-cairo = "0.8.0" 43 | pretty_env_logger = "0.5" 44 | process-data = { path = "lib/process_data" } 45 | rmp-serde = "1.3.0" 46 | ron = "0.12.0" 47 | rust-ini = "0.21.3" 48 | strum = "0.27.2" 49 | strum_macros = "0.27.2" 50 | sysconf = "0.3.4" 51 | 52 | [dev-dependencies] 53 | pretty_assertions = "1.4.1" 54 | -------------------------------------------------------------------------------- /data/resources/icons/wlan-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/bin/resources-adjust.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::PathBuf}; 2 | 3 | use nix::{ 4 | sched::{CpuSet, sched_setaffinity}, 5 | unistd::Pid, 6 | }; 7 | 8 | fn main() { 9 | if let Some(pid) = env::args().nth(1).and_then(|s| s.trim().parse().ok()) { 10 | if let Some(nice) = env::args().nth(2).and_then(|s| s.trim().parse().ok()) { 11 | if let Some(mask) = env::args().nth(3) { 12 | let mut cpu_set = CpuSet::new(); 13 | 14 | for (i, c) in mask.chars().enumerate() { 15 | if c == '1' { 16 | cpu_set.set(i).unwrap_or_default(); 17 | } 18 | } 19 | 20 | adjust(pid, nice, &cpu_set); 21 | 22 | // find tasks that belong to this process 23 | let tasks_path = PathBuf::from("/proc/").join(pid.to_string()).join("task"); 24 | for entry in std::fs::read_dir(tasks_path).unwrap().flatten() { 25 | let thread_id = entry.file_name().to_string_lossy().parse().unwrap(); 26 | 27 | adjust(thread_id, nice, &cpu_set); 28 | } 29 | 30 | std::process::exit(0) 31 | } 32 | } 33 | } 34 | std::process::exit(255); 35 | } 36 | 37 | fn adjust(id: i32, nice: i32, cpu_set: &CpuSet) { 38 | unsafe { 39 | libc::setpriority(libc::PRIO_PROCESS, id as u32, nice); 40 | }; 41 | 42 | let error = std::io::Error::last_os_error() 43 | .raw_os_error() 44 | .unwrap_or_default(); 45 | 46 | if error != 0 { 47 | std::process::exit(error) 48 | } 49 | 50 | let _ = sched_setaffinity(Pid::from_raw(id), cpu_set); 51 | } 52 | -------------------------------------------------------------------------------- /data/resources/icons/vpn-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/ui/pages/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::LazyLock}; 2 | 3 | use process_data::Niceness; 4 | 5 | use crate::i18n::pi18n; 6 | 7 | pub mod applications; 8 | pub mod battery; 9 | pub mod cpu; 10 | pub mod drive; 11 | pub mod gpu; 12 | pub mod memory; 13 | pub mod network; 14 | pub mod npu; 15 | pub mod processes; 16 | 17 | const APPLICATIONS_PRIMARY_ORD: u32 = 0; 18 | const PROCESSES_PRIMARY_ORD: u32 = 1; 19 | const CPU_PRIMARY_ORD: u32 = 2; 20 | const MEMORY_PRIMARY_ORD: u32 = 3; 21 | const GPU_PRIMARY_ORD: u32 = 4; 22 | const NPU_PRIMARY_ORD: u32 = 5; 23 | const DRIVE_PRIMARY_ORD: u32 = 6; 24 | const NETWORK_PRIMARY_ORD: u32 = 7; 25 | const BATTERY_PRIMARY_ORD: u32 = 8; 26 | 27 | pub static NICE_TO_LABEL: LazyLock> = LazyLock::new(|| { 28 | let mut hash_map = HashMap::new(); 29 | 30 | for i in -20..=-8 { 31 | hash_map.insert( 32 | Niceness::try_new(i).unwrap(), 33 | (pi18n("process priority", "Very High"), 0), 34 | ); 35 | } 36 | 37 | for i in -7..=-3 { 38 | hash_map.insert( 39 | Niceness::try_new(i).unwrap(), 40 | (pi18n("process priority", "High"), 1), 41 | ); 42 | } 43 | 44 | for i in -2..=2 { 45 | hash_map.insert( 46 | Niceness::try_new(i).unwrap(), 47 | (pi18n("process priority", "Normal"), 2), 48 | ); 49 | } 50 | 51 | for i in 3..=6 { 52 | hash_map.insert( 53 | Niceness::try_new(i).unwrap(), 54 | (pi18n("process priority", "Low"), 3), 55 | ); 56 | } 57 | 58 | for i in 7..=19 { 59 | hash_map.insert( 60 | Niceness::try_new(i).unwrap(), 61 | (pi18n("process priority", "Very Low"), 4), 62 | ); 63 | } 64 | 65 | hash_map 66 | }); 67 | -------------------------------------------------------------------------------- /src/utils/link/mod.rs: -------------------------------------------------------------------------------- 1 | mod pcie; 2 | mod sata; 3 | mod usb; 4 | mod wifi; 5 | 6 | use crate::i18n::i18n; 7 | use crate::utils::link::pcie::PcieLinkData; 8 | use crate::utils::link::sata::SataSpeed; 9 | use crate::utils::link::usb::UsbSpeed; 10 | use crate::utils::link::wifi::WifiGeneration; 11 | use anyhow::Result; 12 | use std::fmt::{Display, Formatter}; 13 | 14 | #[derive(Debug, Default)] 15 | pub enum Link { 16 | Pcie(LinkData), 17 | Sata(LinkData), 18 | Usb(LinkData), 19 | Wifi(LinkData), 20 | #[default] 21 | Unknown, 22 | } 23 | 24 | #[derive(Debug)] 25 | pub struct LinkData { 26 | pub current: T, 27 | pub max: Result, 28 | } 29 | 30 | impl Display for LinkData 31 | where 32 | T: Display, 33 | T: PartialEq, 34 | { 35 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 36 | let has_different_max = { 37 | if let Ok(max) = self.max.as_ref() { 38 | self.current != *max 39 | } else { 40 | false 41 | } 42 | }; 43 | if has_different_max { 44 | write!(f, "{} / {}", self.current, self.max.as_ref().unwrap()) 45 | } else { 46 | write!(f, "{}", self.current) 47 | } 48 | } 49 | } 50 | 51 | impl Display for Link { 52 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 53 | write!( 54 | f, 55 | "{}", 56 | match self { 57 | Link::Pcie(data) => data.to_string(), 58 | Link::Sata(data) => data.to_string(), 59 | Link::Usb(data) => data.to_string(), 60 | Link::Wifi(data) => data.to_string(), 61 | Link::Unknown => i18n("N/A"), 62 | } 63 | ) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /hooks/pre-commit.hook: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Source: https://gitlab.gnome.org/GNOME/fractal/blob/master/hooks/pre-commit.hook 3 | 4 | install_rustfmt() { 5 | if ! which rustup &> /dev/null; then 6 | curl https://sh.rustup.rs -sSf | sh -s -- -y 7 | export PATH=$PATH:$HOME/.cargo/bin 8 | if ! which rustup &> /dev/null; then 9 | echo "Failed to install rustup. Performing the commit without style checking." 10 | exit 0 11 | fi 12 | fi 13 | 14 | if ! rustup component list|grep rustfmt &> /dev/null; then 15 | echo "Installing rustfmt…" 16 | rustup component add rustfmt 17 | fi 18 | } 19 | 20 | if ! which cargo >/dev/null 2>&1 || ! cargo fmt --help >/dev/null 2>&1; then 21 | echo "Unable to check the project’s code style, because rustfmt could not be run." 22 | 23 | if [ ! -t 1 ]; then 24 | # No input is possible 25 | echo "Performing commit." 26 | exit 0 27 | fi 28 | 29 | echo "" 30 | echo "y: Install rustfmt via rustup" 31 | echo "n: Don't install rustfmt and perform the commit" 32 | echo "Q: Don't install rustfmt and abort the commit" 33 | 34 | echo "" 35 | while true 36 | do 37 | echo -n "Install rustfmt via rustup? [y/n/Q]: "; read yn < /dev/tty 38 | case $yn in 39 | [Yy]* ) install_rustfmt; break;; 40 | [Nn]* ) echo "Performing commit."; exit 0;; 41 | [Qq]* | "" ) echo "Aborting commit."; exit -1 >/dev/null 2>&1;; 42 | * ) echo "Invalid input";; 43 | esac 44 | done 45 | 46 | fi 47 | 48 | echo "--Checking style--" 49 | cargo fmt --all -- --check 50 | if test $? != 0; then 51 | echo "--Checking style fail--" 52 | echo "Please fix the above issues, either manually or by running: cargo fmt --all" 53 | 54 | exit -1 55 | else 56 | echo "--Checking style pass--" 57 | fi 58 | -------------------------------------------------------------------------------- /data/resources/icons/emmc-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 Feature Request" 2 | description: Suggest an idea for this project 3 | labels: ["enhancement"] 4 | body: 5 | - type: checkboxes 6 | attributes: 7 | label: Is there an existing issue for this? 8 | description: Please search to see if an issue already exists for the bug you encountered. 9 | options: 10 | - label: I have searched the existing issues 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: Is your feature request related to a problem? Please describe. 15 | description: A clear and concise description of what the problem is. 16 | placeholder: I'm always frustrated when I have to press this small button. 17 | validations: 18 | required: false 19 | - type: textarea 20 | attributes: 21 | label: Describe the solution you'd like 22 | description: A clear and concise description of what you want to happen. 23 | placeholder: Make this button bigger 24 | validations: 25 | required: false 26 | - type: textarea 27 | attributes: 28 | label: Describe alternatives you've considered 29 | description: A clear and concise description of any alternative solutions or features you've considered. 30 | placeholder: I wanted to but bigger display, but it would be too expensive. 31 | validations: 32 | required: false 33 | - type: textarea 34 | attributes: 35 | label: Additional context 36 | description: | 37 | Add any other context or screenshots about the feature request here. 38 | 39 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 40 | validations: 41 | required: false 42 | - type: markdown 43 | attributes: 44 | value: | 45 | --- 46 | The more thumbs up 👍 your idea gets, the more likely it is that we'll implement it. ✨ 47 | -------------------------------------------------------------------------------- /data/resources/icons/select-all-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 34 | 37 | 40 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /data/resources/icons/bridge-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/icons/vm-bridge-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/icons/docker-bridge-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/icons/loop-device-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/icons/ram-disk-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/ui/widgets/graph_box.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 55 | -------------------------------------------------------------------------------- /src/utils/npu/other.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use process_data::pci_slot::PciSlot; 3 | 4 | use std::path::{Path, PathBuf}; 5 | 6 | use crate::utils::pci::Device; 7 | 8 | use super::NpuImpl; 9 | 10 | #[derive(Debug, Clone, Default)] 11 | 12 | pub struct OtherNpu { 13 | pub device: Option<&'static Device>, 14 | pub pci_slot: PciSlot, 15 | pub driver: String, 16 | sysfs_path: PathBuf, 17 | first_hwmon_path: Option, 18 | } 19 | 20 | impl OtherNpu { 21 | pub fn new( 22 | device: Option<&'static Device>, 23 | pci_slot: PciSlot, 24 | driver: String, 25 | sysfs_path: PathBuf, 26 | first_hwmon_path: Option, 27 | ) -> Self { 28 | Self { 29 | device, 30 | pci_slot, 31 | driver, 32 | sysfs_path, 33 | first_hwmon_path, 34 | } 35 | } 36 | } 37 | 38 | impl NpuImpl for OtherNpu { 39 | fn device(&self) -> Option<&'static Device> { 40 | self.device 41 | } 42 | 43 | fn pci_slot(&self) -> PciSlot { 44 | self.pci_slot 45 | } 46 | 47 | fn driver(&self) -> &str { 48 | &self.driver 49 | } 50 | 51 | fn sysfs_path(&self) -> &Path { 52 | &self.sysfs_path 53 | } 54 | 55 | fn first_hwmon(&self) -> Option<&Path> { 56 | self.first_hwmon_path.as_deref() 57 | } 58 | 59 | fn name(&self) -> Result { 60 | self.drm_name() 61 | } 62 | 63 | fn usage(&self) -> Result { 64 | self.drm_usage().map(|usage| usage as f64 / 100.0) 65 | } 66 | 67 | fn used_vram(&self) -> Result { 68 | self.drm_used_memory().map(|usage| usage as usize) 69 | } 70 | 71 | fn total_vram(&self) -> Result { 72 | self.drm_total_memory().map(|usage| usage as usize) 73 | } 74 | 75 | fn temperature(&self) -> Result { 76 | self.hwmon_temperature() 77 | } 78 | 79 | fn power_usage(&self) -> Result { 80 | self.hwmon_power_usage() 81 | } 82 | 83 | fn core_frequency(&self) -> Result { 84 | self.hwmon_core_frequency() 85 | } 86 | 87 | fn memory_frequency(&self) -> Result { 88 | self.hwmon_memory_frequency() 89 | } 90 | 91 | fn power_cap(&self) -> Result { 92 | self.hwmon_power_cap() 93 | } 94 | 95 | fn power_cap_max(&self) -> Result { 96 | self.hwmon_power_cap_max() 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /data/resources/icons/device-settings-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /data/resources/icons/generic-process-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'resources', 3 | 'rust', 4 | version: '1.9.1', 5 | meson_version: '>= 0.59', 6 | ) 7 | 8 | i18n = import('i18n') 9 | gnome = import('gnome') 10 | 11 | base_id = 'net.nokyan.Resources' 12 | 13 | dependency('glib-2.0', version: '>= 2.66') 14 | dependency('gio-2.0', version: '>= 2.66') 15 | dependency('gtk4', version: '>= 4.12.0') 16 | dependency('libadwaita-1', version: '>= 1.8.0') 17 | 18 | glib_compile_resources = find_program('glib-compile-resources', required: true) 19 | glib_compile_schemas = find_program('glib-compile-schemas', required: true) 20 | desktop_file_validate = find_program('desktop-file-validate', required: false) 21 | appstreamcli = find_program('appstreamcli', required: false) 22 | cargo = find_program('cargo', required: true) 23 | 24 | version = meson.project_version() 25 | 26 | prefix = get_option('prefix') 27 | bindir = prefix / get_option('bindir') 28 | localedir = prefix / get_option('localedir') 29 | libexecdir = prefix / get_option('libexecdir') / meson.project_name() 30 | 31 | datadir = prefix / get_option('datadir') 32 | pkgdatadir = datadir / meson.project_name() 33 | iconsdir = datadir / 'icons' 34 | podir = meson.project_source_root() / 'po' 35 | gettext_package = meson.project_name() 36 | 37 | if get_option('profile') == 'development' 38 | profile = 'Devel' 39 | vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD').stdout().strip() 40 | vcs_branch = run_command('git', 'rev-parse', '--abbrev-ref', 'HEAD').stdout().strip() 41 | if vcs_tag == '' or vcs_branch == '' 42 | version_suffix = '-devel' 43 | else 44 | version_suffix = '-@0@@@1@'.format(vcs_branch, vcs_tag) 45 | endif 46 | application_id = '@0@.@1@'.format(base_id, profile) 47 | else 48 | profile = '' 49 | version_suffix = '' 50 | application_id = base_id 51 | endif 52 | 53 | meson.add_dist_script( 54 | 'build-aux/dist-vendor.sh', 55 | meson.project_build_root() / 'meson-dist' / meson.project_name() 56 | + '-' 57 | + version, 58 | meson.project_source_root(), 59 | ) 60 | 61 | if get_option('profile') == 'development' 62 | # Setup pre-commit hook for ensuring coding style is always consistent 63 | message('Setting up git pre-commit hook..') 64 | run_command('cp', '-f', 'hooks/pre-commit.hook', '.git/hooks/pre-commit') 65 | endif 66 | 67 | subdir('data') 68 | subdir('po') 69 | subdir('src') 70 | subdir('lib/process_data/src') 71 | 72 | gnome.post_install( 73 | gtk_update_icon_cache: true, 74 | glib_compile_schemas: true, 75 | update_desktop_database: true, 76 | ) -------------------------------------------------------------------------------- /data/resources/ui/widgets/stack_sidebar_item.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 72 | -------------------------------------------------------------------------------- /data/resources/icons/raid-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/utils/gpu/other.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, bail}; 2 | use process_data::gpu_usage::GpuIdentifier; 3 | 4 | use std::path::{Path, PathBuf}; 5 | 6 | use crate::utils::pci::Device; 7 | 8 | use super::GpuImpl; 9 | 10 | #[derive(Debug, Clone, Default)] 11 | 12 | pub struct OtherGpu { 13 | pub device: Option<&'static Device>, 14 | pub gpu_identifier: GpuIdentifier, 15 | pub driver: String, 16 | sysfs_path: PathBuf, 17 | first_hwmon_path: Option, 18 | } 19 | 20 | impl OtherGpu { 21 | pub fn new( 22 | device: Option<&'static Device>, 23 | gpu_identifier: GpuIdentifier, 24 | driver: String, 25 | sysfs_path: PathBuf, 26 | first_hwmon_path: Option, 27 | ) -> Self { 28 | Self { 29 | device, 30 | gpu_identifier, 31 | driver, 32 | sysfs_path, 33 | first_hwmon_path, 34 | } 35 | } 36 | } 37 | 38 | impl GpuImpl for OtherGpu { 39 | fn device(&self) -> Option<&'static Device> { 40 | self.device 41 | } 42 | 43 | fn gpu_identifier(&self) -> GpuIdentifier { 44 | self.gpu_identifier 45 | } 46 | 47 | fn driver(&self) -> &str { 48 | &self.driver 49 | } 50 | 51 | fn sysfs_path(&self) -> &Path { 52 | &self.sysfs_path 53 | } 54 | 55 | fn first_hwmon(&self) -> Option<&Path> { 56 | self.first_hwmon_path.as_deref() 57 | } 58 | 59 | fn name(&self) -> Result { 60 | self.drm_name() 61 | } 62 | 63 | fn usage(&self) -> Result { 64 | self.drm_usage().map(|usage| usage as f64 / 100.0) 65 | } 66 | 67 | fn encode_usage(&self) -> Result { 68 | bail!("encode usage not implemented for other") 69 | } 70 | 71 | fn decode_usage(&self) -> Result { 72 | bail!("decode usage not implemented for other") 73 | } 74 | 75 | fn combined_media_engine(&self) -> Result { 76 | bail!("can't know for other GPUs") 77 | } 78 | 79 | fn used_vram(&self) -> Result { 80 | self.drm_used_vram().map(|usage| usage as u64) 81 | } 82 | 83 | fn total_vram(&self) -> Result { 84 | self.drm_total_vram().map(|usage| usage as u64) 85 | } 86 | 87 | fn temperature(&self) -> Result { 88 | self.hwmon_temperature() 89 | } 90 | 91 | fn power_usage(&self) -> Result { 92 | self.hwmon_power_usage() 93 | } 94 | 95 | fn core_frequency(&self) -> Result { 96 | self.hwmon_core_frequency() 97 | } 98 | 99 | fn vram_frequency(&self) -> Result { 100 | self.hwmon_vram_frequency() 101 | } 102 | 103 | fn power_cap(&self) -> Result { 104 | self.hwmon_power_cap() 105 | } 106 | 107 | fn power_cap_max(&self) -> Result { 108 | self.hwmon_power_cap_max() 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/ui/widgets/graph_box.rs: -------------------------------------------------------------------------------- 1 | use adw::{prelude::*, subclass::prelude::*}; 2 | use gtk::glib; 3 | use log::trace; 4 | 5 | use crate::config::PROFILE; 6 | 7 | use super::graph::ResGraph; 8 | 9 | mod imp { 10 | use crate::ui::widgets::graph::ResGraph; 11 | 12 | use super::*; 13 | 14 | use gtk::CompositeTemplate; 15 | 16 | #[derive(Debug, CompositeTemplate, Default)] 17 | #[template(resource = "/net/nokyan/Resources/ui/widgets/graph_box.ui")] 18 | pub struct ResGraphBox { 19 | #[template_child] 20 | pub graph: TemplateChild, 21 | #[template_child] 22 | pub title_label: TemplateChild, 23 | #[template_child] 24 | pub info_label: TemplateChild, 25 | } 26 | 27 | #[glib::object_subclass] 28 | impl ObjectSubclass for ResGraphBox { 29 | const NAME: &'static str = "ResGraphBox"; 30 | type Type = super::ResGraphBox; 31 | type ParentType = adw::PreferencesRow; 32 | 33 | fn class_init(klass: &mut Self::Class) { 34 | Self::bind_template(klass); 35 | } 36 | 37 | // You must call `Widget`'s `init_template()` within `instance_init()`. 38 | fn instance_init(obj: &glib::subclass::InitializingObject) { 39 | obj.init_template(); 40 | } 41 | } 42 | 43 | impl ObjectImpl for ResGraphBox { 44 | fn constructed(&self) { 45 | self.parent_constructed(); 46 | let obj = self.obj(); 47 | 48 | // Devel Profile 49 | if PROFILE == "Devel" { 50 | obj.add_css_class("devel"); 51 | } 52 | } 53 | } 54 | 55 | impl WidgetImpl for ResGraphBox {} 56 | 57 | impl ListBoxRowImpl for ResGraphBox {} 58 | 59 | impl PreferencesRowImpl for ResGraphBox {} 60 | } 61 | 62 | glib::wrapper! { 63 | pub struct ResGraphBox(ObjectSubclass) 64 | @extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, 65 | @implements gtk::Buildable, gtk::ConstraintTarget, gtk::Accessible, gtk::Actionable; 66 | } 67 | 68 | impl Default for ResGraphBox { 69 | fn default() -> Self { 70 | Self::new() 71 | } 72 | } 73 | 74 | impl ResGraphBox { 75 | pub fn new() -> Self { 76 | trace!("Creating ResGraphBox GObject…"); 77 | 78 | glib::Object::new::() 79 | } 80 | 81 | pub fn graph(&self) -> ResGraph { 82 | self.imp().graph.get() 83 | } 84 | 85 | pub fn set_title_label(&self, str: &str) { 86 | let imp = self.imp(); 87 | imp.title_label.set_label(str); 88 | } 89 | 90 | pub fn set_subtitle(&self, str: &str) { 91 | let imp = self.imp(); 92 | imp.info_label.set_label(str); 93 | } 94 | 95 | pub fn set_tooltip(&self, str: Option<&str>) { 96 | let imp = self.imp(); 97 | imp.info_label.set_tooltip_text(str); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/utils/gpu/v3d.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, bail}; 2 | use process_data::gpu_usage::GpuIdentifier; 3 | 4 | use std::path::{Path, PathBuf}; 5 | 6 | use crate::utils::{pci::Device, read_parsed}; 7 | 8 | use super::GpuImpl; 9 | 10 | #[derive(Debug, Clone, Default)] 11 | 12 | pub struct V3dGpu { 13 | pub device: Option<&'static Device>, 14 | pub gpu_identifier: GpuIdentifier, 15 | pub driver: String, 16 | sysfs_path: PathBuf, 17 | first_hwmon_path: Option, 18 | } 19 | 20 | impl V3dGpu { 21 | pub fn new( 22 | device: Option<&'static Device>, 23 | gpu_identifier: GpuIdentifier, 24 | driver: String, 25 | sysfs_path: PathBuf, 26 | first_hwmon_path: Option, 27 | ) -> Self { 28 | Self { 29 | device, 30 | gpu_identifier, 31 | driver, 32 | sysfs_path, 33 | first_hwmon_path, 34 | } 35 | } 36 | } 37 | 38 | impl GpuImpl for V3dGpu { 39 | fn device(&self) -> Option<&'static Device> { 40 | self.device 41 | } 42 | 43 | fn gpu_identifier(&self) -> GpuIdentifier { 44 | self.gpu_identifier 45 | } 46 | 47 | fn driver(&self) -> &str { 48 | &self.driver 49 | } 50 | 51 | fn sysfs_path(&self) -> &Path { 52 | &self.sysfs_path 53 | } 54 | 55 | fn first_hwmon(&self) -> Option<&Path> { 56 | self.first_hwmon_path.as_deref() 57 | } 58 | 59 | fn name(&self) -> Result { 60 | self.drm_name() 61 | } 62 | 63 | fn usage(&self) -> Result { 64 | self.drm_usage().map(|usage| usage as f64 / 100.0) 65 | } 66 | 67 | fn encode_usage(&self) -> Result { 68 | bail!("encode usage not implemented for v3d") 69 | } 70 | 71 | fn decode_usage(&self) -> Result { 72 | bail!("decode usage not implemented for v3d") 73 | } 74 | 75 | fn combined_media_engine(&self) -> Result { 76 | Ok(true) 77 | } 78 | 79 | fn used_vram(&self) -> Result { 80 | self.drm_used_vram().map(|usage| usage as u64) 81 | } 82 | 83 | fn total_vram(&self) -> Result { 84 | self.drm_total_vram().map(|usage| usage as u64) 85 | } 86 | 87 | fn temperature(&self) -> Result { 88 | self.hwmon_temperature() 89 | } 90 | 91 | fn power_usage(&self) -> Result { 92 | self.hwmon_power_usage() 93 | } 94 | 95 | fn core_frequency(&self) -> Result { 96 | read_parsed::(self.sysfs_path().join("gt_cur_freq_mhz")).map(|freq| freq * 1_000_000.0) 97 | } 98 | 99 | fn vram_frequency(&self) -> Result { 100 | self.hwmon_vram_frequency() 101 | } 102 | 103 | fn power_cap(&self) -> Result { 104 | self.hwmon_power_cap() 105 | } 106 | 107 | fn power_cap_max(&self) -> Result { 108 | self.hwmon_power_cap_max() 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /data/resources/icons/npu-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | subdir('icons') 2 | subdir('resources') 3 | # Desktop file 4 | desktop_conf = configuration_data() 5 | desktop_conf.set('icon', application_id) 6 | desktop_file = i18n.merge_file( 7 | type: 'desktop', 8 | input: configure_file( 9 | input: '@0@.desktop.in.in'.format(base_id), 10 | output: '@BASENAME@', 11 | configuration: desktop_conf 12 | ), 13 | output: '@0@.desktop'.format(application_id), 14 | po_dir: podir, 15 | install: true, 16 | install_dir: datadir / 'applications' 17 | ) 18 | # Validate Desktop file 19 | if desktop_file_validate.found() 20 | test( 21 | 'validate-desktop', 22 | desktop_file_validate, 23 | args: [ 24 | desktop_file.full_path() 25 | ], 26 | depends: desktop_file, 27 | ) 28 | endif 29 | 30 | # Appdata 31 | appdata_conf = configuration_data() 32 | appdata_conf.set('app-id', application_id) 33 | appdata_conf.set('gettext-package', gettext_package) 34 | appdata_file = i18n.merge_file( 35 | input: configure_file( 36 | input: '@0@.metainfo.xml.in.in'.format(base_id), 37 | output: '@BASENAME@', 38 | configuration: appdata_conf 39 | ), 40 | output: '@0@.metainfo.xml'.format(application_id), 41 | po_dir: podir, 42 | install: true, 43 | install_dir: datadir / 'metainfo' 44 | ) 45 | # Validate Appdata 46 | if appstreamcli.found() 47 | test( 48 | 'validate-appdata', appstreamcli, 49 | args: [ 50 | 'validate', '--no-net', '--explain', appdata_file.full_path() 51 | ], 52 | depends: appdata_file, 53 | ) 54 | endif 55 | 56 | # GSchema 57 | gschema_conf = configuration_data() 58 | gschema_conf.set('app-id', application_id) 59 | gschema_conf.set('gettext-package', gettext_package) 60 | configure_file( 61 | input: '@0@.gschema.xml.in'.format(base_id), 62 | output: '@0@.gschema.xml'.format(application_id), 63 | configuration: gschema_conf, 64 | install: true, 65 | install_dir: datadir / 'glib-2.0' / 'schemas' 66 | ) 67 | 68 | # Validate GSchema 69 | if glib_compile_schemas.found() 70 | test( 71 | 'validate-gschema', glib_compile_schemas, 72 | args: [ 73 | '--strict', '--dry-run', meson.current_build_dir() 74 | ], 75 | ) 76 | endif 77 | 78 | # DBus service 79 | # install_data('net.nokyan.Resources.conf', install_dir : datadir / 'dbus-1' / 'system.d') 80 | 81 | # systemd Service file 82 | #service_conf = configuration_data() 83 | #service_conf.set('bindir', bindir) 84 | #configure_file( 85 | # input: '@0@.service.in'.format(base_id), 86 | # output: '@0@.service'.format(application_id), 87 | # configuration: service_conf, 88 | # install: true, 89 | # install_dir: prefix / 'lib' / 'systemd' / 'system' 90 | #) 91 | 92 | # polkit Policy file 93 | policy_conf = configuration_data() 94 | policy_conf.set('libexecdir', libexecdir) 95 | policy_conf.set('gettext-package', gettext_package) 96 | i18n.merge_file( 97 | input: configure_file( 98 | input: '@0@.policy.in.in'.format(base_id), 99 | output: '@BASENAME@', 100 | configuration: policy_conf, 101 | ), 102 | output: '@0@.policy'.format(application_id), 103 | po_dir: podir, 104 | data_dirs: podir, 105 | install: true, 106 | install_dir: datadir / 'polkit-1' / 'actions' 107 | ) 108 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41E Bug Report" 2 | description: Create a bug report to help us fix it 3 | labels: ["bug"] 4 | body: 5 | - type: checkboxes 6 | attributes: 7 | label: Is there an existing issue for this? 8 | description: Please search to see if an issue already exists for the bug you encountered. 9 | options: 10 | - label: I searched the existing issues and did not find anything similar. 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: Current Behavior 15 | description: A concise description of what you're experiencing. 16 | validations: 17 | required: false 18 | - type: textarea 19 | attributes: 20 | label: Expected Behavior 21 | description: A concise description of what you expected to happen. 22 | validations: 23 | required: false 24 | - type: textarea 25 | attributes: 26 | label: Steps to Reproduce 27 | description: Steps to reproduce the behavior. 28 | placeholder: | 29 | 1. Go to '…' 30 | 2. Click on '…' 31 | 3. Scroll down to '…' 32 | 4. See error 33 | validations: 34 | required: false 35 | - type: textarea 36 | attributes: 37 | label: Debug Logs 38 | description: | 39 | Please run Resources once with debug logs enabled (if possible) and include the terminal output here. This helps us to get hardware and software information in a streamlined way. 40 | You can do this by running `flatpak run --env=RUST_LOG=resources=debug net.nokyan.Resources` in your terminal if you've installed Resources using Flatpak. Otherwise run `RUST_LOG=resources=debug resources`. 41 | Especially during process and app detection, personally identifiable information may be printed in the debug logs, please double-check that there's nothing inside that you don't want to be public. If your issue is unrelated to process/app detection, you can safely omit any messages that start with `DEBUG resources::utils::app`. 42 | value: | 43 |
44 | Expand logs 45 | 46 | 47 | ``` 48 | REPLACE THIS SENTENCE WITH THE TERMINAL OUTPUT OF THE AFOREMENTIONED COMMAND. 49 | ``` 50 |
51 | validations: 52 | required: true 53 | - type: textarea 54 | attributes: 55 | label: Environment 56 | description: | 57 | Please provide information about your environment if you were not able to include debug logs as described above. 58 | placeholder: | 59 | Resources version: 1.9.1 (you can find this in the 'About' dialog) 60 | Package type: Flatpak 61 | Operating system: Ubuntu 22.04 62 | Hardware info: Intel i7-7700k, Nvidia GTX 1080, … 63 | … 64 | render: markdown 65 | validations: 66 | required: false 67 | - type: textarea 68 | attributes: 69 | label: Anything Else? 70 | description: | 71 | Links? References? Anything that will give us more context about the issue you are encountering! 72 | 73 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 74 | validations: 75 | required: false 76 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | global_conf = configuration_data() 2 | global_conf.set_quoted('APP_ID', application_id) 3 | global_conf.set_quoted('PKGDATADIR', pkgdatadir) 4 | global_conf.set_quoted('PROFILE', profile) 5 | global_conf.set_quoted('VERSION', version + version_suffix) 6 | global_conf.set_quoted('GETTEXT_PACKAGE', gettext_package) 7 | global_conf.set_quoted('LOCALEDIR', localedir) 8 | global_conf.set_quoted('LIBEXECDIR', libexecdir) 9 | config = configure_file(input: 'config.rs.in', output: 'config.rs', configuration: global_conf) 10 | # Copy the config.rs output to the source directory. 11 | run_command( 12 | 'cp', 13 | meson.project_build_root() / 'src' / 'config.rs', 14 | meson.project_source_root() / 'src' / 'config.rs', 15 | check: true, 16 | ) 17 | 18 | cargo_options = ['--manifest-path', meson.project_source_root() / 'Cargo.toml'] 19 | cargo_options += ['--target-dir', meson.project_build_root() / 'src'] 20 | 21 | if get_option('profile') == 'default' 22 | cargo_options += ['--release'] 23 | rust_target = 'release' 24 | message('Building in release mode') 25 | else 26 | rust_target = 'debug' 27 | message('Building in debug mode') 28 | endif 29 | 30 | cargo_env = ['CARGO_HOME=' + meson.project_build_root() / 'cargo'] 31 | 32 | test( 33 | 'Cargo tests (main application)', 34 | cargo, 35 | args: ['test', cargo_options], 36 | timeout: 3600, 37 | env: cargo_env, 38 | ) 39 | 40 | cargo_build = custom_target( 41 | 'cargo-build', 42 | depends: resources, 43 | build_by_default: true, 44 | build_always_stale: true, 45 | output: rust_target, 46 | console: true, 47 | command: [ 48 | 'env', 49 | cargo_env, 50 | cargo, 51 | 'build', 52 | cargo_options, 53 | ], 54 | ) 55 | 56 | copy_binary = custom_target( 57 | 'cp-binary', 58 | depends: cargo_build, 59 | build_by_default: true, 60 | build_always_stale: true, 61 | install: true, 62 | install_dir: bindir, 63 | output: meson.project_name(), 64 | command: [ 65 | 'cp', 66 | 'src' / rust_target / meson.project_name(), 67 | '@OUTPUT@', 68 | ], 69 | ) 70 | 71 | copy_kill_binary = custom_target( 72 | 'cp-kill-binary', 73 | depends: cargo_build, 74 | build_by_default: true, 75 | build_always_stale: true, 76 | install: true, 77 | install_dir: libexecdir, 78 | output: meson.project_name() + '-kill', 79 | command: [ 80 | 'cp', 81 | 'src' / rust_target / meson.project_name() + '-kill', 82 | '@OUTPUT@', 83 | ], 84 | ) 85 | 86 | copy_processes_binary = custom_target( 87 | 'cp-processes-binary', 88 | depends: cargo_build, 89 | build_by_default: true, 90 | build_always_stale: true, 91 | install: true, 92 | install_dir: libexecdir, 93 | output: meson.project_name() + '-processes', 94 | command: [ 95 | 'cp', 96 | 'src' / rust_target / meson.project_name() + '-processes', 97 | '@OUTPUT@', 98 | ], 99 | ) 100 | 101 | copy_adjust_binary = custom_target( 102 | 'cp-adjust-binary', 103 | depends: cargo_build, 104 | build_by_default: true, 105 | build_always_stale: true, 106 | install: true, 107 | install_dir: libexecdir, 108 | output: meson.project_name() + '-adjust', 109 | command: [ 110 | 'cp', 111 | 'src' / rust_target / meson.project_name() + '-adjust', 112 | '@OUTPUT@', 113 | ], 114 | ) -------------------------------------------------------------------------------- /src/utils/npu/intel.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use process_data::{pci_slot::PciSlot, unix_as_secs_f64}; 3 | 4 | use std::{ 5 | cell::Cell, 6 | path::{Path, PathBuf}, 7 | }; 8 | 9 | use crate::utils::{pci::Device, read_parsed}; 10 | 11 | use super::NpuImpl; 12 | 13 | #[derive(Debug, Clone, Default)] 14 | 15 | pub struct IntelNpu { 16 | pub device: Option<&'static Device>, 17 | pub pci_slot: PciSlot, 18 | pub driver: String, 19 | sysfs_path: PathBuf, 20 | first_hwmon_path: Option, 21 | last_busy_time_us: Cell, 22 | last_busy_time_timestamp: Cell, 23 | } 24 | 25 | impl IntelNpu { 26 | pub fn new( 27 | device: Option<&'static Device>, 28 | pci_slot: PciSlot, 29 | driver: String, 30 | sysfs_path: PathBuf, 31 | first_hwmon_path: Option, 32 | ) -> Self { 33 | Self { 34 | device, 35 | pci_slot, 36 | driver, 37 | sysfs_path, 38 | first_hwmon_path, 39 | last_busy_time_us: Cell::default(), 40 | last_busy_time_timestamp: Cell::default(), 41 | } 42 | } 43 | } 44 | 45 | impl NpuImpl for IntelNpu { 46 | fn device(&self) -> Option<&'static Device> { 47 | self.device 48 | } 49 | 50 | fn pci_slot(&self) -> PciSlot { 51 | self.pci_slot 52 | } 53 | 54 | fn driver(&self) -> &str { 55 | &self.driver 56 | } 57 | 58 | fn sysfs_path(&self) -> &Path { 59 | &self.sysfs_path 60 | } 61 | 62 | fn first_hwmon(&self) -> Option<&Path> { 63 | self.first_hwmon_path.as_deref() 64 | } 65 | 66 | fn name(&self) -> Result { 67 | self.drm_name() 68 | } 69 | 70 | fn usage(&self) -> Result { 71 | let last_timestamp = self.last_busy_time_timestamp.get(); 72 | let last_busy_time = self.last_busy_time_us.get(); 73 | 74 | let new_timestamp = unix_as_secs_f64(); 75 | let new_busy_time = read_parsed(self.sysfs_path().join("device/npu_busy_time_us"))?; 76 | 77 | self.last_busy_time_timestamp.set(new_timestamp); 78 | self.last_busy_time_us.set(new_busy_time); 79 | 80 | let delta_timestamp = new_timestamp - last_timestamp; 81 | let delta_busy_time = new_busy_time.saturating_sub(last_busy_time) as f64; 82 | 83 | Ok(delta_busy_time / delta_timestamp) 84 | } 85 | 86 | fn used_vram(&self) -> Result { 87 | self.drm_used_memory().map(|usage| usage as usize) 88 | } 89 | 90 | fn total_vram(&self) -> Result { 91 | self.drm_total_memory().map(|usage| usage as usize) 92 | } 93 | 94 | fn temperature(&self) -> Result { 95 | self.hwmon_temperature() 96 | } 97 | 98 | fn power_usage(&self) -> Result { 99 | self.hwmon_power_usage() 100 | } 101 | 102 | fn core_frequency(&self) -> Result { 103 | self.hwmon_core_frequency() 104 | } 105 | 106 | fn memory_frequency(&self) -> Result { 107 | self.hwmon_memory_frequency() 108 | } 109 | 110 | fn power_cap(&self) -> Result { 111 | self.hwmon_power_cap() 112 | } 113 | 114 | fn power_cap_max(&self) -> Result { 115 | self.hwmon_power_cap_max() 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /data/icons/net.nokyan.Resources-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /data/resources/icons/generic-settings-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/ui/shortcuts.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | General 7 | 8 | 9 | Preferences 10 | app.settings 11 | 12 | 13 | 14 | 15 | Show Shortcuts 16 | app.shortcuts 17 | 18 | 19 | 20 | 21 | Quit 22 | app.quit 23 | 24 | 25 | 26 | 27 | 28 | 29 | Apps/Processes 30 | 31 | 32 | Toggle Search Field 33 | app.toggle-search 34 | 35 | 36 | 37 | 38 | End App/Process 39 | app.end-app-process 40 | 41 | 42 | 43 | 44 | Kill App/Process 45 | app.kill-app-process 46 | 47 | 48 | 49 | 50 | Halt App/Process 51 | app.halt-app-process 52 | 53 | 54 | 55 | 56 | Continue App/Process 57 | app.continue-app-process 58 | 59 | 60 | 61 | 62 | Show Information for App/Process 63 | app.information-app-process 64 | 65 | 66 | 67 | 68 | Show Process Options 69 | app.process-options 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/gui.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsString; 2 | use std::sync::LazyLock; 3 | 4 | use crate::application; 5 | #[rustfmt::skip] 6 | use crate::config; 7 | use crate::utils::IS_FLATPAK; 8 | use crate::utils::app::DATA_DIRS; 9 | 10 | use clap::{Parser, command}; 11 | use gettextrs::{LocaleCategory, gettext}; 12 | use gtk::{gio, glib}; 13 | use log::trace; 14 | 15 | use self::application::Application; 16 | use self::config::{GETTEXT_PACKAGE, LOCALEDIR, RESOURCES_FILE}; 17 | 18 | pub static ARGS: LazyLock = LazyLock::new(Args::parse); 19 | 20 | #[derive(Parser, Debug)] 21 | #[command(version, about)] 22 | pub struct Args { 23 | /// Disable GPU monitoring 24 | #[arg(short = 'g', long, default_value_t = false)] 25 | pub disable_gpu_monitoring: bool, 26 | 27 | /// Disable network interface monitoring 28 | #[arg(short = 'n', long, default_value_t = false)] 29 | pub disable_network_interface_monitoring: bool, 30 | 31 | /// Disable drive monitoring 32 | #[arg(short = 'd', long, default_value_t = false)] 33 | pub disable_drive_monitoring: bool, 34 | 35 | /// Disable battery monitoring 36 | #[arg(short = 'b', long, default_value_t = false)] 37 | pub disable_battery_monitoring: bool, 38 | 39 | /// Disable CPU monitoring 40 | #[arg(short = 'c', long, default_value_t = false)] 41 | pub disable_cpu_monitoring: bool, 42 | 43 | /// Disable memory monitoring 44 | #[arg(short = 'm', long, default_value_t = false)] 45 | pub disable_memory_monitoring: bool, 46 | 47 | /// Disable NPU monitoring 48 | #[arg(short = 'v', long, default_value_t = false)] 49 | pub disable_npu_monitoring: bool, 50 | 51 | /// Disable process monitoring 52 | #[arg(short = 'p', long, default_value_t = false)] 53 | pub disable_process_monitoring: bool, 54 | 55 | /// Open tab specified by ID. 56 | /// Valid IDs are: "applications", "processes", "cpu", "memory", "gpu-$PCI_SLOT$", 57 | /// "drive-$MODEL_NAME_OR_DEVICE_NAME$", "network-$INTERFACE_NAME$", 58 | /// "battery-$MANUFACTURER$-$MODEL_NAME$-$DEVICE_NAME$" 59 | #[arg(short = 't', long)] 60 | pub open_tab_id: Option, 61 | } 62 | 63 | pub fn main() { 64 | // Force args parsing here so we don't start printing logs before printing the help page 65 | std::hint::black_box(ARGS.disable_battery_monitoring); 66 | 67 | // Initialize logger 68 | pretty_env_logger::init(); 69 | trace!("Trace logs activated. Brace yourself for *lots* of logs. Slowdowns may occur."); 70 | 71 | // reset XDG_DATA_DIRS to use absolute paths instead of relative paths because Flatpak seemingly cannot resolve them 72 | // this must happen now because once the GTK app is loaded, it's too late 73 | if *IS_FLATPAK { 74 | unsafe { 75 | std::env::set_var( 76 | "XDG_DATA_DIRS", 77 | DATA_DIRS 78 | .iter() 79 | .map(|pathbuf| pathbuf.as_os_str().to_owned()) 80 | .collect::>() 81 | .join(&OsString::from(":")), 82 | ); 83 | } 84 | } 85 | 86 | // Prepare i18n 87 | gettextrs::setlocale(LocaleCategory::LcAll, ""); 88 | gettextrs::bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR).expect("Unable to bind the text domain"); 89 | gettextrs::textdomain(GETTEXT_PACKAGE).expect("Unable to switch to the text domain"); 90 | 91 | glib::set_application_name(&gettext("Resources")); 92 | 93 | let res = gio::Resource::load(RESOURCES_FILE).expect("Could not load gresource file"); 94 | gio::resources_register(&res); 95 | 96 | let app = Application::new(); 97 | app.run(); 98 | } 99 | -------------------------------------------------------------------------------- /data/resources/icons/zfs-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/utils/link/sata.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::drive::AtaSlot; 2 | use crate::utils::link::LinkData; 3 | use crate::utils::link::sata::SataSpeed::{Sata150, Sata300, Sata600}; 4 | use anyhow::{Context, Error, anyhow}; 5 | use log::trace; 6 | use std::fmt::{Display, Formatter}; 7 | use std::path::Path; 8 | use std::str::FromStr; 9 | 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 11 | pub enum SataSpeed { 12 | Sata150, 13 | Sata300, 14 | Sata600, 15 | } 16 | 17 | impl LinkData { 18 | pub fn from_ata_slot(ata_slot: &AtaSlot) -> anyhow::Result { 19 | trace!("Reading ATA link data for {ata_slot:?}…"); 20 | 21 | let ata_link_path = 22 | Path::new("/sys/class/ata_link").join(format!("link{}", ata_slot.ata_link)); 23 | 24 | let current_sata_speed_raw = std::fs::read_to_string(ata_link_path.join("sata_spd")) 25 | .map(|x| x.trim().to_string()) 26 | .context("Could not read sata_spd")?; 27 | 28 | let max_sata_speed_raw = std::fs::read_to_string(ata_link_path.join("sata_spd_max")) 29 | .map(|x| x.trim().to_string()) 30 | .context("Could not read sata_spd_max"); 31 | 32 | let current = SataSpeed::from_str(¤t_sata_speed_raw) 33 | .context("Could not parse current sata speed")?; 34 | let max = max_sata_speed_raw.and_then(|raw| SataSpeed::from_str(&raw)); 35 | 36 | Ok(Self { current, max }) 37 | } 38 | } 39 | 40 | impl FromStr for SataSpeed { 41 | type Err = Error; 42 | 43 | fn from_str(s: &str) -> std::result::Result { 44 | match s { 45 | // https://en.wikipedia.org/wiki/SATA 46 | "1.5 Gbps" => Ok(Sata150), 47 | "3.0 Gbps" => Ok(Sata300), 48 | "6.0 Gbps" => Ok(Sata600), 49 | _ => Err(anyhow!("Could not parse SATA speed: '{s}'")), 50 | } 51 | } 52 | } 53 | 54 | impl Display for SataSpeed { 55 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 56 | write!( 57 | f, 58 | "{}", 59 | match self { 60 | Sata150 => "SATA-150", 61 | Sata300 => "SATA-300", 62 | Sata600 => "SATA-600", 63 | } 64 | ) 65 | } 66 | } 67 | 68 | #[cfg(test)] 69 | mod test { 70 | use crate::utils::link::sata::SataSpeed; 71 | use std::collections::HashMap; 72 | use std::str::FromStr; 73 | 74 | #[test] 75 | fn parse_sata_link_speeds() { 76 | let map = HashMap::from([ 77 | ("1.5 Gbps", SataSpeed::Sata150), 78 | ("3.0 Gbps", SataSpeed::Sata300), 79 | ("6.0 Gbps", SataSpeed::Sata600), 80 | ]); 81 | 82 | for input in map.keys() { 83 | let result = SataSpeed::from_str(input); 84 | assert!(result.is_ok(), "Could not parse SATA speed for '{input}'"); 85 | let expected = map[input]; 86 | pretty_assertions::assert_eq!(expected, result.unwrap()); 87 | } 88 | } 89 | 90 | #[test] 91 | fn parse_sata_link_speeds_failure() { 92 | let invalid = vec!["4.0 Gbps", "SOMETHING_ELSE", ""]; 93 | 94 | for input in invalid { 95 | let result = SataSpeed::from_str(input); 96 | assert!( 97 | result.is_err(), 98 | "Could parse SATA speed for '{input}' while we don't expect that" 99 | ); 100 | } 101 | } 102 | 103 | #[test] 104 | fn display_sata_link_speeds() { 105 | let map = HashMap::from([ 106 | (SataSpeed::Sata150, "SATA-150"), 107 | (SataSpeed::Sata300, "SATA-300"), 108 | (SataSpeed::Sata600, "SATA-600"), 109 | ]); 110 | 111 | for input in map.keys() { 112 | let result = input.to_string(); 113 | let expected = map[input]; 114 | pretty_assertions::assert_str_eq!(expected, result); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/i18n.rs: -------------------------------------------------------------------------------- 1 | // i18n.rs 2 | // 3 | // Copyright 2020 Christopher Davis 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | // SPDX-License-Identifier: GPL-3.0-or-later 19 | 20 | use gettextrs::{gettext, ngettext, npgettext, pgettext}; 21 | use lazy_regex::{Captures, Regex}; 22 | 23 | #[allow(dead_code)] 24 | fn freplace(input: String, args: &[&str]) -> String { 25 | let mut parts = input.split("{}"); 26 | let mut output = parts.next().unwrap_or_default().to_string(); 27 | for (p, a) in parts.zip(args.iter()) { 28 | output += &((*a).to_string() + p); 29 | } 30 | output 31 | } 32 | 33 | #[allow(dead_code)] 34 | fn kreplace(input: String, kwargs: &[(&str, &str)]) -> String { 35 | let mut s = input; 36 | for (k, v) in kwargs { 37 | if let Ok(re) = Regex::new(&format!("\\{{{k}\\}}")) { 38 | s = re 39 | .replace_all(&s, |_: &Captures<'_>| (*v).to_string()) 40 | .to_string(); 41 | } 42 | } 43 | 44 | s 45 | } 46 | 47 | // Simple translations functions 48 | 49 | #[allow(dead_code)] 50 | pub fn i18n(format: &str) -> String { 51 | gettext(format) 52 | } 53 | 54 | #[allow(dead_code)] 55 | pub fn i18n_f(format: &str, args: &[&str]) -> String { 56 | let s = gettext(format); 57 | freplace(s, args) 58 | } 59 | 60 | #[allow(dead_code)] 61 | pub fn i18n_k(format: &str, kwargs: &[(&str, &str)]) -> String { 62 | let s = gettext(format); 63 | kreplace(s, kwargs) 64 | } 65 | 66 | // Singular and plural translations functions 67 | 68 | #[allow(dead_code)] 69 | pub fn ni18n(single: &str, multiple: &str, number: u32) -> String { 70 | ngettext(single, multiple, number) 71 | } 72 | 73 | #[allow(dead_code)] 74 | pub fn ni18n_f(single: &str, multiple: &str, number: u32, args: &[&str]) -> String { 75 | let s = ngettext(single, multiple, number); 76 | freplace(s, args) 77 | } 78 | 79 | #[allow(dead_code)] 80 | pub fn ni18n_k(single: &str, multiple: &str, number: u32, kwargs: &[(&str, &str)]) -> String { 81 | let s = ngettext(single, multiple, number); 82 | kreplace(s, kwargs) 83 | } 84 | 85 | // Translations with context functions 86 | 87 | #[allow(dead_code)] 88 | pub fn pi18n(ctx: &str, format: &str) -> String { 89 | pgettext(ctx, format) 90 | } 91 | 92 | #[allow(dead_code)] 93 | pub fn pi18n_f(ctx: &str, format: &str, args: &[&str]) -> String { 94 | let s = pgettext(ctx, format); 95 | freplace(s, args) 96 | } 97 | 98 | #[allow(dead_code)] 99 | pub fn pi18n_k(ctx: &str, format: &str, kwargs: &[(&str, &str)]) -> String { 100 | let s = pgettext(ctx, format); 101 | kreplace(s, kwargs) 102 | } 103 | 104 | // Singular and plural with context 105 | 106 | #[allow(dead_code)] 107 | pub fn pni18n(ctx: &str, single: &str, multiple: &str, number: u32) -> String { 108 | npgettext(ctx, single, multiple, number) 109 | } 110 | 111 | #[allow(dead_code)] 112 | pub fn pni18n_f(ctx: &str, single: &str, multiple: &str, number: u32, args: &[&str]) -> String { 113 | let s = npgettext(ctx, single, multiple, number); 114 | freplace(s, args) 115 | } 116 | 117 | #[allow(dead_code)] 118 | pub fn pni18n_k( 119 | ctx: &str, 120 | single: &str, 121 | multiple: &str, 122 | number: u32, 123 | kwargs: &[(&str, &str)], 124 | ) -> String { 125 | let s = npgettext(ctx, single, multiple, number); 126 | kreplace(s, kwargs) 127 | } 128 | -------------------------------------------------------------------------------- /data/resources/icons/mapped-device-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/ui/widgets/double_graph_box.rs: -------------------------------------------------------------------------------- 1 | use adw::{prelude::*, subclass::prelude::*}; 2 | use gtk::glib; 3 | use log::trace; 4 | 5 | use crate::config::PROFILE; 6 | 7 | use super::graph::ResGraph; 8 | 9 | mod imp { 10 | use crate::ui::widgets::graph::ResGraph; 11 | 12 | use super::*; 13 | 14 | use gtk::CompositeTemplate; 15 | 16 | #[derive(Debug, CompositeTemplate, Default)] 17 | #[template(resource = "/net/nokyan/Resources/ui/widgets/double_graph_box.ui")] 18 | pub struct ResDoubleGraphBox { 19 | #[template_child] 20 | pub start_graph: TemplateChild, 21 | #[template_child] 22 | pub start_title_label: TemplateChild, 23 | #[template_child] 24 | pub start_info_label: TemplateChild, 25 | #[template_child] 26 | pub end_graph: TemplateChild, 27 | #[template_child] 28 | pub end_title_label: TemplateChild, 29 | #[template_child] 30 | pub end_info_label: TemplateChild, 31 | } 32 | 33 | #[glib::object_subclass] 34 | impl ObjectSubclass for ResDoubleGraphBox { 35 | const NAME: &'static str = "ResDoubleGraphBox"; 36 | type Type = super::ResDoubleGraphBox; 37 | type ParentType = adw::PreferencesRow; 38 | 39 | fn class_init(klass: &mut Self::Class) { 40 | Self::bind_template(klass); 41 | } 42 | 43 | // You must call `Widget`'s `init_template()` within `instance_init()`. 44 | fn instance_init(obj: &glib::subclass::InitializingObject) { 45 | obj.init_template(); 46 | } 47 | } 48 | 49 | impl ObjectImpl for ResDoubleGraphBox { 50 | fn constructed(&self) { 51 | self.parent_constructed(); 52 | let obj = self.obj(); 53 | 54 | // Devel Profile 55 | if PROFILE == "Devel" { 56 | obj.add_css_class("devel"); 57 | } 58 | } 59 | } 60 | 61 | impl WidgetImpl for ResDoubleGraphBox {} 62 | 63 | impl ListBoxRowImpl for ResDoubleGraphBox {} 64 | 65 | impl PreferencesRowImpl for ResDoubleGraphBox {} 66 | } 67 | 68 | glib::wrapper! { 69 | pub struct ResDoubleGraphBox(ObjectSubclass) 70 | @extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, 71 | @implements gtk::Buildable, gtk::ConstraintTarget, gtk::Accessible, gtk::Actionable; 72 | } 73 | 74 | impl Default for ResDoubleGraphBox { 75 | fn default() -> Self { 76 | Self::new() 77 | } 78 | } 79 | 80 | impl ResDoubleGraphBox { 81 | pub fn new() -> Self { 82 | trace!("Creating ResDoubleGraphBox GObject…"); 83 | 84 | glib::Object::new::() 85 | } 86 | 87 | pub fn set_graphs_visible(&self, visible: bool) { 88 | let imp = self.imp(); 89 | imp.start_graph.set_visible(visible); 90 | imp.end_graph.set_visible(visible); 91 | } 92 | 93 | pub fn start_graph(&self) -> ResGraph { 94 | self.imp().start_graph.get() 95 | } 96 | 97 | pub fn set_start_title_label(&self, str: &str) { 98 | let imp = self.imp(); 99 | imp.start_title_label.set_label(str); 100 | } 101 | 102 | pub fn set_start_subtitle(&self, str: &str) { 103 | let imp = self.imp(); 104 | imp.start_info_label.set_label(str); 105 | } 106 | 107 | pub fn set_start_tooltip(&self, str: Option<&str>) { 108 | let imp = self.imp(); 109 | imp.start_info_label.set_tooltip_text(str); 110 | } 111 | 112 | pub fn end_graph(&self) -> ResGraph { 113 | self.imp().end_graph.get() 114 | } 115 | 116 | pub fn set_end_title_label(&self, str: &str) { 117 | let imp = self.imp(); 118 | imp.end_title_label.set_label(str); 119 | } 120 | 121 | pub fn set_end_subtitle(&self, str: &str) { 122 | let imp = self.imp(); 123 | imp.end_info_label.set_label(str); 124 | } 125 | 126 | pub fn set_end_tooltip(&self, str: Option<&str>) { 127 | let imp = self.imp(); 128 | imp.end_info_label.set_tooltip_text(str); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /data/resources/ui/widgets/double_graph_box.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 104 | -------------------------------------------------------------------------------- /data/resources/ui/pages/applications.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | Kill App 7 | applications.kill-app 8 | 9 | 10 | Halt App 11 | applications.halt-app 12 | 13 | 14 | Continue App 15 | applications.continue-app 16 | 17 |
18 |
19 | 20 |
21 | 22 | End App 23 | applications.context-end-app 24 | 25 | 26 | Kill App 27 | applications.context-kill-app 28 | 29 | 30 | Halt App 31 | applications.context-halt-app 32 | 33 | 34 | Continue App 35 | applications.context-continue-app 36 | 37 |
38 |
39 | 40 | Information 41 | applications.context-information 42 | 43 |
44 |
45 | 111 |
-------------------------------------------------------------------------------- /data/resources/ui/pages/memory.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 99 | -------------------------------------------------------------------------------- /data/resources/ui/pages/battery.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 106 | -------------------------------------------------------------------------------- /src/ui/pages/processes/process_name_cell.rs: -------------------------------------------------------------------------------- 1 | use adw::{glib::property::PropertySet, prelude::*, subclass::prelude::*}; 2 | use gtk::{gio::Icon, glib}; 3 | use log::trace; 4 | 5 | mod imp { 6 | use std::cell::{Cell, RefCell}; 7 | 8 | use super::*; 9 | 10 | use gtk::{ 11 | Box, CompositeTemplate, 12 | gio::ThemedIcon, 13 | glib::{ParamSpec, Properties, Value}, 14 | }; 15 | 16 | #[derive(CompositeTemplate, Properties)] 17 | #[template(resource = "/net/nokyan/Resources/ui/widgets/process_name_cell.ui")] 18 | #[properties(wrapper_type = super::ResProcessNameCell)] 19 | pub struct ResProcessNameCell { 20 | #[template_child] 21 | pub image: TemplateChild, 22 | #[template_child] 23 | pub inscription: TemplateChild, 24 | 25 | #[property(get = Self::name, set = Self::set_name, type = glib::GString)] 26 | name: Cell, 27 | #[property(get = Self::tooltip, set = Self::set_tooltip, type = glib::GString)] 28 | tooltip: Cell, 29 | #[property(get = Self::icon, set = Self::set_icon, type = Icon)] 30 | icon: RefCell, 31 | #[property(get, set = Self::set_symbolic)] 32 | symbolic: Cell, 33 | } 34 | 35 | impl Default for ResProcessNameCell { 36 | fn default() -> Self { 37 | Self { 38 | image: Default::default(), 39 | inscription: Default::default(), 40 | name: Default::default(), 41 | tooltip: Default::default(), 42 | icon: RefCell::new(ThemedIcon::new("generic-process").into()), 43 | symbolic: Default::default(), 44 | } 45 | } 46 | } 47 | 48 | impl ResProcessNameCell { 49 | pub fn name(&self) -> glib::GString { 50 | let name = self.name.take(); 51 | self.name.set(name.clone()); 52 | name 53 | } 54 | 55 | pub fn set_name(&self, name: &str) { 56 | self.name.set(glib::GString::from(name)); 57 | self.inscription.set_text(Some(name)); 58 | } 59 | 60 | pub fn tooltip(&self) -> glib::GString { 61 | let tooltip = self.tooltip.take(); 62 | self.tooltip.set(tooltip.clone()); 63 | tooltip 64 | } 65 | 66 | pub fn set_tooltip(&self, tooltip: &str) { 67 | self.tooltip.set(glib::GString::from(tooltip)); 68 | self.inscription.set_tooltip_text(Some(tooltip)); 69 | } 70 | 71 | pub fn icon(&self) -> Icon { 72 | let icon = self 73 | .icon 74 | .replace_with(|_| ThemedIcon::new("generic-process").into()); 75 | self.icon.set(icon.clone()); 76 | icon 77 | } 78 | 79 | pub fn set_icon(&self, icon: &Icon) { 80 | let current_icon = self 81 | .icon 82 | .replace_with(|_| ThemedIcon::new("generic-process").into()); 83 | 84 | if ¤t_icon == icon { 85 | self.icon.set(current_icon); 86 | return; 87 | } 88 | 89 | self.image.set_from_gicon(icon); 90 | 91 | self.icon.set(icon.clone()); 92 | } 93 | 94 | pub fn set_symbolic(&self, symbolic: bool) { 95 | self.symbolic.set(symbolic); 96 | 97 | if symbolic { 98 | self.image.set_css_classes(&[]); 99 | } else { 100 | self.image.set_css_classes(&["lowres-icon"]); 101 | } 102 | } 103 | } 104 | 105 | #[glib::object_subclass] 106 | impl ObjectSubclass for ResProcessNameCell { 107 | const NAME: &'static str = "ResProcessNameCell"; 108 | type Type = super::ResProcessNameCell; 109 | type ParentType = Box; 110 | 111 | fn class_init(klass: &mut Self::Class) { 112 | Self::bind_template(klass); 113 | } 114 | 115 | // You must call `Widget`'s `init_template()` within `instance_init()`. 116 | fn instance_init(obj: &glib::subclass::InitializingObject) { 117 | obj.init_template(); 118 | } 119 | } 120 | 121 | impl ObjectImpl for ResProcessNameCell { 122 | fn constructed(&self) { 123 | self.parent_constructed(); 124 | } 125 | 126 | fn properties() -> &'static [ParamSpec] { 127 | Self::derived_properties() 128 | } 129 | 130 | fn set_property(&self, id: usize, value: &Value, pspec: &ParamSpec) { 131 | self.derived_set_property(id, value, pspec); 132 | } 133 | 134 | fn property(&self, id: usize, pspec: &ParamSpec) -> Value { 135 | self.derived_property(id, pspec) 136 | } 137 | } 138 | 139 | impl WidgetImpl for ResProcessNameCell {} 140 | 141 | impl BoxImpl for ResProcessNameCell {} 142 | } 143 | 144 | glib::wrapper! { 145 | pub struct ResProcessNameCell(ObjectSubclass) 146 | @extends gtk::Widget, gtk::Box, 147 | @implements gtk::Buildable, gtk::ConstraintTarget, gtk::Accessible; 148 | } 149 | 150 | impl Default for ResProcessNameCell { 151 | fn default() -> Self { 152 | Self::new() 153 | } 154 | } 155 | 156 | impl ResProcessNameCell { 157 | pub fn new() -> Self { 158 | trace!("Creating ResProcessNameCell GObject…"); 159 | 160 | glib::Object::new::() 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/ui/pages/applications/application_name_cell.rs: -------------------------------------------------------------------------------- 1 | use adw::{glib::property::PropertySet, prelude::*, subclass::prelude::*}; 2 | use gtk::{gio::Icon, glib}; 3 | use log::trace; 4 | 5 | mod imp { 6 | use std::cell::{Cell, RefCell}; 7 | 8 | use super::*; 9 | 10 | use gtk::{ 11 | Box, CompositeTemplate, 12 | gio::ThemedIcon, 13 | glib::{ParamSpec, Properties, Value}, 14 | }; 15 | 16 | #[derive(CompositeTemplate, Properties)] 17 | #[template(resource = "/net/nokyan/Resources/ui/widgets/application_name_cell.ui")] 18 | #[properties(wrapper_type = super::ResApplicationNameCell)] 19 | pub struct ResApplicationNameCell { 20 | #[template_child] 21 | pub image: TemplateChild, 22 | #[template_child] 23 | pub inscription: TemplateChild, 24 | 25 | #[property(get = Self::name, set = Self::set_name, type = glib::GString)] 26 | name: Cell, 27 | #[property(get = Self::tooltip, set = Self::set_tooltip, type = glib::GString)] 28 | tooltip: Cell, 29 | #[property(get = Self::icon, set = Self::set_icon, type = Icon)] 30 | icon: RefCell, 31 | #[property(get, set = Self::set_symbolic)] 32 | symbolic: Cell, 33 | } 34 | 35 | impl Default for ResApplicationNameCell { 36 | fn default() -> Self { 37 | Self { 38 | image: Default::default(), 39 | inscription: Default::default(), 40 | name: Default::default(), 41 | tooltip: Default::default(), 42 | icon: RefCell::new(ThemedIcon::new("generic-process").into()), 43 | symbolic: Default::default(), 44 | } 45 | } 46 | } 47 | 48 | impl ResApplicationNameCell { 49 | pub fn name(&self) -> glib::GString { 50 | let name = self.name.take(); 51 | let result = name.clone(); 52 | self.name.set(name); 53 | 54 | result 55 | } 56 | 57 | pub fn set_name(&self, name: &str) { 58 | self.name.set(glib::GString::from(name)); 59 | self.inscription.set_text(Some(name)); 60 | } 61 | 62 | pub fn tooltip(&self) -> glib::GString { 63 | let tooltip = self.tooltip.take(); 64 | let result = tooltip.clone(); 65 | self.tooltip.set(tooltip); 66 | 67 | result 68 | } 69 | 70 | pub fn set_tooltip(&self, tooltip: &str) { 71 | self.tooltip.set(glib::GString::from(tooltip)); 72 | self.inscription.set_tooltip_text(Some(tooltip)); 73 | } 74 | 75 | pub fn icon(&self) -> Icon { 76 | let icon = self 77 | .icon 78 | .replace_with(|_| ThemedIcon::new("generic-process").into()); 79 | self.icon.set(icon.clone()); 80 | 81 | icon 82 | } 83 | 84 | pub fn set_icon(&self, icon: &Icon) { 85 | let current_icon = self 86 | .icon 87 | .replace_with(|_| ThemedIcon::new("generic-process").into()); 88 | 89 | if ¤t_icon == icon { 90 | self.icon.set(current_icon); 91 | return; 92 | } 93 | 94 | self.image.set_from_gicon(icon); 95 | 96 | self.icon.set(icon.clone()); 97 | } 98 | 99 | pub fn set_symbolic(&self, symbolic: bool) { 100 | self.symbolic.set(symbolic); 101 | 102 | if symbolic { 103 | self.image.set_css_classes(&["bubble"]); 104 | self.image.set_pixel_size(16); 105 | } else { 106 | self.image.set_css_classes(&["lowres-icon"]); 107 | self.image.set_pixel_size(32); 108 | } 109 | } 110 | } 111 | 112 | #[glib::object_subclass] 113 | impl ObjectSubclass for ResApplicationNameCell { 114 | const NAME: &'static str = "ResApplicationNameCell"; 115 | type Type = super::ResApplicationNameCell; 116 | type ParentType = Box; 117 | 118 | fn class_init(klass: &mut Self::Class) { 119 | Self::bind_template(klass); 120 | } 121 | 122 | // You must call `Widget`'s `init_template()` within `instance_init()`. 123 | fn instance_init(obj: &glib::subclass::InitializingObject) { 124 | obj.init_template(); 125 | } 126 | } 127 | 128 | impl ObjectImpl for ResApplicationNameCell { 129 | fn constructed(&self) { 130 | self.parent_constructed(); 131 | } 132 | 133 | fn properties() -> &'static [ParamSpec] { 134 | Self::derived_properties() 135 | } 136 | 137 | fn set_property(&self, id: usize, value: &Value, pspec: &ParamSpec) { 138 | self.derived_set_property(id, value, pspec); 139 | } 140 | 141 | fn property(&self, id: usize, pspec: &ParamSpec) -> Value { 142 | self.derived_property(id, pspec) 143 | } 144 | } 145 | 146 | impl WidgetImpl for ResApplicationNameCell {} 147 | 148 | impl BoxImpl for ResApplicationNameCell {} 149 | } 150 | 151 | glib::wrapper! { 152 | pub struct ResApplicationNameCell(ObjectSubclass) 153 | @extends gtk::Widget, gtk::Box, 154 | @implements gtk::Buildable, gtk::ConstraintTarget, gtk::Accessible; 155 | } 156 | 157 | impl Default for ResApplicationNameCell { 158 | fn default() -> Self { 159 | Self::new() 160 | } 161 | } 162 | 163 | impl ResApplicationNameCell { 164 | pub fn new() -> Self { 165 | trace!("Creating ResApplicationNameCell GObject…"); 166 | 167 | glib::Object::new::() 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /data/resources/ui/pages/drive.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 118 | -------------------------------------------------------------------------------- /src/utils/gpu/amd.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result, bail}; 2 | use lazy_regex::{Lazy, Regex, lazy_regex}; 3 | use log::{debug, trace, warn}; 4 | use process_data::gpu_usage::GpuIdentifier; 5 | 6 | use std::{ 7 | collections::HashMap, 8 | path::{Path, PathBuf}, 9 | sync::LazyLock, 10 | time::Instant, 11 | }; 12 | 13 | use crate::utils::{ 14 | IS_FLATPAK, 15 | pci::{self, Device}, 16 | read_parsed, 17 | }; 18 | 19 | use super::GpuImpl; 20 | 21 | static RE_AMDGPU_IDS: Lazy = lazy_regex!(r"([0-9A-F]{4}),\s*([0-9A-F]{2}),\s*(.*)"); 22 | 23 | static AMDGPU_IDS: LazyLock> = LazyLock::new(|| { 24 | AmdGpu::read_libdrm_ids() 25 | .inspect_err(|e| warn!("Unable to parse amdgpu.ids!\n{e}\n{}", e.backtrace())) 26 | .unwrap_or_default() 27 | }); 28 | 29 | #[derive(Debug, Clone, Default)] 30 | 31 | pub struct AmdGpu { 32 | pub device: Option<&'static Device>, 33 | pub gpu_identifier: GpuIdentifier, 34 | pub driver: String, 35 | sysfs_path: PathBuf, 36 | first_hwmon_path: Option, 37 | combined_media_engine: bool, 38 | } 39 | 40 | impl AmdGpu { 41 | pub fn new( 42 | device: Option<&'static Device>, 43 | gpu_identifier: GpuIdentifier, 44 | driver: String, 45 | sysfs_path: PathBuf, 46 | first_hwmon_path: Option, 47 | ) -> Self { 48 | let mut gpu = Self { 49 | device, 50 | gpu_identifier, 51 | driver, 52 | sysfs_path, 53 | first_hwmon_path, 54 | combined_media_engine: false, 55 | }; 56 | 57 | if let Ok(vcn_version) = read_parsed::( 58 | gpu.sysfs_path() 59 | .join("device/ip_discovery/die/0/UVD/0/major"), 60 | ) { 61 | if vcn_version >= 4 { 62 | gpu.combined_media_engine = true; 63 | } 64 | } 65 | 66 | gpu 67 | } 68 | 69 | pub fn read_libdrm_ids() -> Result> { 70 | let path = if *IS_FLATPAK { 71 | PathBuf::from("/run/host/usr/share/libdrm/amdgpu.ids") 72 | } else { 73 | PathBuf::from("/usr/share/libdrm/amdgpu.ids") 74 | }; 75 | 76 | debug!("Parsing {}…", path.to_string_lossy()); 77 | 78 | let start = Instant::now(); 79 | 80 | let mut map = HashMap::new(); 81 | 82 | let amdgpu_ids_raw = read_parsed::(&path)?; 83 | 84 | for capture in RE_AMDGPU_IDS.captures_iter(&amdgpu_ids_raw) { 85 | if let (Some(device_id), Some(revision), Some(name)) = 86 | (capture.get(1), capture.get(2), capture.get(3)) 87 | { 88 | let device_id = u16::from_str_radix(device_id.as_str().trim(), 16).unwrap(); 89 | let revision = u8::from_str_radix(revision.as_str().trim(), 16).unwrap(); 90 | let name = name.as_str().into(); 91 | trace!("Found {name} ({device_id:04x}, rev {revision:02x})"); 92 | map.insert((device_id, revision), name); 93 | } 94 | } 95 | 96 | let elapsed = start.elapsed(); 97 | 98 | debug!( 99 | "Successfully parsed {} within {elapsed:.2?} ({} entries)", 100 | path.to_string_lossy(), 101 | map.len() 102 | ); 103 | 104 | Ok(map) 105 | } 106 | } 107 | 108 | impl GpuImpl for AmdGpu { 109 | fn device(&self) -> Option<&'static Device> { 110 | self.device 111 | } 112 | 113 | fn gpu_identifier(&self) -> GpuIdentifier { 114 | self.gpu_identifier 115 | } 116 | 117 | fn driver(&self) -> &str { 118 | &self.driver 119 | } 120 | 121 | fn sysfs_path(&self) -> &Path { 122 | &self.sysfs_path 123 | } 124 | 125 | fn first_hwmon(&self) -> Option<&Path> { 126 | self.first_hwmon_path.as_deref() 127 | } 128 | 129 | fn name(&self) -> Result { 130 | let revision = u8::from_str_radix( 131 | read_parsed::(self.sysfs_path().join("device/revision"))? 132 | .strip_prefix("0x") 133 | .context("missing hex prefix")?, 134 | 16, 135 | )?; 136 | Ok((*AMDGPU_IDS) 137 | .get(&(self.device().map_or(0, pci::Device::pid), revision)) 138 | .cloned() 139 | .unwrap_or_else(|| { 140 | if let Ok(drm_name) = self.drm_name() { 141 | format!("AMD Radeon Graphics ({drm_name})") 142 | } else { 143 | "AMD Radeon Graphics".into() 144 | } 145 | })) 146 | } 147 | 148 | fn usage(&self) -> Result { 149 | self.drm_usage().map(|usage| usage as f64 / 100.0) 150 | } 151 | 152 | fn encode_usage(&self) -> Result { 153 | bail!("encode usage not implemented for AMD") 154 | } 155 | 156 | fn decode_usage(&self) -> Result { 157 | bail!("decode usage not implemented for AMD") 158 | } 159 | 160 | fn combined_media_engine(&self) -> Result { 161 | Ok(self.combined_media_engine) 162 | } 163 | 164 | fn used_vram(&self) -> Result { 165 | self.drm_used_vram().map(|usage| usage as u64) 166 | } 167 | 168 | fn total_vram(&self) -> Result { 169 | self.drm_total_vram().map(|usage| usage as u64) 170 | } 171 | 172 | fn temperature(&self) -> Result { 173 | self.hwmon_temperature() 174 | } 175 | 176 | fn power_usage(&self) -> Result { 177 | self.hwmon_power_usage() 178 | } 179 | 180 | fn core_frequency(&self) -> Result { 181 | self.hwmon_core_frequency() 182 | } 183 | 184 | fn vram_frequency(&self) -> Result { 185 | self.hwmon_vram_frequency() 186 | } 187 | 188 | fn power_cap(&self) -> Result { 189 | self.hwmon_power_cap() 190 | } 191 | 192 | fn power_cap_max(&self) -> Result { 193 | self.hwmon_power_cap_max() 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /data/resources/ui/pages/npu.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 123 | -------------------------------------------------------------------------------- /data/resources/ui/pages/network.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Resources 2 | 3 | Download on Flathub 4 | 5 | [![GNOME Circle](https://circle.gnome.org/assets/button/badge.svg 6 | )](https://apps.gnome.org/app/net.nokyan.Resources/) [![Please do not theme this app](https://stopthemingmy.app/badge.svg)](https://stopthemingmy.app) 7 | 8 | Resources is a simple yet powerful monitor for your system resources and processes, written in Rust and using GTK 4 and libadwaita for its GUI. It’s capable of displaying usage and details of your CPU, memory, GPUs, NPUs, network interfaces and block devices. It’s also capable of listing and terminating running graphical applications as well as processes. 9 | 10 | Resources is *not* a program that will try to display every single possible piece of information about each tiny part of your device. Instead, it aims to strike a balance between information richness, user-friendliness and a balanced user interface — showing you most of the information most of you need most of the time. 11 | 12 |
13 | Click me for screenshots! 14 | 15 | ![Apps View](data/resources/screenshots/1.png?raw=true "Apps View") 16 | 17 | ![Processes View](data/resources/screenshots/2.png?raw=true "Processes View") 18 | 19 | ![Processor View](data/resources/screenshots/3.png?raw=true "Processor View") 20 | 21 | ![Memory View](data/resources/screenshots/4.png?raw=true "Memory View") 22 | 23 | ![GPU View](data/resources/screenshots/5.png?raw=true "GPU View") 24 | 25 | ![Drive View](data/resources/screenshots/6.png?raw=true "Drive View") 26 | 27 | ![Network Interface View](data/resources/screenshots/7.png?raw=true "Network Interface View") 28 | 29 | ![Battery View](data/resources/screenshots/8.png?raw=true "Battery View") 30 | 31 |
32 | 33 | ## Installing 34 | 35 | The **official** and **only supported** way of installing Resources is using Flatpak. Simply use your graphical software manager like GNOME Software or Discover to install Resources from Flathub or type ``flatpak install flathub net.nokyan.Resources`` in your terminal. 36 | Please keep in mind that you need to have Flathub set up on your device. You can find out how to set up Flathub [here](https://flathub.org/setup). 37 | 38 | ### Unofficial Packages 39 | 40 | Resources has been packaged for some Linux distributions by volunteers. Keep in mind that these are not supported. 41 | If you’re packaging Resources for another distribution, feel free to send a pull request to add your package to this list! 42 | 43 | #### Arch Linux 44 | 45 | Unofficially packaged in the [extra](https://archlinux.org/packages/extra/x86_64/resources/) repository. 46 | 47 | You can install Resources using `pacman` with no further configuration required. 48 | 49 | ```sh 50 | pacman -S resources 51 | ``` 52 | 53 | #### Fedora 54 | 55 | Unofficially packaged in [Copr](https://copr.fedorainfracloud.org/coprs/atim/resources/) for Fedora 39 and newer. 56 | 57 | You first need to enable the `atim/resources` Copr repository and then use `dnf` to install Resources. 58 | 59 | ```sh 60 | dnf copr enable atim/resources 61 | dnf install resources 62 | ``` 63 | 64 | #### Nix 65 | 66 | Unofficially packaged for Nix/NixOS. The Flatpak version is [known to have issues](https://github.com/nokyan/resources/issues/76) with showing running apps and processes on NixOS, which the native package may resolve. 67 | 68 | In `configuration.nix`: 69 | ``` 70 | environment.systemPackages = [ 71 | pkgs.resources 72 | ]; 73 | ``` 74 | 75 | ## Building 76 | 77 | You can also build Resources yourself using either Meson directly or preferably using Flatpak Builder. 78 | 79 | ### Build Dependencies 80 | 81 | - `glib-2.0` ≥ 2.66 82 | - `gio-2.0` ≥ 2.66 83 | - `gtk-4` ≥ 4.12 84 | - `libadwaita-1` ≥ 1.8 85 | - `cargo` 86 | 87 | Other dependencies are handled by `cargo`. 88 | Resources’ minimum supported Rust version (MSRV) is **1.85.0**. 89 | 90 | ### Runtime Dependencies 91 | 92 | These dependencies are not needed to build Resources but Resources may lack certain functionalities when they are not present. 93 | 94 | - `systemd` (needed for app detection using cgroups) 95 | - `polkit` (needed for executing privileged actions like killing certain processes) 96 | 97 | ### Building Using Flatpak Builder 98 | 99 | ```sh 100 | flatpak install org.gnome.Sdk//49 org.freedesktop.Sdk.Extension.rust-stable//25.08 org.gnome.Platform//49 org.freedesktop.Sdk.Extension.llvm21//25.08 101 | flatpak-builder --user flatpak_app build-aux/net.nokyan.Resources.Devel.json 102 | ``` 103 | 104 | If you use [GNOME Builder](https://apps.gnome.org/app/org.gnome.Builder/) or Visual Studio Code with the [Flatpak extension](https://marketplace.visualstudio.com/items?itemName=bilelmoussaoui.flatpak-vscode), Resources can be built and run automatically. 105 | 106 | ### Building Natively Using Meson 107 | 108 | ```sh 109 | meson . build --prefix=/usr/local 110 | ninja -C build install 111 | ``` 112 | 113 | ## Running 114 | 115 | Running Resources is as simple as typing `flatpak run net.nokyan.Resources` into a terminal or running it from your app launcher. 116 | If you’ve built Resources natively or installed it from a traditional package manager such as `apt` or `dnf`, or if you’ve built Resources yourself, typing `resources` in a terminal will start Resources. 117 | If you’ve built Resources as a Flatpak, type `flatpak-builder --run flatpak_app build-aux/net.nokyan.Resources.Devel.json resources` into your terminal or use one of the aforementioned IDEs to do that automatically. 118 | 119 | ## Contributing 120 | 121 | If you have an idea, bug report, question or something else, don’t hesitate to [open an issue](https://github.com/nokyan/resources/issues)! Translations are always welcome but need to go through [GNOME Damned Lies](https://l10n.gnome.org/module/resources/), ordinary pull requests for translation changes cannot be accepted anymore. 122 | 123 | ## Code of Conduct 124 | 125 | Resources follows the [GNOME Code of Conduct](/CODE_OF_CONDUCT.md). 126 | All communications in project spaces are expected to follow it. 127 | -------------------------------------------------------------------------------- /data/resources/ui/pages/gpu.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 129 | -------------------------------------------------------------------------------- /src/utils/link/usb.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::drive::UsbSlot; 2 | use crate::utils::link::LinkData; 3 | use crate::utils::units::convert_speed_bits_decimal_with_places; 4 | use anyhow::{Context, Error, anyhow}; 5 | use log::trace; 6 | use std::fmt::{Display, Formatter}; 7 | use std::path::Path; 8 | use std::str::FromStr; 9 | 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 11 | pub enum UsbSpeed { 12 | // https://en.wikipedia.org/wiki/USB#Release_versions 13 | Usb1_0, 14 | Usb1_1(usize), 15 | Usb2_0(usize), 16 | Usb3_0(usize), 17 | Usb3_1(usize), 18 | Usb3_2(usize), 19 | Usb4(usize), 20 | Usb4_2_0(usize), 21 | } 22 | 23 | impl LinkData { 24 | pub fn from_usb_slot(usb_slot: &UsbSlot) -> anyhow::Result { 25 | trace!("Reading USB link data for {usb_slot:?}…"); 26 | 27 | let usb_bus_path = 28 | Path::new("/sys/bus/usb/devices/").join(format!("usb{}", usb_slot.usb_bus)); 29 | 30 | let max_usb_port_speed_raw = std::fs::read_to_string(usb_bus_path.join("speed")) 31 | .map(|x| x.trim().to_string()) 32 | .context("Could not read usb port speed"); 33 | 34 | let usb_device_speed = 35 | std::fs::read_to_string(usb_bus_path.join(&usb_slot.usb_device).join("speed")) 36 | .map(|x| x.trim().to_string()) 37 | .context("Could not read usb device speed")?; 38 | 39 | let usb_port_speed = max_usb_port_speed_raw.and_then(|x| UsbSpeed::from_str(&x)); 40 | 41 | let usb_device_speed = 42 | UsbSpeed::from_str(&usb_device_speed).context("Could not parse USB device speed")?; 43 | 44 | Ok(LinkData { 45 | current: usb_device_speed, 46 | max: usb_port_speed, 47 | }) 48 | } 49 | } 50 | 51 | impl Display for UsbSpeed { 52 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 53 | write!( 54 | f, 55 | "{} ({})", 56 | // https://en.wikipedia.org/wiki/USB#Release_versions 57 | match self { 58 | UsbSpeed::Usb1_0 => "USB 1.0", 59 | UsbSpeed::Usb1_1(_) => "USB 1.1", 60 | UsbSpeed::Usb2_0(_) => "USB 2.0", 61 | UsbSpeed::Usb3_0(_) => "USB 3.0", 62 | UsbSpeed::Usb3_1(_) => "USB 3.1", 63 | UsbSpeed::Usb3_2(_) => "USB 3.2", 64 | UsbSpeed::Usb4(_) => "USB4", 65 | UsbSpeed::Usb4_2_0(_) => "USB4 2.0", 66 | }, 67 | match self { 68 | UsbSpeed::Usb1_0 => convert_speed_bits_decimal_with_places(1.5 * 1_000_000.0, 1), 69 | UsbSpeed::Usb1_1(mbit) 70 | | UsbSpeed::Usb2_0(mbit) 71 | | UsbSpeed::Usb3_0(mbit) 72 | | UsbSpeed::Usb3_1(mbit) 73 | | UsbSpeed::Usb3_2(mbit) 74 | | UsbSpeed::Usb4(mbit) 75 | | UsbSpeed::Usb4_2_0(mbit) => 76 | convert_speed_bits_decimal_with_places(*mbit as f64 * 1_000_000.0, 0), 77 | } 78 | ) 79 | } 80 | } 81 | 82 | impl FromStr for UsbSpeed { 83 | type Err = Error; 84 | fn from_str(s: &str) -> std::result::Result { 85 | match s { 86 | // https://en.wikipedia.org/wiki/USB#Release_versions 87 | //https://www.kernel.org/doc/Documentation/ABI/stable/sysfs-bus-usb 88 | "1.5" => Ok(UsbSpeed::Usb1_0), 89 | "12" => Ok(UsbSpeed::Usb1_1(12)), 90 | "480" => Ok(UsbSpeed::Usb2_0(480)), 91 | "5000" => Ok(UsbSpeed::Usb3_0(5_000)), 92 | "10000" => Ok(UsbSpeed::Usb3_1(10_000)), 93 | "20000" => Ok(UsbSpeed::Usb3_2(20_000)), 94 | "40000" => Ok(UsbSpeed::Usb4(40_000)), 95 | "80000" => Ok(UsbSpeed::Usb4_2_0(80_000)), 96 | "120000" => Ok(UsbSpeed::Usb4_2_0(120_000)), 97 | _ => Err(anyhow!("Could not parse USB speed: '{s}'")), 98 | } 99 | } 100 | } 101 | 102 | #[cfg(test)] 103 | mod test { 104 | use crate::utils::link::usb::UsbSpeed; 105 | use std::collections::HashMap; 106 | use std::str::FromStr; 107 | 108 | #[test] 109 | fn parse_usb_link_speeds() { 110 | let map = HashMap::from([ 111 | ("1.5", UsbSpeed::Usb1_0), 112 | ("12", UsbSpeed::Usb1_1(12)), 113 | ("480", UsbSpeed::Usb2_0(480)), 114 | ("5000", UsbSpeed::Usb3_0(5_000)), 115 | ("10000", UsbSpeed::Usb3_1(10_000)), 116 | ("20000", UsbSpeed::Usb3_2(20_000)), 117 | ("40000", UsbSpeed::Usb4(40_000)), 118 | ("80000", UsbSpeed::Usb4_2_0(80_000)), 119 | ("120000", UsbSpeed::Usb4_2_0(120_000)), 120 | ]); 121 | 122 | for input in map.keys() { 123 | let result = UsbSpeed::from_str(input); 124 | assert!(result.is_ok(), "Could not parse USB speed for '{input}'"); 125 | let expected = map[input]; 126 | pretty_assertions::assert_eq!(expected, result.unwrap()); 127 | } 128 | } 129 | 130 | #[test] 131 | fn parse_usb_link_speeds_failure() { 132 | let invalid = vec!["4000", "160000", "SOMETHING_ELSE", ""]; 133 | 134 | for input in invalid { 135 | let result = UsbSpeed::from_str(input); 136 | assert!( 137 | result.is_err(), 138 | "Could parse USB speed for '{input}' while we don't expect that" 139 | ); 140 | } 141 | } 142 | 143 | #[test] 144 | fn display_usb_link_speeds() { 145 | let map = HashMap::from([ 146 | (UsbSpeed::Usb1_0, "USB 1.0 (1.5 Mb/s)"), 147 | (UsbSpeed::Usb1_1(12), "USB 1.1 (12 Mb/s)"), 148 | (UsbSpeed::Usb2_0(480), "USB 2.0 (480 Mb/s)"), 149 | (UsbSpeed::Usb3_0(5_000), "USB 3.0 (5 Gb/s)"), 150 | (UsbSpeed::Usb3_1(10_000), "USB 3.1 (10 Gb/s)"), 151 | (UsbSpeed::Usb3_2(20_000), "USB 3.2 (20 Gb/s)"), 152 | (UsbSpeed::Usb4(40_000), "USB4 (40 Gb/s)"), 153 | (UsbSpeed::Usb4_2_0(80_000), "USB4 2.0 (80 Gb/s)"), 154 | (UsbSpeed::Usb4_2_0(120_000), "USB4 2.0 (120 Gb/s)"), 155 | ]); 156 | 157 | for input in map.keys() { 158 | let result = input.to_string(); 159 | let expected = map[input]; 160 | pretty_assertions::assert_str_eq!(expected, result); 161 | } 162 | } 163 | } 164 | --------------------------------------------------------------------------------