diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..b7efcee --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,4 @@ +# https://github.com/rui314/mold?tab=readme-ov-file#how-to-use +[target.'cfg(target_os = "linux")'] +linker = "clang" +rustflags = ["-C", "link-arg=-fuse-ld=mold"] diff --git a/.github/workflows/hotpath-comment.yml b/.github/workflows/hotpath-comment.yml index 4e072fe..ebeb924 100644 --- a/.github/workflows/hotpath-comment.yml +++ b/.github/workflows/hotpath-comment.yml @@ -17,10 +17,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Download profiling results - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v6 with: name: hotpath-results path: /tmp/ diff --git a/.github/workflows/hotpath-profile.yml b/.github/workflows/hotpath-profile.yml index 1b4ce26..eeca3ec 100644 --- a/.github/workflows/hotpath-profile.yml +++ b/.github/workflows/hotpath-profile.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Checkout PR HEAD - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -53,7 +53,7 @@ jobs: echo '${{ github.event.pull_request.number }}' > /tmp/pr_number.txt - name: Upload profiling results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: hotpath-results path: | diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b742a24..5f435fb 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -24,7 +24,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: "Setup Rust toolchain" uses: actions-rust-lang/setup-rust-toolchain@v1 diff --git a/.gitignore b/.gitignore index 8ea0ee8..8799178 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target result* +/.direnv diff --git a/Cargo.lock b/Cargo.lock index 88deaeb..49e7cf4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,9 +280,9 @@ dependencies = [ [[package]] name = "criterion" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0dfe5e9e71bdcf4e4954f7d14da74d1cdb92a3a07686452d1509652684b1aab" +checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf" dependencies = [ "alloca", "anes", @@ -305,9 +305,9 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de36c2bee19fba779808f92bf5d9b0fa5a40095c277aba10c458a12b35d21d6" +checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4" dependencies = [ "cast", "itertools", @@ -557,9 +557,9 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hotpath" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08382b985a19a79d95d35e2e201b02cc4b99efe2f47d82f3fd4301bb0005bb68" +checksum = "4b0a2c66c081fe3684a54a7e5d059c9d9ad6b3ee5ccea14f6e4f056dbd77becf" dependencies = [ "arc-swap", "base64", @@ -586,9 +586,9 @@ dependencies = [ [[package]] name = "hotpath-macros" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d618063f89423ebe079a69f5435a13d4909219d4e359757118b75fd05ae65d0" +checksum = "a38fa43ca80cf906cd05127e490d740a51abb38316db7bce9d95e89724a81761" dependencies = [ "proc-macro2", "quote", @@ -786,9 +786,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libredox" @@ -832,7 +832,7 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "microfetch" -version = "0.4.11" +version = "0.4.12" dependencies = [ "criterion", "hotpath", diff --git a/Cargo.toml b/Cargo.toml index f7a2f81..0c70446 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "microfetch" -version = "0.4.11" +version = "0.4.12" edition = "2024" [lib] @@ -12,11 +12,11 @@ name = "microfetch" path = "src/main.rs" [dependencies] -hotpath = { optional = true, version = "0.7.5" } -libc = "0.2.177" +hotpath = { optional = true, version = "0.8.0" } +libc = "0.2.178" [dev-dependencies] -criterion = "0.8.0" +criterion = "0.8.1" [features] hotpath = [ "dep:hotpath", "hotpath/hotpath" ] diff --git a/README.md b/README.md index aa299ce..8f59640 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,11 @@
-

- Microfetch -

-

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

+

Microfetch

+

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


Synopsis
- Features | Motivation
+ Features | Motivation
| Benchmarks
Installation
@@ -43,7 +39,7 @@ on your system: it is pretty _[fast](#benchmarks)_... - Fast - Really fast - Minimal dependencies -- Tiny binary (~370kb) +- Tiny binary (~370kb [^1]) - Actually really fast - Cool NixOS logo (other, inferior, distros are not supported) - Reliable detection of following info: @@ -60,36 +56,47 @@ on your system: it is pretty _[fast](#benchmarks)_... - Did I mention fast? - Respects [`NO_COLOR` spec](https://no-color.org/) +[^1]: With the Mold linker, which is enabled by default in the Flake package, + the binary size is roughly 350kb. That's nearly 20kb reduction in size :) + ## Motivation -Fastfetch, as its name probably hinted, is a very fast fetch tool written in C. -However, I am not interested in _any_ of its additional features, and I'm not -interested in its configuration options. Sure I can _configure_ it when I -dislike the defaults, but how often would I really change the configuration... +[Rube-Goldmark Machine]: https://en.wikipedia.org/wiki/Rube_Goldberg_machine -Microfetch is my response to this problem. It is an _even faster_ fetch tool -that I would've written in Bash and put in my `~/.bashrc` but is _actually_ -incredibly fast because it opts out of all the customization options provided by -tools such as Fastfetch. Ultimately, it's a small, opinionated binary with a -nice size that doesn't bother me, and incredible speed. Customization? No thank -you. I cannot re-iterate it enough, Microfetch is _annoyingly fast_. +Fastfetch, as its name _probably_ already hinted, is a very fast fetch tool +written in C. I used to use Fastfetch on my systems, but I eventually came to +the realization that I am _not interested in any of its additional features_. I +don't use Sixel, I don't change my configuration more than maybe once a year and +I don't even display most of the fields that it does. Sure the configurability +is nice and I can configure the defaults that I do not like but how often do I +really do that? -The project is written in Rust, which comes at the cost of "bloated" dependency -trees and the increased build times, but we make an extended effort to keep the -dependencies minimal and build times managable. The latter is also very easily -mitigated with Nix's binary cache systems. Since Microfetch is already in -Nixpkgs, you are recommended to use it to utilize the binary cache properly. The -usage of Rust _is_ nice, however, since it provides us with incredible tooling -and a very powerful language that allows for Microfetch to be as fast as -possible. Sure C could've been used here as well, but do you think I hate -myself? +Since I already enjoy programming challenges, and don't use a fetch program that +often, I eventually came to try and answer the question _how fast can I make my +fetch script?_ It is an _even faster_ fetch tool that I would've written in Bash +and put in my `~/.bashrc` but is _actually_ incredibly fast because it opts out +of all the customization options provided by tools such as Fastfetch. Since +Fetch scripts are kind of a coming-of-age ritual for most Linux users, I've +decided to use it on my system. You also might be interested if you like the +defaults and like speed. -> [!IMPORTANT] -> **Update as of November 30th, 2025**: -> -> Microfetch now inlines handwritten assembly for even better performance. I -> know I previously said I do not hate myself but I'm beginning to suspect this -> is no longer the case. Enjoy the performance benefits! +Ultimately, it's a small, opinionated binary with a nice size that doesn't +bother me, and incredible speed. Customization? No thank you. I cannot +re-iterate it enough, Microfetch is _annoyingly fast_. It does not, however, +solve a technical problem. The "problem" Microfetch solves is entirely +self-imposed. On the matter of _size_, the project is written in Rust, which +comes at the cost of "bloated" dependency trees and the increased build times, +but we make an extended effort to keep the dependencies minimal and build times +managable. The latter is also very easily mitigated with Nix's binary cache +systems. Since Microfetch is already in Nixpkgs, you are recommended to use it +to utilize the binary cache properly. The usage of Rust _is_ nice, however, +since it provides us with incredible tooling and a very powerful language that +allows for Microfetch to be as fast as possible. ~~Sure C could've been used +here as well, but do you think I hate myself?~~ Microfetch now features +handwritten assembly to unsafely optimize some areas. In hindsight you all +should have seen this coming. Is it faster? Yes. + +Also see: [Rube-Goldmark Machine] ## Benchmarks @@ -197,17 +204,31 @@ You can't. ### Why? -Customization, of any kind, is expensive: I could try reading environment -variables, parse command-line arguments or read a configuration file but all of -those increment execution time and resource consumption by a lot. +Customization, of most kinds, are expensive: I could try reading environment +variables, parse command-line arguments or read a configuration file to allow +configuring various fields but those inflate execution time and the resource +consumption by a lot. Since Microfetch is closer to a code golf challenge than a +program that attempts to fill a gap, I have elected not to make this trade. ### Really? -To be fair, you _can_ customize Microfetch by, well, patching it. It's not the -best way per se, but it will be the only way that does not compromise on speed. +[main module]: ./src/main.rs +[discussions tab]: https://github.com/NotAShelf/microfetch/discussions + +To be fair, you _can_ customize Microfetch by, well, patching it. It is +certainly not the easiest way of doing so but if you are planning to change +something in Microfetch, patching is the best way to go. It will also the only +way that does not compromise on speed, unless you patch in bad code. Various +users have adapted Microfetch to their distribution by patching the +[main module] and inserting the logo of their choice. This is also the best way +to go if you plan to make small changes. If your changes are not small, you +might want to look for a program that is designed to be customizable; Microfetch +is built for maximum performance. The Nix package allows passing patches in a streamlined manner by passing -`.overrideAttrs` to the derivation. +`.overrideAttrs` to the derivation. You can apply your patches in `patches` and +share your derivations with people. Feel free to use the [discussions tab] to +share your own variants of Microfetch! ## Contributing @@ -219,13 +240,22 @@ Contributions that help improve performance in specific areas of Microfetch are welcome. Though, prepare to be bombarded with questions if your changes are large. -## Hacking +### Hacking -A Nix flake is provided. `nix develop` to get started. Direnv users may simply -run `direnv allow` to get started. +A Nix flake is provided. You may use `nix develop` to get started. Direnv users +may instead run `direnv allow` to get a complete environment with shell +integration. -Non-nix users will need `cargo` and `gcc` installed on their system, see -`Cargo.toml` for available release profiles. +Non-Nix user will need `cargo`, `clang` and `mold` installed on their system to +build Microfetch. As Mold seems to yield _slightly_ better results than the +default linker, it has been set as the default in `.cargo/config.toml` for +x86-64 Linux. You may override those defaults using the `RUSTFLAGS` environment +variable. For example: + +```sh +# Use ld instead of Mold +$ RUSTFLAGS="-C linker=/path/to/ld.lld" cargo build +``` ## Thanks @@ -242,6 +272,7 @@ person about current issues. To list a few, special thanks to: - [@sioodmy](https://github.com/sioodmy) - Being cute - [@mewoocat](https://github.com/mewoocat) - The awesome NixOS logo ASCII used in Microfetch +- [@uzaaft](https://github.com/uzaaft) - Helping me going faster Additionally a big thank you to everyone who used, talked about or criticized Microfetch. I might have missed your name here, but you have my thanks. diff --git a/flake.nix b/flake.nix index ed36872..90978a2 100644 --- a/flake.nix +++ b/flake.nix @@ -10,9 +10,12 @@ forEachSystem = nixpkgs.lib.genAttrs systems; pkgsForEach = nixpkgs.legacyPackages; in { - packages = forEachSystem (system: { + packages = forEachSystem (system: let + pkgs = pkgsForEach.${system}; + in { default = self.packages.${system}.microfetch; - microfetch = pkgsForEach.${system}.callPackage ./nix/package.nix {}; + microfetch = pkgs.callPackage ./nix/package.nix {}; + microfetch-mold = pkgs.callPackage ./nix/package.nix {useMold = true;}; }); devShells = forEachSystem (system: { diff --git a/nix/package.nix b/nix/package.nix index dca3cc2..052f576 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -1,14 +1,22 @@ { lib, - rustPlatform, + stdenv, stdenvAdapters, + rustPlatform, llvm, + useMold ? stdenv.isLinux && !stdenv.hostPlatform.isAarch, }: let toml = (lib.importTOML ../Cargo.toml).package; pname = toml.name; inherit (toml) version; + + # Select stdenv based on useMold flag + stdenv = + if useMold + then stdenvAdapters.useMoldLinker llvm.stdenv + else llvm.stdenv; in - rustPlatform.buildRustPackage.override {stdenv = stdenvAdapters.useMoldLinker llvm.stdenv;} { + rustPlatform.buildRustPackage.override {inherit stdenv;} { inherit pname version; src = let fs = lib.fileset; @@ -26,7 +34,14 @@ in cargoLock.lockFile = ../Cargo.lock; enableParallelBuilding = true; - env.RUSTFLAGS = "-C link-arg=-fuse-ld=mold"; + buildNoDefaultFeatures = true; + doCheck = false; + + # Only set RUSTFLAGS for mold if useMold is enabled + env = lib.optionalAttrs useMold { + CARGO_LINKER = "clang"; + RUSTFLAGS = "-C link-arg=-fuse-ld=mold"; + }; meta = { description = "Microscopic fetch script in Rust, for NixOS systems"; diff --git a/nix/shell.nix b/nix/shell.nix index 111b803..ae65e10 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -1,27 +1,31 @@ { mkShell, + cargo, + rustc, + mold, + clang, rust-analyzer-unwrapped, rustfmt, clippy, - cargo, taplo, - rustc, rustPlatform, gnuplot, }: mkShell { + name = "microfetch"; strictDeps = true; - nativeBuildInputs = [ cargo rustc + mold + clang rust-analyzer-unwrapped (rustfmt.override {asNightly = true;}) clippy taplo - gnuplot # For Criterion.rs plots + gnuplot # for Criterion.rs plots ]; env.RUST_SRC_PATH = "${rustPlatform.rustLibSrc}"; diff --git a/src/colors.rs b/src/colors.rs index 07ab9bd..7c65944 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -1,4 +1,4 @@ -use std::{env, sync::LazyLock}; +use std::sync::LazyLock; pub struct Colors { pub reset: &'static str, @@ -37,8 +37,8 @@ impl Colors { } pub static COLORS: LazyLock = LazyLock::new(|| { - // Check for NO_COLOR once at startup - let is_no_color = env::var("NO_COLOR").is_ok(); + const NO_COLOR: *const libc::c_char = c"NO_COLOR".as_ptr(); + let is_no_color = unsafe { !libc::getenv(NO_COLOR).is_null() }; Colors::new(is_no_color) }); diff --git a/src/desktop.rs b/src/desktop.rs index 561be03..501e967 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -1,22 +1,27 @@ -use std::fmt::Write; +use std::{ffi::CStr, 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 desktop_str = match desktop_env { - Err(_) => "Unknown", - Ok(ref s) if s.starts_with("none+") => &s[5..], - Ok(ref s) => s.as_str(), + let desktop_str = unsafe { + let ptr = libc::getenv(c"XDG_CURRENT_DESKTOP".as_ptr()); + if ptr.is_null() { + "Unknown" + } else { + let s = CStr::from_ptr(ptr).to_str().unwrap_or("Unknown"); + s.strip_prefix("none+").unwrap_or(s) + } }; - let backend_str = match display_backend { - Err(_) => "Unknown", - Ok(ref s) if s.is_empty() => "Unknown", - Ok(ref s) => s.as_str(), + let backend_str = unsafe { + let ptr = libc::getenv(c"XDG_SESSION_TYPE".as_ptr()); + if ptr.is_null() { + "Unknown" + } else { + let s = CStr::from_ptr(ptr).to_str().unwrap_or("Unknown"); + if s.is_empty() { "Unknown" } else { s } + } }; // Pre-calculate capacity: desktop_len + " (" + backend_len + ")" diff --git a/src/main.rs b/src/main.rs index 4e664bb..e29d6d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ mod syscall; mod system; mod uptime; -use std::io::{Write, stdout}; +use std::io::{self, Cursor, Write}; pub use microfetch_lib::UtsName; @@ -81,16 +81,32 @@ fn print_system_info( let cyan = COLORS.cyan; let blue = COLORS.blue; let reset = COLORS.reset; - let system_info = format!(" + + let mut buf = [0u8; 2048]; + let mut cursor = Cursor::new(&mut buf[..]); + + write!( + cursor, + " {cyan} ▟█▖ {blue}▝█▙ ▗█▛ {user_info} ~{reset} {cyan} ▗▄▄▟██▄▄▄▄▄{blue}▝█▙█▛ {cyan}▖ {cyan} {blue}System{reset}  {os_name} {cyan} ▀▀▀▀▀▀▀▀▀▀▀▘{blue}▝██ {cyan}▟█▖ {cyan} {blue}Kernel{reset}  {kernel_version} {blue} ▟█▛ {blue}▝█▘{cyan}▟█▛ {cyan} {blue}Shell{reset}  {shell} {blue}▟█████▛ {cyan}▟█████▛ {cyan} {blue}Uptime{reset}  {uptime} {blue} ▟█▛{cyan}▗█▖ {cyan}▟█▛ {cyan} {blue}Desktop{reset}  {desktop} - {blue} ▝█▛ {cyan}██▖{blue}▗▄▄▄▄▄▄▄▄▄▄▄ {cyan} {blue}Memory{reset}  {memory_usage} + {blue} ▝█▛ {cyan}██▖{blue}▗▄▄▄▄▄▄▄▄▄▄▄ {cyan}󰍛 {blue}Memory{reset}  {memory_usage} {blue} ▝ {cyan}▟█▜█▖{blue}▀▀▀▀▀██▛▀▀▘ {cyan}󱥎 {blue}Storage (/){reset}  {storage} - {cyan} ▟█▘ ▜█▖ {blue}▝█▛ {cyan} {blue}Colors{reset}  {colors}\n"); + {cyan} ▟█▘ ▜█▖ {blue}▝█▛ {cyan} {blue}Colors{reset}  {colors}\n\n" + )?; - Ok(stdout().write_all(system_info.as_bytes())?) + let len = cursor.position() as usize; + // Direct syscall to avoid stdout buffering allocation + let written = unsafe { libc::write(libc::STDOUT_FILENO, buf.as_ptr().cast(), len) }; + if written < 0 { + return Err(io::Error::last_os_error().into()); + } + if written as usize != len { + return Err(io::Error::new(io::ErrorKind::WriteZero, "partial write to stdout").into()); + } + Ok(()) } diff --git a/src/system.rs b/src/system.rs index 24406ec..ba8fe79 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,11 +1,18 @@ -use std::{env, fmt::Write as _, io, mem::MaybeUninit}; +use std::{ffi::CStr, fmt::Write as _, io, mem::MaybeUninit}; use crate::{UtsName, colors::COLORS, syscall::read_file_fast}; #[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 username = unsafe { + let ptr = libc::getenv(c"USER".as_ptr()); + if ptr.is_null() { + "unknown_user" + } else { + CStr::from_ptr(ptr).to_str().unwrap_or("unknown_user") + } + }; let hostname = utsname.nodename().to_str().unwrap_or("unknown_host"); let capacity = COLORS.yellow.len() @@ -18,7 +25,7 @@ pub fn get_username_and_hostname(utsname: &UtsName) -> String { let mut result = String::with_capacity(capacity); result.push_str(COLORS.yellow); - result.push_str(&username); + result.push_str(username); result.push_str(COLORS.red); result.push('@'); result.push_str(COLORS.green); @@ -31,15 +38,17 @@ pub fn get_username_and_hostname(utsname: &UtsName) -> String { #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_shell() -> String { - let shell_path = - env::var("SHELL").unwrap_or_else(|_| "unknown_shell".to_owned()); + unsafe { + let ptr = libc::getenv(c"SHELL".as_ptr()); + if ptr.is_null() { + return "unknown_shell".into(); + } - // Find last '/' and get the part after it, avoiding allocation - shell_path - .rsplit('/') - .next() - .unwrap_or("unknown_shell") - .to_owned() + let bytes = CStr::from_ptr(ptr).to_bytes(); + let start = bytes.iter().rposition(|&b| b == b'/').map_or(0, |i| i + 1); + let name = std::str::from_utf8_unchecked(&bytes[start..]); + name.into() + } } /// Gets the root disk usage information. @@ -106,7 +115,7 @@ pub fn get_memory_usage() -> Result { 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 buffer = [0u8; 1024]; // Use fast syscall-based file reading let bytes_read = read_file_fast("/proc/meminfo", &mut buffer)?;