Compare commits

..

18 commits

Author SHA1 Message Date
b24e720dd8
chore: tag 0.4.11
Some checks are pending
Rust / build (push) Waiting to run
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I42b59d68cdac17ff60d52a4c25bef4686a6a6964
2025-11-17 19:19:31 +03:00
ca76a6e1bd
chore: format with rustfmt nightly
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I9c9e4e010b09d37f0e0994a5407f3ce56a6a6964
2025-11-17 19:18:39 +03:00
00159d6454
docs: README makeover; update benchmarks
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ic1ca95ab7e0b2ff78ed7967c604739546a6a6964
2025-11-17 19:17:56 +03:00
raf
11a726428b
Merge pull request #22 from NotAShelf/notashelf/push-uysmruuoyvnz
various: reduce allocations where available
2025-11-17 18:42:58 +03:00
789ece866b
ci: initial benchmarking workflows
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I367444097eafbd1020c02707c42351bf6a6a6964
2025-11-17 18:40:23 +03:00
f4f3385ff7
various: fix clippy warnings
- Adds proper documentation comments with `# Errors` sections for all
  functions returning `Result`
- `cast_precision_loss` on `u64` -> `f64` for disk sizes is acceptable
  since disk sizes won't exceed f64's precision limit in practice.
  Thus, we can suppress those.
- `cast_sign_loss` and `cast_possible_truncation` on the percentage
  calculation is safe since percentages are always 0-100. Once again,
  it's safe to suppress.

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Id4dd7ebc9674407d2be4f38ff4de24bc6a6a6964
2025-11-17 18:26:34 +03:00
2ad765ef98
various: reduce allocations where available
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I517d855b14c015569a325deb64948f3b6a6a6964
2025-11-17 18:11:45 +03:00
325ec69024
chore: tag 0.4.10
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a2158a305d5f249b52c8b21dc5aaca86a6a6964
2025-11-17 18:11:44 +03:00
2a6fe2a3f1
treewide: set up Hotpath for benchmarking individual allocations
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I0351e5753996e6d0391fc9e2f329878a6a6a6964
2025-11-17 17:40:46 +03:00
9bd4c9a70a
treewide: format with nightly rustfmt rules
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ib8502372dafe2e970024f606b44825af6a6a6964
2025-11-17 16:20:43 +03:00
af8031f9ec
meta: selectively enable Clippy lint-groups projectwide
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ic6eedd28e1ad48a67c988607dd45d7686a6a6964
2025-11-17 16:20:42 +03:00
4c22cf5d2a
nix: use nightly rustfmt; add taplo
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ie4a1b5a29166931aac006a44874374346a6a6964
2025-11-17 16:20:41 +03:00
d438800738
nix: streamline source filter; move RUSTFLAGS to env attrs
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I9761b908d05efd7dc10d53c54ca80fb26a6a6964
2025-11-17 16:20:40 +03:00
1d69d3107c
microfetch: trim fetch screen by one space
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I200c72b5a8249ed3d23754aa3f01aea86a6a6964
2025-11-17 16:20:39 +03:00
dab8f556af
meta: set up formatters for Rust and TOML
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I917d66d2af199b96f84aa50071a0ccd56a6a6964
2025-11-17 16:20:38 +03:00
6150e55ba5
flake: bump nixpkgs
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6ba6412f88305b295ffb571313606bd56a6a6964
2025-11-17 16:20:37 +03:00
8800b69ef3
chore: bump dependencies
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a69647ad9cfdd52eec7fa9ae4a184b9c8ea0d
2025-11-17 16:20:36 +03:00
e355ddc517
flake: bump nixpkgs
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ica041259225e45e345de08325a0cc75f6a6a6964
2025-11-17 16:20:35 +03:00
17 changed files with 1781 additions and 334 deletions

57
.github/workflows/hotpath-comment.yml vendored Normal file
View file

@ -0,0 +1,57 @@
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"

63
.github/workflows/hotpath-profile.yml vendored Normal file
View file

@ -0,0 +1,63 @@
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

26
.rustfmt.toml Normal file
View file

@ -0,0 +1,26 @@
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

13
.taplo.toml Normal file
View file

@ -0,0 +1,13 @@
[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

1069
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.9" version = "0.4.11"
edition = "2024" edition = "2024"
[lib] [lib]
@ -12,28 +12,59 @@ name = "microfetch"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
nix = { version = "0.30", features = ["fs", "hostname", "feature"] } hotpath = { optional = true, version = "0.6.0" }
libc = "0.2" libc = "0.2.177"
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 = 3 opt-level = 1
[profile.release] [profile.release]
strip = true
opt-level = "s"
lto = true
codegen-units = 1 codegen-units = 1
panic = "abort" lto = true
opt-level = "s"
panic = "abort"
strip = true
[profile.profiler] [profile.profiler]
inherits = "release" debug = true
debug = true inherits = "release"
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,17 +1,34 @@
<!-- 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/v/release/notashelf/microfetch?display_name=tag&color=DEA584"> --> <img src="https://img.shields.io/github/stars/notashelf/microfetch?label=stars&color=DEA584" alt="stars">
<img src="https://img.shields.io/github/stars/notashelf/microfetch?label=stars&color=DEA584">
</div> </div>
<h1 align="center">Microfetch</h1> <div id="doc-begin" align="center">
<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>
Stupidly simple, laughably fast fetch tool. Written in Rust for speed and ease ## Synopsis
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 [fastfetch]: https://github.com/fastfetch-cli/fastfetch
to replace [fastfetch](https://github.com/fastfetch-cli/fastfetch) on my
personal system, but [probably not yours](#customizing). Though, you are more Stupidly small and simple, laughably fast and pretty fetch tool. Written in Rust
than welcome to use it on your system: it's pretty [fast](#benchmarks)... 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)_...
<p align="center"> <p align="center">
<img <img
@ -26,6 +43,7 @@ than welcome to use it on your system: it's 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:
@ -44,39 +62,64 @@ than welcome to use it on your system: it's pretty [fast](#benchmarks)...
## Motivation ## Motivation
Fastfetch, as its name indicates, a very fast fetch tool written in C, however, Fastfetch, as its name probably hinted, is a very fast fetch tool written in C.
I am not interested in any of its additional features, such as customization, However, I am not interested in _any_ of its additional features, and I'm not
and I very much dislike the defaults. Microfetch is my response to this problem, interested in its configuration options. Sure I can _configure_ it when I
a _very fast_ fetch tool that you would normally write in Bash and put in your dislike the defaults, but how often would I really change the configuration...
`~/.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.
I cannot re-iterate it enough, Microfetch is _annoyingly fast_. 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_.
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
The performance may be sometimes influenced by hardware-specific race Below are the benchmarks that I've used to back up my claims of Microfetch's
conditions, or even your kernel configuration meaning it may (at times) depend speed. It is fast, it is _very_ fast and that is the point of its existence. It
on your hardware. However, the overall trend appears to be less than 1.3ms on _could_ be faster, and it will be. Eventually.
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.
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative | Written by raf? | At this point in time, the performance may be sometimes influenced by
| :----------- | -----------: | -------: | -------: | -------------: | --------------: | hardware-specific race conditions or even your kernel configuration. Which means
| `microfetch` | 1.0 ± 0.1 | 0.9 | 1.7 | 1.00 | yes | that Microfetch's speed may (at times) depend on your hardware setup. However,
| `fastfetch` | 48.6 ± 1.6 | 45.8 | 61.3 | 46.65 ± 4.75 | no | even with the worst possible hardware I could find in my house, I've achieved a
| `pfetch` | 206.0 ± 4.5 | 198.0 | 241.4 | 197.50 ± 19.53 | no | nice less-than-1ms invocation time. Which is pretty good. While Microfetch
| `neofetch` | 689.1 ± 29.1 | 637.7 | 811.2 | 660.79 ± 69.56 | no | _could_ be made faster, we're in the territory of environmental bottlenecks
given how little Microfetch actually allocates.
As far as I'm concerned, Microfetch is significantly faster than every other Below are the actual benchmarks with Hyperfine measured on my Desktop system.
fetch tool that I have tried. The only downsides of using Rust for the project The benchmarks were performed under medium system load, and may not be the same
(in exchange for speed and maintainability) is the slightly "bloated" dependency on your system. Please _also_ note that those benchmarks will not be always kept
trees, and the increased build times. The latter is very easily mitigated with up to date, but I will try to update the numbers as I make Microfetch faster.
Nix's binary cache. Since Microfetch is already in Nixpkgs, you are recommended
to use it to utilize the binary cache properly | Command | Mean [µs] | Min [µs] | Max [µs] | Relative | Written by raf? |
| :----------- | ----------------: | -------: | -------: | -------------: | --------------: |
| `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
@ -87,6 +130,32 @@ 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,26 +1,31 @@
use criterion::{Criterion, criterion_group, criterion_main}; use criterion::{Criterion, criterion_group, criterion_main};
use microfetch_lib::colors::print_dots; use microfetch_lib::{
use microfetch_lib::desktop::get_desktop_info; colors::print_dots,
use microfetch_lib::release::{get_os_pretty_name, get_system_info}; desktop::get_desktop_info,
use microfetch_lib::system::{ release::{get_os_pretty_name, get_system_info},
get_memory_usage, get_root_disk_usage, get_shell, get_username_and_hostname, system::{
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,11 +2,14 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1754214453, "lastModified": 1743359643,
"narHash": "sha256-Q/I2xJn/j1wpkGhWkQnm20nShYnG7TI99foDBpXm1SY=", "narHash": "sha256-RkyJ9a67s0zEIz4O66TyZOIGh4TFZ4dKHKMgnxZCh2I=",
"lastModified": 1763381801,
"narHash": "sha256-325fR0JmHW7B74/gHPv/S9w1Rfj/M2HniwQFUwdrZ9k=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "5b09dc45f24cf32316283e62aec81ffee3c3e376", "rev": "ca77b4bc80e558ce59f2712fdb276f90c0ee309a",
"rev": "46931757ea8bdbba25c076697f8e73b8dc39fef5",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -9,23 +9,29 @@
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
src = builtins.path { fs = lib.fileset;
name = "${pname}-${version}"; s = ../.;
path = lib.sources.cleanSource ../.; in
}; 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 = "A microscopic fetch script in Rust, for NixOS systems"; description = "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 = with lib.maintainers; [NotAShelf]; maintainers = [lib.maintainers.NotAShelf];
mainProgram = "microfetch"; mainProgram = "microfetch";
}; };
} }

View file

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

View file

@ -1,57 +1,81 @@
use std::env; use std::{env, sync::LazyLock};
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 {
format!( // Pre-calculate capacity: 6 color codes + " " (glyph + 2 spaces) per color
"{} {} {} {} {} {} {}", const GLYPH: &str = "";
COLORS.blue, let capacity = COLORS.blue.len()
COLORS.cyan, + COLORS.cyan.len()
COLORS.green, + COLORS.green.len()
COLORS.yellow, + COLORS.yellow.len()
COLORS.red, + COLORS.red.len()
COLORS.magenta, + COLORS.magenta.len()
COLORS.reset, + 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
} }

View file

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

View file

@ -4,78 +4,90 @@ 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};
fn main() -> Result<(), Box<dyn std::error::Error>> { use crate::{
if Some("--version") == std::env::args().nth(1).as_deref() { colors::print_dots,
println!("Microfetch {}", env!("CARGO_PKG_VERSION")); desktop::get_desktop_info,
} else { release::{get_os_pretty_name, get_system_info},
let utsname = nix::sys::utsname::uname()?; system::{
let fields = Fields { get_memory_usage,
user_info: get_username_and_hostname(&utsname), get_root_disk_usage,
os_name: get_os_pretty_name()?, get_shell,
kernel_version: get_system_info(&utsname), get_username_and_hostname,
shell: get_shell(), },
desktop: get_desktop_info(), uptime::get_current,
uptime: get_current()?, };
memory_usage: get_memory_usage()?,
storage: get_root_disk_usage()?,
colors: print_dots(),
};
print_system_info(&fields)?;
}
Ok(()) #[cfg_attr(feature = "hotpath", hotpath::main)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
if Some("--version") == std::env::args().nth(1).as_deref() {
println!("Microfetch {}", env!("CARGO_PKG_VERSION"));
} else {
let utsname = nix::sys::utsname::uname()?;
let fields = Fields {
user_info: get_username_and_hostname(&utsname),
os_name: get_os_pretty_name()?,
kernel_version: get_system_info(&utsname),
shell: get_shell(),
desktop: get_desktop_info(),
uptime: get_current()?,
memory_usage: get_memory_usage()?,
storage: get_root_disk_usage()?,
colors: print_dots(),
};
print_system_info(&fields)?;
}
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,
} }
fn print_system_info(fields: &Fields) -> Result<(), Box<dyn std::error::Error>> { #[cfg_attr(feature = "hotpath", hotpath::measure)]
use crate::colors::COLORS; fn print_system_info(
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,33 +1,48 @@
use nix::sys::utsname::UtsName;
use std::{ use std::{
fs::File, fmt::Write as _,
io::{self, BufRead, BufReader}, fs::File,
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 {
format!( let sysname = utsname.sysname().to_str().unwrap_or("Unknown");
"{} {} ({})", let release = utsname.release().to_str().unwrap_or("Unknown");
utsname.sysname().to_str().unwrap_or("Unknown"), let machine = utsname.machine().to_str().unwrap_or("Unknown");
utsname.release().to_str().unwrap_or("Unknown"),
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
} }
/// 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> {
let file = File::open("/etc/os-release")?; // We use a stack-allocated buffer here, which seems to perform MUCH better
let reader = BufReader::new(file); // than `BufReader`. In hindsight, I should've seen this coming.
let mut buffer = String::with_capacity(1024);
File::open("/etc/os-release")?.read_to_string(&mut buffer)?;
for line in reader.lines() { for line in buffer.lines() {
let line = line?; if let Some(pretty_name) = line.strip_prefix("PRETTY_NAME=") {
if let Some(pretty_name) = line.strip_prefix("PRETTY_NAME=") { if let Some(trimmed) = pretty_name
if let Some(trimmed) = pretty_name .strip_prefix('"')
.strip_prefix('"') .and_then(|s| s.strip_suffix('"'))
.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,87 +1,137 @@
use crate::colors::COLORS;
use nix::sys::{statvfs::statvfs, utsname::UtsName};
use std::{ use std::{
env, env,
fs::File, fmt::Write as _,
io::{self, Read}, fs::File,
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_string()); let username = env::var("USER").unwrap_or_else(|_| "unknown_user".to_owned());
let hostname = utsname let hostname = utsname.nodename().to_str().unwrap_or("unknown_host");
.nodename()
.to_str() let capacity = COLORS.yellow.len()
.unwrap_or("unknown_host") + username.len()
.to_string(); + COLORS.red.len()
format!( + 1
"{yellow}{username}{red}@{green}{hostname}{reset}", + COLORS.green.len()
yellow = COLORS.yellow, + hostname.len()
red = COLORS.red, + COLORS.reset.len();
green = COLORS.green, let mut result = String::with_capacity(capacity);
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 = env::var("SHELL").unwrap_or_else(|_| "unknown_shell".to_string()); let shell_path =
let shell_name = shell_path.rsplit('/').next().unwrap_or("unknown_shell"); env::var("SHELL").unwrap_or_else(|_| "unknown_shell".to_owned());
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;
Ok(format!( let mut result = String::with_capacity(64);
"{used_size:.2} GiB / {total_size:.2} GiB ({cyan}{usage:.0}%{reset})", write!(
cyan = COLORS.cyan, result,
reset = COLORS.reset, "{used_size:.2} GiB / {total_size:.2} GiB ({cyan}{usage:.0}%{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> {
fn parse_memory_info() -> Result<(f64, f64), io::Error> { #[cfg_attr(feature = "hotpath", hotpath::measure)]
let mut total_memory_kb = 0.0; fn parse_memory_info() -> Result<(f64, f64), io::Error> {
let mut available_memory_kb = 0.0; let mut total_memory_kb = 0.0;
let mut meminfo = String::with_capacity(2048); let mut available_memory_kb = 0.0;
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:" => total_memory_kb = split.next().unwrap_or("0").parse().unwrap_or(0.0), "MemTotal:" => {
"MemAvailable:" => { total_memory_kb = split.next().unwrap_or("0").parse().unwrap_or(0.0);
available_memory_kb = split.next().unwrap_or("0").parse().unwrap_or(0.0); },
// MemTotal comes before MemAvailable, stop parsing "MemAvailable:" => {
break; available_memory_kb =
} 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 (used_memory, total_memory) = parse_memory_info()?; let total_memory_gb = total_memory_kb / 1024.0 / 1024.0;
let percentage_used = (used_memory / total_memory * 100.0).round() as u64; let available_memory_gb = available_memory_kb / 1024.0 / 1024.0;
let used_memory_gb = total_memory_gb - available_memory_gb;
Ok(format!( Ok((used_memory_gb, total_memory_gb))
"{used_memory:.2} GiB / {total_memory:.2} GiB ({cyan}{percentage_used}%{reset})", }
cyan = COLORS.cyan,
reset = COLORS.reset, 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,
"{used_memory:.2} GiB / {total_memory:.2} GiB \
({cyan}{percentage_used}%{reset})",
cyan = COLORS.cyan,
reset = COLORS.reset,
)
.unwrap();
Ok(result)
} }

View file

@ -1,40 +1,49 @@
use std::{io, mem::MaybeUninit}; use std::{fmt::Write, 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());
} }
unsafe { info.assume_init().uptime as u64 } #[allow(clippy::cast_sign_loss)]
}; 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 {
result.push_str(&days.to_string()); let _ = write!(result, "{days}");
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(", ");
} }
if hours > 0 { let _ = write!(result, "{hours}");
if !result.is_empty() { result.push_str(if hours == 1 { " hour" } else { " hours" });
result.push_str(", "); }
} if minutes > 0 {
result.push_str(&hours.to_string()); if !result.is_empty() {
result.push_str(if hours == 1 { " hour" } else { " hours" }); result.push_str(", ");
}
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)
} }