Compare commits

..

No commits in common. "b24e720dd81d52ef4e71ede470d7b611d5474a6d" and "9d8905354efca612a1e9fe87845293a4460e0553" have entirely different histories.

17 changed files with 331 additions and 1778 deletions

View file

@ -1,57 +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
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
hotpath profile-pr \
--repo ${{ github.repository }} \
--pr-number ${{ steps.pr.outputs.number }} \
--head-json head-timing.json \
--base-json base-timing.json \
--mode timing \
--title "⏱️ Hotpath Timing Profile"
- name: Post allocation comparison comment
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
hotpath profile-pr \
--repo ${{ github.repository }} \
--pr-number ${{ steps.pr.outputs.number }} \
--head-json head-alloc.json \
--base-json base-alloc.json \
--mode alloc \
--title "📊 Hotpath Allocation Profile"

View file

@ -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

View file

@ -1,26 +0,0 @@
condense_wildcard_suffixes = true
doc_comment_code_block_width = 80
edition = "2024" # Keep in sync with Cargo.toml.
enum_discrim_align_threshold = 60
force_explicit_abi = false
force_multiline_blocks = true
format_code_in_doc_comments = true
format_macro_matchers = true
format_strings = true
group_imports = "StdExternalCrate"
hex_literal_case = "Upper"
imports_granularity = "Crate"
imports_layout = "HorizontalVertical"
inline_attribute_width = 60
match_block_trailing_comma = true
max_width = 80
newline_style = "Unix"
normalize_comments = true
normalize_doc_attributes = true
overflow_delimited_expr = true
struct_field_align_threshold = 60
tab_spaces = 2
unstable_features = true
use_field_init_shorthand = true
use_try_shorthand = true
wrap_comments = true

View file

@ -1,13 +0,0 @@
[formatting]
align_entries = true
column_width = 100
compact_arrays = false
reorder_inline_tables = true
reorder_keys = true
[[rule]]
include = [ "**/Cargo.toml" ]
keys = [ "package" ]
[rule.formatting]
reorder_keys = false

1065
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "microfetch" name = "microfetch"
version = "0.4.11" version = "0.4.9"
edition = "2024" edition = "2024"
[lib] [lib]
@ -12,59 +12,28 @@ name = "microfetch"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
hotpath = { optional = true, version = "0.6.0" } nix = { version = "0.30", features = ["fs", "hostname", "feature"] }
libc = "0.2.177" libc = "0.2"
nix = { default-features = false, features = [ "fs", "hostname", "feature" ], version = "0.30.1" }
[dev-dependencies] [dev-dependencies]
criterion = "0.7" criterion = "0.7"
[features]
hotpath = [ "dep:hotpath", "hotpath/hotpath" ]
hotpath-alloc-bytes-total = [ "hotpath/hotpath-alloc-bytes-total" ]
hotpath-alloc-count-total = [ "hotpath/hotpath-alloc-count-total" ]
hotpath-off = [ "hotpath/hotpath-off" ]
[[bench]] [[bench]]
name = "benchmark"
harness = false harness = false
name = "benchmark"
[profile.dev] [profile.dev]
opt-level = 1 opt-level = 3
[profile.release] [profile.release]
strip = true
opt-level = "s"
lto = true
codegen-units = 1 codegen-units = 1
lto = true panic = "abort"
opt-level = "s"
panic = "abort"
strip = true
[profile.profiler] [profile.profiler]
debug = true inherits = "release"
inherits = "release" debug = true
split-debuginfo = "unpacked" split-debuginfo = "unpacked"
strip = "none" strip = "none"
[lints.clippy]
complexity = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
perf = { level = "warn", priority = -1 }
style = { level = "warn", priority = -1 }
# The lint groups above enable some less-than-desirable rules, we should manually
# enable those to keep our sanity.
absolute_paths = "allow"
arbitrary_source_item_ordering = "allow"
implicit_return = "allow"
missing_docs_in_private_items = "allow"
non_ascii_literal = "allow"
pattern_type_mismatch = "allow"
print_stdout = "allow"
question_mark_used = "allow"
similar_names = "allow"
single_call_fn = "allow"
std_instead_of_core = "allow"
too_long_first_doc_paragraph = "allow"
too_many_lines = "allow"
unused_trait_names = "allow"

141
README.md
View file

@ -1,34 +1,17 @@
<!-- markdownlint-disable MD013 MD033 MD041 -->
<div align="center"> <div align="center">
<img src="https://deps.rs/repo/github/notashelf/microfetch/status.svg" alt="https://deps.rs/repo/github/notashelf/microfetch"> <img src="https://deps.rs/repo/github/notashelf/microfetch/status.svg" alt="https://deps.rs/repo/github/notashelf/microfetch">
<img src="https://img.shields.io/github/stars/notashelf/microfetch?label=stars&color=DEA584" alt="stars"> <!-- <img src="https://img.shields.io/github/v/release/notashelf/microfetch?display_name=tag&color=DEA584"> -->
<img src="https://img.shields.io/github/stars/notashelf/microfetch?label=stars&color=DEA584">
</div> </div>
<div id="doc-begin" align="center"> <h1 align="center">Microfetch</h1>
<h1 id="header">
Microfetch
</h1>
<p>
Microscopic fetch tool in Rust, for NixOS systems, with special emphasis on speed
</p>
<br/>
<a href="#synopsis">Synopsis</a><br/>
<a href="#features">Features</a> | <a href="#motivation">Motivation</a><br/>
<a href="#installation">Installation</a>
<br/>
</div>
## Synopsis 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
[fastfetch]: https://github.com/fastfetch-cli/fastfetch 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
Stupidly small and simple, laughably fast and pretty fetch tool. Written in Rust personal system, but [probably not yours](#customizing). Though, you are more
for speed and ease of maintainability. Runs in a _fraction of a millisecond_ and than welcome to use it on your system: it's pretty [fast](#benchmarks)...
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)_...
<p align="center"> <p align="center">
<img <img
@ -43,7 +26,6 @@ on your system: it is pretty _[fast](#benchmarks)_...
- Fast - Fast
- Really fast - Really fast
- Minimal dependencies - Minimal dependencies
- Tiny binary (~410kb)
- Actually really fast - Actually really fast
- Cool NixOS logo (other, inferior, distros are not supported) - Cool NixOS logo (other, inferior, distros are not supported)
- Reliable detection of following info: - Reliable detection of following info:
@ -62,64 +44,39 @@ on your system: it is pretty _[fast](#benchmarks)_...
## Motivation ## Motivation
Fastfetch, as its name probably hinted, is a very fast fetch tool written in C. Fastfetch, as its name indicates, a very fast fetch tool written in C, however,
However, I am not interested in _any_ of its additional features, and I'm not I am not interested in any of its additional features, such as customization,
interested in its configuration options. Sure I can _configure_ it when I and I very much dislike the defaults. Microfetch is my response to this problem,
dislike the defaults, but how often would I really change the configuration... a _very fast_ fetch tool that you would normally write in Bash and put in your
`~/.bashrc` but actually _really_ fast because it opts-out of all customization
options provided by Fastfetch, and is written in Rust. Why? Because I can, and
because I prefer Rust for "structured" Bash scripts.
Microfetch is my response to this problem. It is an _even faster_ fetch tool I cannot re-iterate it enough, Microfetch is _annoyingly fast_.
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_.
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? [^1]
[^1]: Okay, maybe a little bit. One of the future goals of Microfetch is to
defer to inline Assembly for the costliest functions, but that's for a
future date and until I do that I can pretend to be sane.
## Benchmarks ## Benchmarks
Below are the benchmarks that I've used to back up my claims of Microfetch's The performance may be sometimes influenced by hardware-specific race
speed. It is fast, it is _very_ fast and that is the point of its existence. It conditions, or even your kernel configuration meaning it may (at times) depend
_could_ be faster, and it will be. Eventually. on your hardware. However, the overall trend appears to be less than 1.3ms on
any modern (2015 and after) CPU that I own. Below are the benchmarks with
Hyperfine on my desktop system. Please note that those benchmarks will not be
always kept up to date, but I will try to update the numbers as I make
Microfetch faster.
At this point in time, the performance may be sometimes influenced by | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | Written by raf? |
hardware-specific race conditions or even your kernel configuration. Which means | :----------- | -----------: | -------: | -------: | -------------: | --------------: |
that Microfetch's speed may (at times) depend on your hardware setup. However, | `microfetch` | 1.0 ± 0.1 | 0.9 | 1.7 | 1.00 | yes |
even with the worst possible hardware I could find in my house, I've achieved a | `fastfetch` | 48.6 ± 1.6 | 45.8 | 61.3 | 46.65 ± 4.75 | no |
nice less-than-1ms invocation time. Which is pretty good. While Microfetch | `pfetch` | 206.0 ± 4.5 | 198.0 | 241.4 | 197.50 ± 19.53 | no |
_could_ be made faster, we're in the territory of environmental bottlenecks | `neofetch` | 689.1 ± 29.1 | 637.7 | 811.2 | 660.79 ± 69.56 | no |
given how little Microfetch actually allocates.
Below are the actual benchmarks with Hyperfine measured on my Desktop system. As far as I'm concerned, Microfetch is significantly faster than every other
The benchmarks were performed under medium system load, and may not be the same fetch tool that I have tried. The only downsides of using Rust for the project
on your system. Please _also_ note that those benchmarks will not be always kept (in exchange for speed and maintainability) is the slightly "bloated" dependency
up to date, but I will try to update the numbers as I make Microfetch faster. trees, and the increased build times. The latter is very easily mitigated with
Nix's binary cache. Since Microfetch is already in Nixpkgs, you are recommended
| Command | Mean [µs] | Min [µs] | Max [µs] | Relative | Written by raf? | to use it to utilize the binary cache properly
| :----------- | ----------------: | -------: | -------: | -------------: | --------------: |
| `microfetch` | 604.4 ± 64.2 | 516.0 | 1184.6 | 1.00 | Yes |
| `fastfetch` | 140836.6 ± 1258.6 | 139204.7 | 143299.4 | 233.00 ± 24.82 | No |
| `pfetch` | 177036.6 ± 1614.3 | 174199.3 | 180830.2 | 292.89 ± 31.20 | No |
| `neofetch` | 406309.9 ± 1810.0 | 402757.3 | 409526.3 | 672.20 ± 71.40 | No |
| `nitch` | 127743.7 ± 1391.7 | 123933.5 | 130451.2 | 211.34 ± 22.55 | No |
| `macchina` | 13603.7 ± 339.7 | 12642.9 | 14701.4 | 22.51 ± 2.45 | No |
The point stands that Microfetch is significantly faster than every other fetch
tool I have tried. This is to be expected, of course, since Microfetch is
designed _explicitly_ for speed and makes some tradeoffs to achieve it's
signature speed.
### Benchmarking Individual Functions ### Benchmarking Individual Functions
@ -130,32 +87,6 @@ To benchmark individual functions, [Criterion.rs] is used. See Criterion's
[Getting Started Guide] for details or just run `cargo bench` to benchmark all [Getting Started Guide] for details or just run `cargo bench` to benchmark all
features of Microfetch. features of Microfetch.
### Profiling Allocations and Timing
[Hotpath]: https://github.com/pawurb/hotpath
Microfetch uses [Hotpath] for profiling function execution timing and heap
allocations. This helps identify performance bottlenecks and track optimization
progress. It is so effective that thanks to Hotpath, Microfetch has seen a 60%
reduction in the number of allocations.
To profile timing:
```bash
HOTPATH_JSON=true cargo run --features=hotpath
```
To profile allocations:
```bash
HOTPATH_JSON=true cargo run --features=hotpath,hotpath-alloc-count-total
```
The JSON output can be analyzed with the `hotpath` CLI tool for detailed
performance metrics. On pull requests, GitHub Actions automatically profiles
both timing and allocations, posting comparison comments to help catch
performance regressions.
## Installation ## Installation
> [!NOTE] > [!NOTE]

View file

@ -1,31 +1,26 @@
use criterion::{Criterion, criterion_group, criterion_main}; use criterion::{Criterion, criterion_group, criterion_main};
use microfetch_lib::{ use microfetch_lib::colors::print_dots;
colors::print_dots, use microfetch_lib::desktop::get_desktop_info;
desktop::get_desktop_info, use microfetch_lib::release::{get_os_pretty_name, get_system_info};
release::{get_os_pretty_name, get_system_info}, use microfetch_lib::system::{
system::{ get_memory_usage, get_root_disk_usage, get_shell, get_username_and_hostname,
get_memory_usage,
get_root_disk_usage,
get_shell,
get_username_and_hostname,
},
uptime::get_current,
}; };
use microfetch_lib::uptime::get_current;
fn main_benchmark(c: &mut Criterion) { fn main_benchmark(c: &mut Criterion) {
let utsname = nix::sys::utsname::uname().expect("lol"); let utsname = nix::sys::utsname::uname().expect("lol");
c.bench_function("user_info", |b| { c.bench_function("user_info", |b| {
b.iter(|| get_username_and_hostname(&utsname)); b.iter(|| get_username_and_hostname(&utsname));
}); });
c.bench_function("os_name", |b| b.iter(get_os_pretty_name)); c.bench_function("os_name", |b| b.iter(get_os_pretty_name));
c.bench_function("kernel_version", |b| b.iter(|| get_system_info(&utsname))); c.bench_function("kernel_version", |b| b.iter(|| get_system_info(&utsname)));
c.bench_function("shell", |b| b.iter(get_shell)); c.bench_function("shell", |b| b.iter(get_shell));
c.bench_function("desktop", |b| b.iter(get_desktop_info)); c.bench_function("desktop", |b| b.iter(get_desktop_info));
c.bench_function("uptime", |b| b.iter(get_current)); c.bench_function("uptime", |b| b.iter(get_current));
c.bench_function("memory_usage", |b| b.iter(get_memory_usage)); c.bench_function("memory_usage", |b| b.iter(get_memory_usage));
c.bench_function("storage", |b| b.iter(get_root_disk_usage)); c.bench_function("storage", |b| b.iter(get_root_disk_usage));
c.bench_function("colors", |b| b.iter(print_dots)); c.bench_function("colors", |b| b.iter(print_dots));
} }
criterion_group!(benches, main_benchmark); criterion_group!(benches, main_benchmark);

9
flake.lock generated
View file

@ -2,14 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1743359643, "lastModified": 1754214453,
"narHash": "sha256-RkyJ9a67s0zEIz4O66TyZOIGh4TFZ4dKHKMgnxZCh2I=", "narHash": "sha256-Q/I2xJn/j1wpkGhWkQnm20nShYnG7TI99foDBpXm1SY=",
"lastModified": 1763381801,
"narHash": "sha256-325fR0JmHW7B74/gHPv/S9w1Rfj/M2HniwQFUwdrZ9k=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "ca77b4bc80e558ce59f2712fdb276f90c0ee309a", "rev": "5b09dc45f24cf32316283e62aec81ffee3c3e376",
"rev": "46931757ea8bdbba25c076697f8e73b8dc39fef5",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -9,29 +9,23 @@
inherit (toml) version; inherit (toml) version;
in in
rustPlatform.buildRustPackage.override {stdenv = stdenvAdapters.useMoldLinker llvm.stdenv;} { rustPlatform.buildRustPackage.override {stdenv = stdenvAdapters.useMoldLinker llvm.stdenv;} {
RUSTFLAGS = "-C link-arg=-fuse-ld=mold";
inherit pname version; inherit pname version;
src = let
fs = lib.fileset; src = builtins.path {
s = ../.; name = "${pname}-${version}";
in path = lib.sources.cleanSource ../.;
fs.toSource { };
root = s;
fileset = fs.unions [
(fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /src))
(s + /Cargo.lock)
(s + /Cargo.toml)
];
};
cargoLock.lockFile = ../Cargo.lock; cargoLock.lockFile = ../Cargo.lock;
enableParallelBuilding = true; enableParallelBuilding = true;
env.RUSTFLAGS = "-C link-arg=-fuse-ld=mold";
meta = { meta = {
description = "Microscopic fetch script in Rust, for NixOS systems"; description = "A microscopic fetch script in Rust, for NixOS systems";
homepage = "https://github.com/NotAShelf/microfetch"; homepage = "https://github.com/NotAShelf/microfetch";
license = lib.licenses.gpl3Only; license = lib.licenses.gpl3Only;
maintainers = [lib.maintainers.NotAShelf]; maintainers = with lib.maintainers; [NotAShelf];
mainProgram = "microfetch"; mainProgram = "microfetch";
}; };
} }

View file

@ -4,7 +4,6 @@
rustfmt, rustfmt,
clippy, clippy,
cargo, cargo,
taplo,
rustc, rustc,
rustPlatform, rustPlatform,
gnuplot, gnuplot,
@ -17,9 +16,8 @@ mkShell {
rustc rustc
rust-analyzer-unwrapped rust-analyzer-unwrapped
(rustfmt.override {asNightly = true;}) rustfmt
clippy clippy
taplo
gnuplot # For Criterion.rs plots gnuplot # For Criterion.rs plots
]; ];

View file

@ -1,81 +1,57 @@
use std::{env, sync::LazyLock}; use std::env;
use std::sync::LazyLock;
pub struct Colors { pub struct Colors {
pub reset: &'static str, pub reset: &'static str,
pub blue: &'static str, pub blue: &'static str,
pub cyan: &'static str, pub cyan: &'static str,
pub green: &'static str, pub green: &'static str,
pub yellow: &'static str, pub yellow: &'static str,
pub red: &'static str, pub red: &'static str,
pub magenta: &'static str, pub magenta: &'static str,
} }
impl Colors { impl Colors {
const fn new(is_no_color: bool) -> Self { const fn new(is_no_color: bool) -> Self {
if is_no_color { if is_no_color {
Self { Self {
reset: "", reset: "",
blue: "", blue: "",
cyan: "", cyan: "",
green: "", green: "",
yellow: "", yellow: "",
red: "", red: "",
magenta: "", magenta: "",
} }
} else { } else {
Self { Self {
reset: "\x1b[0m", reset: "\x1b[0m",
blue: "\x1b[34m", blue: "\x1b[34m",
cyan: "\x1b[36m", cyan: "\x1b[36m",
green: "\x1b[32m", green: "\x1b[32m",
yellow: "\x1b[33m", yellow: "\x1b[33m",
red: "\x1b[31m", red: "\x1b[31m",
magenta: "\x1b[35m", magenta: "\x1b[35m",
} }
}
} }
}
} }
pub static COLORS: LazyLock<Colors> = LazyLock::new(|| { pub static COLORS: LazyLock<Colors> = 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(); let is_no_color = env::var("NO_COLOR").is_ok();
Colors::new(is_no_color) Colors::new(is_no_color)
}); });
#[must_use]
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn print_dots() -> String { pub fn print_dots() -> String {
// Pre-calculate capacity: 6 color codes + " " (glyph + 2 spaces) per color format!(
const GLYPH: &str = ""; "{} {} {} {} {} {} {}",
let capacity = COLORS.blue.len() COLORS.blue,
+ COLORS.cyan.len() COLORS.cyan,
+ COLORS.green.len() COLORS.green,
+ COLORS.yellow.len() COLORS.yellow,
+ COLORS.red.len() COLORS.red,
+ COLORS.magenta.len() COLORS.magenta,
+ COLORS.reset.len() COLORS.reset,
+ (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
} }

View file

@ -1,37 +1,28 @@
use std::fmt::Write;
#[must_use]
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_desktop_info() -> String { pub fn get_desktop_info() -> String {
// Retrieve the environment variables and handle Result types // Retrieve the environment variables and handle Result types
let desktop_env = std::env::var("XDG_CURRENT_DESKTOP"); 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 { // Capitalize the first letter of the display backend value
Err(_) => "Unknown", let mut display_backend = display_backend_result.unwrap_or_default();
Ok(ref s) if s.starts_with("none+") => &s[5..], if let Some(c) = display_backend.as_mut_str().get_mut(0..1) {
Ok(ref s) => s.as_str(), c.make_ascii_uppercase();
}; }
let backend_str = match display_backend { // Trim "none+" from the start of desktop_env if present
Err(_) => "Unknown", // Use "Unknown" if desktop_env is empty or has an error
Ok(ref s) if s.is_empty() => "Unknown", let desktop_env = match desktop_env {
Ok(ref s) => s.as_str(), Err(_) => "Unknown".to_string(),
}; Ok(s) => s.trim_start_matches("none+").to_string(),
};
// Pre-calculate capacity: desktop_len + " (" + backend_len + ")" // Handle the case where display_backend might be empty after capitalization
// Capitalize first char needs temporary allocation only if backend exists let display_backend = if display_backend.is_empty() {
let mut result = "Unknown"
String::with_capacity(desktop_str.len() + backend_str.len() + 3); } else {
result.push_str(desktop_str); &display_backend
result.push_str(" ("); }
.to_string();
// Capitalize first character of backend format!("{desktop_env} ({display_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()..]);
}
result.push(')');
result
} }

View file

@ -4,90 +4,78 @@ mod release;
mod system; mod system;
mod uptime; mod uptime;
use crate::colors::print_dots;
use crate::desktop::get_desktop_info;
use crate::release::{get_os_pretty_name, get_system_info};
use crate::system::{get_memory_usage, get_root_disk_usage, get_shell, get_username_and_hostname};
use crate::uptime::get_current;
use std::io::{Write, stdout}; use std::io::{Write, stdout};
use crate::{
colors::print_dots,
desktop::get_desktop_info,
release::{get_os_pretty_name, get_system_info},
system::{
get_memory_usage,
get_root_disk_usage,
get_shell,
get_username_and_hostname,
},
uptime::get_current,
};
#[cfg_attr(feature = "hotpath", hotpath::main)]
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
if Some("--version") == std::env::args().nth(1).as_deref() { if Some("--version") == std::env::args().nth(1).as_deref() {
println!("Microfetch {}", env!("CARGO_PKG_VERSION")); println!("Microfetch {}", env!("CARGO_PKG_VERSION"));
} else { } else {
let utsname = nix::sys::utsname::uname()?; let utsname = nix::sys::utsname::uname()?;
let fields = Fields { let fields = Fields {
user_info: get_username_and_hostname(&utsname), user_info: get_username_and_hostname(&utsname),
os_name: get_os_pretty_name()?, os_name: get_os_pretty_name()?,
kernel_version: get_system_info(&utsname), kernel_version: get_system_info(&utsname),
shell: get_shell(), shell: get_shell(),
desktop: get_desktop_info(), desktop: get_desktop_info(),
uptime: get_current()?, uptime: get_current()?,
memory_usage: get_memory_usage()?, memory_usage: get_memory_usage()?,
storage: get_root_disk_usage()?, storage: get_root_disk_usage()?,
colors: print_dots(), colors: print_dots(),
}; };
print_system_info(&fields)?; print_system_info(&fields)?;
} }
Ok(()) Ok(())
} }
// Struct to hold all the fields we need in order to print the fetch. This // Struct to hold all the fields we need in order to print the fetch. This
// helps avoid Clippy warnings about argument count, and makes it slightly // helps avoid Clippy warnings about argument count, and makes it slightly
// easier to pass data around. Though, it is not like we really need to. // easier to pass data around. Though, it is not like we really need to.
struct Fields { struct Fields {
user_info: String, user_info: String,
os_name: String, os_name: String,
kernel_version: String, kernel_version: String,
shell: String, shell: String,
uptime: String, uptime: String,
desktop: String, desktop: String,
memory_usage: String, memory_usage: String,
storage: String, storage: String,
colors: String, colors: String,
} }
#[cfg_attr(feature = "hotpath", hotpath::measure)] fn print_system_info(fields: &Fields) -> Result<(), Box<dyn std::error::Error>> {
fn print_system_info( use crate::colors::COLORS;
fields: &Fields,
) -> Result<(), Box<dyn std::error::Error>> {
use crate::colors::COLORS;
let Fields { let Fields {
user_info, user_info,
os_name, os_name,
kernel_version, kernel_version,
shell, shell,
uptime, uptime,
desktop, desktop,
memory_usage, memory_usage,
storage, storage,
colors, colors,
} = fields; } = fields;
let cyan = COLORS.cyan; let cyan = COLORS.cyan;
let blue = COLORS.blue; let blue = COLORS.blue;
let reset = COLORS.reset; let reset = COLORS.reset;
let system_info = format!(" let system_info = format!("
{cyan} {blue} {user_info} ~{reset} {cyan} {blue} {user_info} ~{reset}
{cyan} {blue} {cyan} {cyan} {blue}System{reset} {os_name} {cyan} {blue} {cyan} {cyan} {blue}System{reset} {os_name}
{cyan} {blue} {cyan} {cyan} {blue}Kernel{reset} {kernel_version} {cyan} {blue} {cyan} {cyan} {blue}Kernel{reset} {kernel_version}
{blue} {blue}{cyan} {cyan} {blue}Shell{reset} {shell} {blue} {blue}{cyan} {cyan} {blue}Shell{reset} {shell}
{blue} {cyan} {cyan} {blue}Uptime{reset} {uptime} {blue} {cyan} {cyan} {blue}Uptime{reset} {uptime}
{blue} {cyan} {cyan} {cyan} {blue}Desktop{reset} {desktop} {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} {blue} {cyan}{blue} {cyan}󱥎 {blue}Storage (/){reset} {storage}
{cyan} {blue} {cyan} {blue}Colors{reset} {colors}\n"); {cyan} {blue} {cyan} {blue}Colors{reset} {colors}\n");
Ok(stdout().write_all(system_info.as_bytes())?) Ok(stdout().write_all(system_info.as_bytes())?)
} }

View file

@ -1,48 +1,33 @@
use nix::sys::utsname::UtsName;
use std::{ use std::{
fmt::Write as _, fs::File,
fs::File, io::{self, BufRead, BufReader},
io::{self, Read},
}; };
use nix::sys::utsname::UtsName;
#[must_use]
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_system_info(utsname: &UtsName) -> String { pub fn get_system_info(utsname: &UtsName) -> String {
let sysname = utsname.sysname().to_str().unwrap_or("Unknown"); format!(
let release = utsname.release().to_str().unwrap_or("Unknown"); "{} {} ({})",
let machine = utsname.machine().to_str().unwrap_or("Unknown"); utsname.sysname().to_str().unwrap_or("Unknown"),
utsname.release().to_str().unwrap_or("Unknown"),
// Pre-allocate capacity: sysname + " " + release + " (" + machine + ")" utsname.machine().to_str().unwrap_or("Unknown")
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
} }
/// 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<String, io::Error> { pub fn get_os_pretty_name() -> Result<String, io::Error> {
// We use a stack-allocated buffer here, which seems to perform MUCH better let file = File::open("/etc/os-release")?;
// than `BufReader`. In hindsight, I should've seen this coming. let reader = BufReader::new(file);
let mut buffer = String::with_capacity(1024);
File::open("/etc/os-release")?.read_to_string(&mut buffer)?;
for line in buffer.lines() { for line in reader.lines() {
if let Some(pretty_name) = line.strip_prefix("PRETTY_NAME=") { let line = line?;
if let Some(trimmed) = pretty_name if let Some(pretty_name) = line.strip_prefix("PRETTY_NAME=") {
.strip_prefix('"') if let Some(trimmed) = pretty_name
.and_then(|s| s.strip_suffix('"')) .strip_prefix('"')
{ .and_then(|s| s.strip_suffix('"'))
return Ok(trimmed.to_owned()); {
} return Ok(trimmed.to_string());
return Ok(pretty_name.to_owned()); }
return Ok(pretty_name.to_string());
}
} }
} Ok("Unknown".to_string())
Ok("Unknown".to_owned())
} }

View file

@ -1,137 +1,87 @@
use crate::colors::COLORS;
use nix::sys::{statvfs::statvfs, utsname::UtsName};
use std::{ use std::{
env, env,
fmt::Write as _, fs::File,
fs::File, io::{self, Read},
io::{self, Read},
}; };
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 { pub fn get_username_and_hostname(utsname: &UtsName) -> String {
let username = env::var("USER").unwrap_or_else(|_| "unknown_user".to_owned()); let username = env::var("USER").unwrap_or_else(|_| "unknown_user".to_string());
let hostname = utsname.nodename().to_str().unwrap_or("unknown_host"); let hostname = utsname
.nodename()
let capacity = COLORS.yellow.len() .to_str()
+ username.len() .unwrap_or("unknown_host")
+ COLORS.red.len() .to_string();
+ 1 format!(
+ COLORS.green.len() "{yellow}{username}{red}@{green}{hostname}{reset}",
+ hostname.len() yellow = COLORS.yellow,
+ COLORS.reset.len(); red = COLORS.red,
let mut result = String::with_capacity(capacity); green = COLORS.green,
reset = COLORS.reset,
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
} }
#[must_use]
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_shell() -> String { pub fn get_shell() -> String {
let shell_path = let shell_path = env::var("SHELL").unwrap_or_else(|_| "unknown_shell".to_string());
env::var("SHELL").unwrap_or_else(|_| "unknown_shell".to_owned()); let shell_name = shell_path.rsplit('/').next().unwrap_or("unknown_shell");
shell_name.to_string()
// Find last '/' and get the part after it, avoiding allocation
shell_path
.rsplit('/')
.next()
.unwrap_or("unknown_shell")
.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<String, io::Error> { pub fn get_root_disk_usage() -> Result<String, io::Error> {
let vfs = statvfs("/")?; let vfs = statvfs("/")?;
let block_size = vfs.block_size() as u64; let block_size = vfs.block_size() as u64;
let total_blocks = vfs.blocks(); let total_blocks = vfs.blocks();
let available_blocks = vfs.blocks_available(); let available_blocks = vfs.blocks_available();
let total_size = block_size * total_blocks; let total_size = block_size * total_blocks;
let used_size = total_size - (block_size * available_blocks); let used_size = total_size - (block_size * available_blocks);
let total_size = total_size as f64 / (1024.0 * 1024.0 * 1024.0); let total_size = total_size as f64 / (1024.0 * 1024.0 * 1024.0);
let used_size = used_size as f64 / (1024.0 * 1024.0 * 1024.0); let used_size = used_size as f64 / (1024.0 * 1024.0 * 1024.0);
let usage = (used_size / total_size) * 100.0; let usage = (used_size / total_size) * 100.0;
let mut result = String::with_capacity(64); Ok(format!(
write!( "{used_size:.2} GiB / {total_size:.2} GiB ({cyan}{usage:.0}%{reset})",
result, cyan = COLORS.cyan,
"{used_size:.2} GiB / {total_size:.2} GiB ({cyan}{usage:.0}%{reset})", reset = COLORS.reset,
cyan = COLORS.cyan, ))
reset = COLORS.reset,
)
.unwrap();
Ok(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<String, io::Error> { pub fn get_memory_usage() -> Result<String, io::Error> {
#[cfg_attr(feature = "hotpath", hotpath::measure)] fn parse_memory_info() -> Result<(f64, f64), io::Error> {
fn parse_memory_info() -> Result<(f64, f64), io::Error> { let mut total_memory_kb = 0.0;
let mut total_memory_kb = 0.0; let mut available_memory_kb = 0.0;
let mut available_memory_kb = 0.0; let mut meminfo = String::with_capacity(2048);
let mut meminfo = String::with_capacity(2048);
File::open("/proc/meminfo")?.read_to_string(&mut meminfo)?; File::open("/proc/meminfo")?.read_to_string(&mut meminfo)?;
for line in meminfo.lines() { for line in meminfo.lines() {
let mut split = line.split_whitespace(); let mut split = line.split_whitespace();
match split.next().unwrap_or_default() { match split.next().unwrap_or_default() {
"MemTotal:" => { "MemTotal:" => total_memory_kb = split.next().unwrap_or("0").parse().unwrap_or(0.0),
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);
"MemAvailable:" => { // MemTotal comes before MemAvailable, stop parsing
available_memory_kb = break;
split.next().unwrap_or("0").parse().unwrap_or(0.0); }
// MemTotal comes before MemAvailable, stop parsing _ => (),
break; }
}, }
_ => (),
} 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 total_memory_gb = total_memory_kb / 1024.0 / 1024.0; let (used_memory, total_memory) = parse_memory_info()?;
let available_memory_gb = available_memory_kb / 1024.0 / 1024.0; let percentage_used = (used_memory / total_memory * 100.0).round() as u64;
let used_memory_gb = total_memory_gb - available_memory_gb;
Ok((used_memory_gb, total_memory_gb)) Ok(format!(
} "{used_memory:.2} GiB / {total_memory:.2} GiB ({cyan}{percentage_used}%{reset})",
cyan = COLORS.cyan,
let (used_memory, total_memory) = parse_memory_info()?; reset = COLORS.reset,
#[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,
"{used_memory:.2} GiB / {total_memory:.2} GiB \
({cyan}{percentage_used}%{reset})",
cyan = COLORS.cyan,
reset = COLORS.reset,
)
.unwrap();
Ok(result)
} }

View file

@ -1,49 +1,40 @@
use std::{fmt::Write, io, mem::MaybeUninit}; use std::{io, mem::MaybeUninit};
/// 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<String, io::Error> { pub fn get_current() -> Result<String, io::Error> {
let uptime_seconds = { let uptime_seconds = {
let mut info = MaybeUninit::uninit(); let mut info = MaybeUninit::uninit();
if unsafe { libc::sysinfo(info.as_mut_ptr()) } != 0 { if unsafe { libc::sysinfo(info.as_mut_ptr()) } != 0 {
return Err(io::Error::last_os_error()); 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; let days = uptime_seconds / 86400;
let hours = (uptime_seconds / 3600) % 24; let hours = (uptime_seconds / 3600) % 24;
let minutes = (uptime_seconds / 60) % 60; let minutes = (uptime_seconds / 60) % 60;
let mut result = String::with_capacity(32); let mut result = String::with_capacity(32);
if days > 0 { if days > 0 {
let _ = write!(result, "{days}"); result.push_str(&days.to_string());
result.push_str(if days == 1 { " day" } else { " days" }); result.push_str(if days == 1 { " day" } else { " days" });
}
if hours > 0 {
if !result.is_empty() {
result.push_str(", ");
} }
let _ = write!(result, "{hours}"); if hours > 0 {
result.push_str(if hours == 1 { " hour" } else { " hours" }); if !result.is_empty() {
} result.push_str(", ");
if minutes > 0 { }
if !result.is_empty() { result.push_str(&hours.to_string());
result.push_str(", "); result.push_str(if hours == 1 { " hour" } else { " hours" });
}
if minutes > 0 {
if !result.is_empty() {
result.push_str(", ");
}
result.push_str(&minutes.to_string());
result.push_str(if minutes == 1 { " minute" } else { " minutes" });
}
if result.is_empty() {
result.push_str("less than a minute");
} }
let _ = write!(result, "{minutes}");
result.push_str(if minutes == 1 { " minute" } else { " minutes" });
}
if result.is_empty() {
result.push_str("less than a minute");
}
Ok(result) Ok(result)
} }