diff --git a/.github/workflows/hotpath-comment.yml b/.github/workflows/hotpath-comment.yml deleted file mode 100644 index 8f83d00..0000000 --- a/.github/workflows/hotpath-comment.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Hotpath Comment - -on: - workflow_run: - workflows: ["Hotpath Profile"] - types: - - completed - -permissions: - pull-requests: write - -jobs: - comment: - runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' }} - - steps: - - name: Download profiling results - uses: actions/download-artifact@v4 - with: - name: hotpath-results - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ github.event.workflow_run.id }} - - - name: Read PR number - id: pr - run: echo "number=$(cat pr_number.txt)" >> $GITHUB_OUTPUT - - - name: Setup Rust - uses: actions-rust-lang/setup-rust-toolchain@v1 - - - name: Install hotpath CLI - run: cargo install hotpath - - - name: Post timing comparison comment - run: | - hotpath profile-pr \ - --head-metrics head-timing.json \ - --base-metrics base-timing.json \ - --github-token ${{ secrets.GITHUB_TOKEN }} \ - --pr-number ${{ steps.pr.outputs.number }} - - - name: Post allocation comparison comment - run: | - hotpath profile-pr \ - --head-metrics head-alloc.json \ - --base-metrics base-alloc.json \ - --github-token ${{ secrets.GITHUB_TOKEN }} \ - --pr-number ${{ steps.pr.outputs.number }} diff --git a/.github/workflows/hotpath-profile.yml b/.github/workflows/hotpath-profile.yml deleted file mode 100644 index b367ca2..0000000 --- a/.github/workflows/hotpath-profile.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Hotpath Profile - -on: - pull_request: - branches: [ "main" ] - -env: - CARGO_TERM_COLOR: always - -jobs: - profile: - runs-on: ubuntu-latest - - steps: - - name: Checkout PR HEAD - uses: actions/checkout@v4 - - - name: Setup Rust - uses: actions-rust-lang/setup-rust-toolchain@v1 - - - name: Run timing profiling on HEAD - env: - HOTPATH_JSON: "true" - run: | - cargo run --features='hotpath' 2>&1 | grep '^{"hotpath_profiling_mode"' > head-timing.json - - - name: Run allocation profiling on HEAD - env: - HOTPATH_JSON: "true" - run: | - cargo run --features='hotpath,hotpath-alloc-count-total' 2>&1 | grep '^{"hotpath_profiling_mode"' > head-alloc.json - - - name: Checkout base branch - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.base.sha }} - - - name: Run timing profiling on base - env: - HOTPATH_JSON: "true" - run: | - cargo run --features='hotpath' 2>&1 | grep '^{"hotpath_profiling_mode"' > base-timing.json - - - name: Run allocation profiling on base - env: - HOTPATH_JSON: "true" - run: | - cargo run --features='hotpath,hotpath-alloc-count-total' 2>&1 | grep '^{"hotpath_profiling_mode"' > base-alloc.json - - - name: Save PR number - run: echo "${{ github.event.number }}" > pr_number.txt - - - name: Upload profiling results - uses: actions/upload-artifact@v4 - with: - name: hotpath-results - path: | - head-timing.json - head-alloc.json - base-timing.json - base-alloc.json - pr_number.txt - retention-days: 1 diff --git a/Cargo.lock b/Cargo.lock index 0147746..c7bf6b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,6 +143,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chunked_transfer" version = "1.5.0" @@ -684,7 +690,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -726,9 +732,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libredox" @@ -766,11 +772,12 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "microfetch" -version = "0.4.11" +version = "0.4.10" dependencies = [ "criterion", "hotpath", "libc", + "nix", ] [[package]] @@ -783,6 +790,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "num-conv" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index e95ef5c..c97b58b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "microfetch" -version = "0.4.11" +version = "0.4.10" edition = "2024" [lib] @@ -12,8 +12,9 @@ name = "microfetch" path = "src/main.rs" [dependencies] -hotpath = { optional = true, version = "0.6.0" } -libc = "0.2.177" +hotpath = { optional = true, version = "0.6" } +libc = "0.2.175" +nix = { default-features = false, features = [ "fs", "hostname", "feature" ], version = "0.30.1" } [dev-dependencies] criterion = "0.7" diff --git a/README.md b/README.md index a40e444..28d6338 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,17 @@ - -
https://deps.rs/repo/github/notashelf/microfetch - stars + +
-
-

- Microfetch -

-

- Microscopic fetch tool in Rust, for NixOS systems, with special emphasis on speed -

-
- Synopsis
- Features | Motivation
- Installation -
-
+

Microfetch

-## Synopsis - -[fastfetch]: https://github.com/fastfetch-cli/fastfetch - -Stupidly small and simple, laughably fast and pretty fetch tool. Written in Rust -for speed and ease of maintainability. Runs in a _fraction of a millisecond_ and -displays _most_ of the nonsense you'd see posted on r/unixporn or other internet -communities. Aims to replace [fastfetch] on my personal system, but -[probably not yours](#customizing). Though, you are more than welcome to use it -on your system: it is pretty _[fast](#benchmarks)_... +Stupidly simple, laughably fast fetch tool. Written in Rust for speed and ease +of maintainability. Runs in a _fraction of a millisecond_ and displays _most_ of +the nonsense you'd see posted on r/unixporn or other internet communities. Aims +to replace [fastfetch](https://github.com/fastfetch-cli/fastfetch) on my +personal system, but [probably not yours](#customizing). Though, you are more +than welcome to use it on your system: it's pretty [fast](#benchmarks)...

[!NOTE] > You will need a Nerdfonts patched font installed, and for your terminal -> emulator to support said font. Microfetch uses nerdfonts glyphs by default, -> but this can be changed by [patching the program](#customizing). +> emulator to support said font. Microfetch uses nerdfonts glyphs by default. Microfetch is packaged in [nixpkgs](https://github.com/nixos/nixpkgs). It can be installed by adding `pkgs.microfetch` to your `environment.systemPackages`. diff --git a/benches/benchmark.rs b/benches/benchmark.rs index 8d62082..33dc1b9 100644 --- a/benches/benchmark.rs +++ b/benches/benchmark.rs @@ -1,6 +1,5 @@ use criterion::{Criterion, criterion_group, criterion_main}; use microfetch_lib::{ - UtsName, colors::print_dots, desktop::get_desktop_info, release::{get_os_pretty_name, get_system_info}, @@ -14,7 +13,7 @@ use microfetch_lib::{ }; fn main_benchmark(c: &mut Criterion) { - let utsname = UtsName::uname().expect("Failed to get uname"); + let utsname = nix::sys::utsname::uname().expect("lol"); c.bench_function("user_info", |b| { b.iter(|| get_username_and_hostname(&utsname)); }); diff --git a/nix/package.nix b/nix/package.nix index dca3cc2..5bb387c 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -20,7 +20,6 @@ in (fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /src)) (s + /Cargo.lock) (s + /Cargo.toml) - (s + /benches) ]; }; diff --git a/src/colors.rs b/src/colors.rs index 07ab9bd..53b12f6 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -37,7 +37,7 @@ impl Colors { } pub static COLORS: LazyLock = LazyLock::new(|| { - // Check for NO_COLOR once at startup + // check for NO_COLOR once at startup let is_no_color = env::var("NO_COLOR").is_ok(); Colors::new(is_no_color) }); @@ -45,37 +45,14 @@ pub static COLORS: LazyLock = LazyLock::new(|| { #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn print_dots() -> String { - // Pre-calculate capacity: 6 color codes + " " (glyph + 2 spaces) per color - const GLYPH: &str = ""; - let capacity = COLORS.blue.len() - + COLORS.cyan.len() - + COLORS.green.len() - + COLORS.yellow.len() - + COLORS.red.len() - + COLORS.magenta.len() - + COLORS.reset.len() - + (GLYPH.len() + 2) * 6; - - let mut result = String::with_capacity(capacity); - result.push_str(COLORS.blue); - result.push_str(GLYPH); - result.push_str(" "); - result.push_str(COLORS.cyan); - result.push_str(GLYPH); - result.push_str(" "); - result.push_str(COLORS.green); - result.push_str(GLYPH); - result.push_str(" "); - result.push_str(COLORS.yellow); - result.push_str(GLYPH); - result.push_str(" "); - result.push_str(COLORS.red); - result.push_str(GLYPH); - result.push_str(" "); - result.push_str(COLORS.magenta); - result.push_str(GLYPH); - result.push_str(" "); - result.push_str(COLORS.reset); - - result + format!( + "{} {} {} {} {} {} {}", + COLORS.blue, + COLORS.cyan, + COLORS.green, + COLORS.yellow, + COLORS.red, + COLORS.magenta, + COLORS.reset, + ) } diff --git a/src/desktop.rs b/src/desktop.rs index 561be03..7d3733a 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -1,37 +1,30 @@ -use std::fmt::Write; - #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_desktop_info() -> String { // Retrieve the environment variables and handle Result types let desktop_env = std::env::var("XDG_CURRENT_DESKTOP"); - let display_backend = std::env::var("XDG_SESSION_TYPE"); + let display_backend_result = std::env::var("XDG_SESSION_TYPE"); - let desktop_str = match desktop_env { - Err(_) => "Unknown", - Ok(ref s) if s.starts_with("none+") => &s[5..], - Ok(ref s) => s.as_str(), - }; - - let backend_str = match display_backend { - Err(_) => "Unknown", - Ok(ref s) if s.is_empty() => "Unknown", - Ok(ref s) => s.as_str(), - }; - - // Pre-calculate capacity: desktop_len + " (" + backend_len + ")" - // Capitalize first char needs temporary allocation only if backend exists - let mut result = - String::with_capacity(desktop_str.len() + backend_str.len() + 3); - result.push_str(desktop_str); - result.push_str(" ("); - - // Capitalize first character of backend - if let Some(first_char) = backend_str.chars().next() { - let _ = write!(result, "{}", first_char.to_ascii_uppercase()); - result.push_str(&backend_str[first_char.len_utf8()..]); + // Capitalize the first letter of the display backend value + let mut display_backend = display_backend_result.unwrap_or_default(); + if let Some(c) = display_backend.as_mut_str().get_mut(0..1) { + c.make_ascii_uppercase(); } - result.push(')'); - result + // Trim "none+" from the start of desktop_env if present + // Use "Unknown" if desktop_env is empty or has an error + let desktop_env = match desktop_env { + Err(_) => "Unknown".to_owned(), + Ok(s) => s.trim_start_matches("none+").to_owned(), + }; + + // Handle the case where display_backend might be empty after capitalization + let display_backend = if display_backend.is_empty() { + "Unknown" + } else { + &display_backend + } + .to_owned(); + + format!("{desktop_env} ({display_backend})") } diff --git a/src/lib.rs b/src/lib.rs index 8c8a6ba..4ba33f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,45 +1,5 @@ pub mod colors; pub mod desktop; pub mod release; -pub mod syscall; pub mod system; pub mod uptime; - -use std::mem::MaybeUninit; - -/// Wrapper for `libc::utsname` with safe accessor methods -pub struct UtsName(libc::utsname); - -impl UtsName { - /// Calls `uname` syscall and returns a `UtsName` wrapper - /// - /// # Errors - /// Returns an error if the uname syscall fails - pub fn uname() -> Result { - let mut uts = MaybeUninit::uninit(); - if unsafe { libc::uname(uts.as_mut_ptr()) } != 0 { - return Err(std::io::Error::last_os_error()); - } - Ok(Self(unsafe { uts.assume_init() })) - } - - #[must_use] - pub const fn nodename(&self) -> &std::ffi::CStr { - unsafe { std::ffi::CStr::from_ptr(self.0.nodename.as_ptr()) } - } - - #[must_use] - pub const fn sysname(&self) -> &std::ffi::CStr { - unsafe { std::ffi::CStr::from_ptr(self.0.sysname.as_ptr()) } - } - - #[must_use] - pub const fn release(&self) -> &std::ffi::CStr { - unsafe { std::ffi::CStr::from_ptr(self.0.release.as_ptr()) } - } - - #[must_use] - pub const fn machine(&self) -> &std::ffi::CStr { - unsafe { std::ffi::CStr::from_ptr(self.0.machine.as_ptr()) } - } -} diff --git a/src/main.rs b/src/main.rs index 4e664bb..5ba7810 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,11 @@ mod colors; mod desktop; mod release; -mod syscall; mod system; mod uptime; use std::io::{Write, stdout}; -pub use microfetch_lib::UtsName; - use crate::{ colors::print_dots, desktop::get_desktop_info, @@ -27,7 +24,7 @@ fn main() -> Result<(), Box> { if Some("--version") == std::env::args().nth(1).as_deref() { println!("Microfetch {}", env!("CARGO_PKG_VERSION")); } else { - let utsname = UtsName::uname()?; + let utsname = nix::sys::utsname::uname()?; let fields = Fields { user_info: get_username_and_hostname(&utsname), os_name: get_os_pretty_name()?, diff --git a/src/release.rs b/src/release.rs index 8d3232e..2f1338c 100644 --- a/src/release.rs +++ b/src/release.rs @@ -1,69 +1,37 @@ -use std::{fmt::Write as _, io}; +use std::{ + fs::File, + io::{self, BufRead, BufReader}, +}; -use crate::{UtsName, syscall::read_file_fast}; +use nix::sys::utsname::UtsName; #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_system_info(utsname: &UtsName) -> String { - let sysname = utsname.sysname().to_str().unwrap_or("Unknown"); - let release = utsname.release().to_str().unwrap_or("Unknown"); - let machine = utsname.machine().to_str().unwrap_or("Unknown"); - - // Pre-allocate capacity: sysname + " " + release + " (" + machine + ")" - let capacity = sysname.len() + 1 + release.len() + 2 + machine.len() + 1; - let mut result = String::with_capacity(capacity); - - write!(result, "{sysname} {release} ({machine})").unwrap(); - result + format!( + "{} {} ({})", + utsname.sysname().to_str().unwrap_or("Unknown"), + utsname.release().to_str().unwrap_or("Unknown"), + utsname.machine().to_str().unwrap_or("Unknown") + ) } -/// Gets the pretty name of the OS from `/etc/os-release`. -/// -/// # Errors -/// -/// Returns an error if `/etc/os-release` cannot be read. #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_os_pretty_name() -> Result { - // Fast byte-level scanning for PRETTY_NAME= - const PREFIX: &[u8] = b"PRETTY_NAME="; + let file = File::open("/etc/os-release")?; + let reader = BufReader::new(file); - let mut buffer = [0u8; 1024]; - - // Use fast syscall-based file reading - let bytes_read = read_file_fast("/etc/os-release", &mut buffer)?; - let content = &buffer[..bytes_read]; - - let mut offset = 0; - - while offset < content.len() { - let remaining = &content[offset..]; - - // Find newline or end - let line_end = remaining - .iter() - .position(|&b| b == b'\n') - .unwrap_or(remaining.len()); - let line = &remaining[..line_end]; - - if line.starts_with(PREFIX) { - let value = &line[PREFIX.len()..]; - - // Strip quotes if present - let trimmed = if value.len() >= 2 - && value[0] == b'"' - && value[value.len() - 1] == b'"' + for line in reader.lines() { + let line = line?; + if let Some(pretty_name) = line.strip_prefix("PRETTY_NAME=") { + if let Some(trimmed) = pretty_name + .strip_prefix('"') + .and_then(|s| s.strip_suffix('"')) { - &value[1..value.len() - 1] - } else { - value - }; - - // Convert to String - should be valid UTF-8 - return Ok(String::from_utf8_lossy(trimmed).into_owned()); + return Ok(trimmed.to_owned()); + } + return Ok(pretty_name.to_owned()); } - - offset += line_end + 1; } - Ok("Unknown".to_owned()) } diff --git a/src/syscall.rs b/src/syscall.rs deleted file mode 100644 index 2776fe7..0000000 --- a/src/syscall.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! Incredibly fast syscall wrappers for using inline assembly. Serves the -//! purposes of completely bypassing Rust's standard library in favor of -//! handwritten Assembly. Is this a good idea? No. Is it fast? Yeah, but only -//! marginally. Either way it serves a purpose and I will NOT accept criticism. -//! What do you mean I wasted two whole hours to make the program only 100µs -//! faster? -//! -//! Supports `x86_64` and `aarch64` architectures. Riscv support will be -//! implemented when and ONLY WHEN I can be bothered to work on it. - -use std::io; - -/// Direct syscall to open a file -/// Returns file descriptor or -1 on error -/// -/// # Safety -/// -/// The caller must ensure: -/// - `path` points to a valid null-terminated C string -/// - The pointer remains valid for the duration of the syscall -#[inline] -#[must_use] -pub unsafe fn sys_open(path: *const u8, flags: i32) -> i32 { - #[cfg(target_arch = "x86_64")] - unsafe { - let fd: i64; - std::arch::asm!( - "syscall", - in("rax") 2i64, // SYS_open - in("rdi") path, - in("rsi") flags, - in("rdx") 0i32, // mode (not used for reading) - lateout("rax") fd, - lateout("rcx") _, - lateout("r11") _, - options(nostack) - ); - #[allow(clippy::cast_possible_truncation)] - { - fd as i32 - } - } - #[cfg(target_arch = "aarch64")] - unsafe { - let fd: i64; - std::arch::asm!( - "svc #0", - in("x8") 56i64, // SYS_openat - in("x0") -100i32, // AT_FDCWD - in("x1") path, - in("x2") flags, - in("x3") 0i32, // mode - lateout("x0") fd, - options(nostack) - ); - #[allow(clippy::cast_possible_truncation)] - { - fd as i32 - } - } - #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] - { - compile_error!("Unsupported architecture for inline assembly syscalls"); - } -} - -/// Direct syscall to read from a file descriptor -/// Returns number of bytes read or -1 on error -/// -/// # Safety -/// -/// The caller must ensure: -/// - `buf` points to a valid writable buffer of at least `count` bytes -/// - `fd` is a valid open file descriptor -#[inline] -pub unsafe fn sys_read(fd: i32, buf: *mut u8, count: usize) -> isize { - #[cfg(target_arch = "x86_64")] - unsafe { - let ret: i64; - std::arch::asm!( - "syscall", - in("rax") 0i64, // SYS_read - in("rdi") fd, - in("rsi") buf, - in("rdx") count, - lateout("rax") ret, - lateout("rcx") _, - lateout("r11") _, - options(nostack) - ); - #[allow(clippy::cast_possible_truncation)] - { - ret as isize - } - } - #[cfg(target_arch = "aarch64")] - unsafe { - let ret: i64; - std::arch::asm!( - "svc #0", - in("x8") 63i64, // SYS_read - in("x0") fd, - in("x1") buf, - in("x2") count, - lateout("x0") ret, - options(nostack) - ); - #[allow(clippy::cast_possible_truncation)] - { - ret as isize - } - } - #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] - { - compile_error!("Unsupported architecture for inline assembly syscalls"); - } -} - -/// Direct syscall to close a file descriptor -/// -/// # Safety -/// -/// The caller must ensure `fd` is a valid open file descriptor -#[inline] -#[must_use] -pub unsafe fn sys_close(fd: i32) -> i32 { - #[cfg(target_arch = "x86_64")] - unsafe { - let ret: i64; - std::arch::asm!( - "syscall", - in("rax") 3i64, // SYS_close - in("rdi") fd, - lateout("rax") ret, - lateout("rcx") _, - lateout("r11") _, - options(nostack) - ); - #[allow(clippy::cast_possible_truncation)] - { - ret as i32 - } - } - #[cfg(target_arch = "aarch64")] - unsafe { - let ret: i64; - std::arch::asm!( - "svc #0", - in("x8") 57i64, // SYS_close - in("x0") fd, - lateout("x0") ret, - options(nostack) - ); - #[allow(clippy::cast_possible_truncation)] - { - ret as i32 - } - } - #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] - { - compile_error!("Unsupported architecture for inline assembly syscalls"); - } -} - -/// Read entire file using direct syscalls. This avoids libc overhead and can be -/// significantly faster for small files. -/// -/// # Errors -/// -/// Returns an error if the file cannot be opened or read -#[inline] -pub fn read_file_fast(path: &str, buffer: &mut [u8]) -> io::Result { - const O_RDONLY: i32 = 0; - - // Use stack-allocated buffer for null-terminated path (max 256 bytes) - let path_bytes = path.as_bytes(); - if path_bytes.len() >= 256 { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "Path too long")); - } - - let mut path_buf = [0u8; 256]; - path_buf[..path_bytes.len()].copy_from_slice(path_bytes); - // XXX: Already zero-terminated since array is initialized to zeros - - unsafe { - let fd = sys_open(path_buf.as_ptr(), O_RDONLY); - if fd < 0 { - return Err(io::Error::last_os_error()); - } - - let bytes_read = sys_read(fd, buffer.as_mut_ptr(), buffer.len()); - let _ = sys_close(fd); - - if bytes_read < 0 { - return Err(io::Error::last_os_error()); - } - - #[allow(clippy::cast_sign_loss)] - { - Ok(bytes_read as usize) - } - } -} diff --git a/src/system.rs b/src/system.rs index 24406ec..f90c912 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,31 +1,29 @@ -use std::{env, fmt::Write as _, io, mem::MaybeUninit}; +use std::{ + env, + fs::File, + io::{self, Read}, +}; -use crate::{UtsName, colors::COLORS, syscall::read_file_fast}; +use nix::sys::{statvfs::statvfs, utsname::UtsName}; + +use crate::colors::COLORS; #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_username_and_hostname(utsname: &UtsName) -> String { let username = env::var("USER").unwrap_or_else(|_| "unknown_user".to_owned()); - let hostname = utsname.nodename().to_str().unwrap_or("unknown_host"); - - let capacity = COLORS.yellow.len() - + username.len() - + COLORS.red.len() - + 1 - + COLORS.green.len() - + hostname.len() - + COLORS.reset.len(); - let mut result = String::with_capacity(capacity); - - result.push_str(COLORS.yellow); - result.push_str(&username); - result.push_str(COLORS.red); - result.push('@'); - result.push_str(COLORS.green); - result.push_str(hostname); - result.push_str(COLORS.reset); - - result + let hostname = utsname + .nodename() + .to_str() + .unwrap_or("unknown_host") + .to_owned(); + format!( + "{yellow}{username}{red}@{green}{hostname}{reset}", + yellow = COLORS.yellow, + red = COLORS.red, + green = COLORS.green, + reset = COLORS.reset, + ) } #[must_use] @@ -33,34 +31,16 @@ pub fn get_username_and_hostname(utsname: &UtsName) -> String { pub fn get_shell() -> String { let shell_path = env::var("SHELL").unwrap_or_else(|_| "unknown_shell".to_owned()); - - // Find last '/' and get the part after it, avoiding allocation - shell_path - .rsplit('/') - .next() - .unwrap_or("unknown_shell") - .to_owned() + let shell_name = shell_path.rsplit('/').next().unwrap_or("unknown_shell"); + shell_name.to_owned() } -/// Gets the root disk usage information. -/// -/// # Errors -/// -/// Returns an error if the filesystem information cannot be retrieved. #[cfg_attr(feature = "hotpath", hotpath::measure)] -#[allow(clippy::cast_precision_loss)] pub fn get_root_disk_usage() -> Result { - let mut vfs = MaybeUninit::uninit(); - let path = b"/\0"; - - if unsafe { libc::statvfs(path.as_ptr().cast(), vfs.as_mut_ptr()) } != 0 { - return Err(io::Error::last_os_error()); - } - - let vfs = unsafe { vfs.assume_init() }; - let block_size = vfs.f_bsize; - let total_blocks = vfs.f_blocks; - let available_blocks = vfs.f_bavail; + let vfs = statvfs("/")?; + let block_size = vfs.block_size() as u64; + let total_blocks = vfs.blocks(); + let available_blocks = vfs.blocks_available(); let total_size = block_size * total_blocks; let used_size = total_size - (block_size * available_blocks); @@ -69,107 +49,53 @@ pub fn get_root_disk_usage() -> Result { let used_size = used_size as f64 / (1024.0 * 1024.0 * 1024.0); let usage = (used_size / total_size) * 100.0; - let mut result = String::with_capacity(64); - write!( - result, + Ok(format!( "{used_size:.2} GiB / {total_size:.2} GiB ({cyan}{usage:.0}%{reset})", cyan = COLORS.cyan, reset = COLORS.reset, - ) - .unwrap(); - - Ok(result) + )) } -/// Fast integer parsing without stdlib overhead -#[inline] -fn parse_u64_fast(s: &[u8]) -> u64 { - let mut result = 0u64; - for &byte in s { - if byte.is_ascii_digit() { - result = result * 10 + u64::from(byte - b'0'); - } else { - break; - } - } - result -} - -/// Gets the system memory usage information. -/// -/// # Errors -/// -/// Returns an error if `/proc/meminfo` cannot be read. #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_memory_usage() -> Result { #[cfg_attr(feature = "hotpath", hotpath::measure)] fn parse_memory_info() -> Result<(f64, f64), io::Error> { - let mut total_memory_kb = 0u64; - let mut available_memory_kb = 0u64; - let mut buffer = [0u8; 2048]; + let mut total_memory_kb = 0.0; + let mut available_memory_kb = 0.0; + let mut meminfo = String::with_capacity(2048); - // Use fast syscall-based file reading - let bytes_read = read_file_fast("/proc/meminfo", &mut buffer)?; - let meminfo = &buffer[..bytes_read]; + File::open("/proc/meminfo")?.read_to_string(&mut meminfo)?; - // Fast scanning for MemTotal and MemAvailable - let mut offset = 0; - let mut found_total = false; - let mut found_available = false; - - while offset < meminfo.len() && (!found_total || !found_available) { - let remaining = &meminfo[offset..]; - - // Find newline or end - let line_end = remaining - .iter() - .position(|&b| b == b'\n') - .unwrap_or(remaining.len()); - let line = &remaining[..line_end]; - - if line.starts_with(b"MemTotal:") { - // Skip "MemTotal:" and whitespace - let mut pos = 9; - while pos < line.len() && line[pos].is_ascii_whitespace() { - pos += 1; - } - total_memory_kb = parse_u64_fast(&line[pos..]); - found_total = true; - } else if line.starts_with(b"MemAvailable:") { - // Skip "MemAvailable:" and whitespace - let mut pos = 13; - while pos < line.len() && line[pos].is_ascii_whitespace() { - pos += 1; - } - available_memory_kb = parse_u64_fast(&line[pos..]); - found_available = true; + for line in meminfo.lines() { + let mut split = line.split_whitespace(); + match split.next().unwrap_or_default() { + "MemTotal:" => { + total_memory_kb = split.next().unwrap_or("0").parse().unwrap_or(0.0) + }, + "MemAvailable:" => { + available_memory_kb = + split.next().unwrap_or("0").parse().unwrap_or(0.0); + // MemTotal comes before MemAvailable, stop parsing + break; + }, + _ => (), } - - offset += line_end + 1; } - #[allow(clippy::cast_precision_loss)] - let total_memory_gb = total_memory_kb as f64 / 1024.0 / 1024.0; - #[allow(clippy::cast_precision_loss)] - let available_memory_gb = available_memory_kb as f64 / 1024.0 / 1024.0; + let total_memory_gb = total_memory_kb / 1024.0 / 1024.0; + let available_memory_gb = available_memory_kb / 1024.0 / 1024.0; let used_memory_gb = total_memory_gb - available_memory_gb; Ok((used_memory_gb, total_memory_gb)) } let (used_memory, total_memory) = parse_memory_info()?; - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] let percentage_used = (used_memory / total_memory * 100.0).round() as u64; - let mut result = String::with_capacity(64); - write!( - result, + Ok(format!( "{used_memory:.2} GiB / {total_memory:.2} GiB \ ({cyan}{percentage_used}%{reset})", cyan = COLORS.cyan, reset = COLORS.reset, - ) - .unwrap(); - - Ok(result) + )) } diff --git a/src/uptime.rs b/src/uptime.rs index 98c9207..d5253d6 100644 --- a/src/uptime.rs +++ b/src/uptime.rs @@ -1,83 +1,13 @@ use std::{io, mem::MaybeUninit}; -/// Fast integer to string conversion (no formatting overhead) -#[inline] -fn itoa(mut n: u64, buf: &mut [u8]) -> &str { - if n == 0 { - return "0"; - } - - let mut i = buf.len(); - while n > 0 { - i -= 1; - buf[i] = b'0' + (n % 10) as u8; - n /= 10; - } - - unsafe { std::str::from_utf8_unchecked(&buf[i..]) } -} - -/// Direct sysinfo syscall using inline assembly -/// -/// # Safety -/// This function uses inline assembly to make a direct syscall. -/// The caller must ensure the sysinfo pointer is valid. -#[inline] -unsafe fn sys_sysinfo(info: *mut libc::sysinfo) -> i64 { - #[cfg(target_arch = "x86_64")] - { - let ret: i64; - unsafe { - std::arch::asm!( - "syscall", - in("rax") 99_i64, // __NR_sysinfo - in("rdi") info, - out("rcx") _, - out("r11") _, - lateout("rax") ret, - options(nostack) - ); - } - ret - } - - #[cfg(target_arch = "aarch64")] - { - let ret: i64; - unsafe { - std::arch::asm!( - "svc #0", - in("x8") 179_i64, // __NR_sysinfo - in("x0") info, - lateout("x0") ret, - options(nostack) - ); - } - ret - } - - #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] - { - unsafe { libc::sysinfo(info) as i64 } - } -} - -/// Gets the current system uptime. -/// -/// # Errors -/// -/// Returns an error if the system uptime cannot be retrieved. #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_current() -> Result { let uptime_seconds = { let mut info = MaybeUninit::uninit(); - if unsafe { sys_sysinfo(info.as_mut_ptr()) } != 0 { + if unsafe { libc::sysinfo(info.as_mut_ptr()) } != 0 { return Err(io::Error::last_os_error()); } - #[allow(clippy::cast_sign_loss)] - unsafe { - info.assume_init().uptime as u64 - } + unsafe { info.assume_init().uptime as u64 } }; let days = uptime_seconds / 86400; @@ -85,24 +15,22 @@ pub fn get_current() -> Result { let minutes = (uptime_seconds / 60) % 60; let mut result = String::with_capacity(32); - let mut buf = [0u8; 20]; // Enough for u64::MAX - if days > 0 { - result.push_str(itoa(days, &mut buf)); + result.push_str(&days.to_string()); result.push_str(if days == 1 { " day" } else { " days" }); } if hours > 0 { if !result.is_empty() { result.push_str(", "); } - result.push_str(itoa(hours, &mut buf)); + result.push_str(&hours.to_string()); result.push_str(if hours == 1 { " hour" } else { " hours" }); } if minutes > 0 { if !result.is_empty() { result.push_str(", "); } - result.push_str(itoa(minutes, &mut buf)); + result.push_str(&minutes.to_string()); result.push_str(if minutes == 1 { " minute" } else { " minutes" }); } if result.is_empty() {