Compare commits

...

87 commits

Author SHA1 Message Date
raf
8d863c64c9
Merge pull request #32 from NotAShelf/dependabot/cargo/criterion-0.8.1
Some checks failed
Rust / Test on aarch64-unknown-linux-gnu (push) Has been cancelled
Rust / Test on x86_64-unknown-linux-gnu (push) Has been cancelled
chore(deps): bump criterion from 0.8.0 to 0.8.1
2025-12-11 18:18:59 +03:00
2066d781fe
docs: update motivation section a little
Some checks are pending
Rust / Test on aarch64-unknown-linux-gnu (push) Waiting to run
Rust / Test on x86_64-unknown-linux-gnu (push) Waiting to run
God redditors are insufferable

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I468d0fe0c1aaaa8d08d41373cfc8993c6a6a6964
2025-12-10 14:07:45 +03:00
dependabot[bot]
6640fdd559
chore(deps): bump criterion from 0.8.0 to 0.8.1
Bumps [criterion](https://github.com/criterion-rs/criterion.rs) from 0.8.0 to 0.8.1.
- [Release notes](https://github.com/criterion-rs/criterion.rs/releases)
- [Changelog](https://github.com/criterion-rs/criterion.rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/criterion-rs/criterion.rs/compare/criterion-v0.8.0...criterion-v0.8.1)

---
updated-dependencies:
- dependency-name: criterion
  dependency-version: 0.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-10 09:08:02 +00:00
cf3321b17a
chore: bump dependencies
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I95094ead2493107a2d4bdd3f797fee246a6a6964
2025-12-10 12:06:46 +03:00
7b8e736ff7
microfetch: update memory icon
Fixes #34

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Iff9da39a11eb4060eb314343efa2674b6a6a6964
2025-12-10 11:10:49 +03:00
raf
927dce4127
Merge pull request #29 from Uzaaft/main
Some checks are pending
Rust / Test on aarch64-unknown-linux-gnu (push) Waiting to run
Rust / Test on x86_64-unknown-linux-gnu (push) Waiting to run
Gotta go small
2025-12-09 21:42:02 +03:00
Uzair Aftab
f883723608
Merge branch 'main' into main 2025-12-09 19:38:21 +01:00
Uzair Aftab
f0cf18dba7
fix: handle partial writes from libc::write 2025-12-09 19:36:29 +01:00
raf
a3b169bbc4
Merge pull request #30 from imnotpoz/main
Some checks failed
Rust / Test on aarch64-unknown-linux-gnu (push) Has been cancelled
Rust / Test on x86_64-unknown-linux-gnu (push) Has been cancelled
minor improvements
2025-12-01 22:39:42 +03:00
poz
e4880661ae
print extra newline after fetch for better looks 2025-12-01 20:37:01 +01:00
poz
80e64a1c70
add .direnv to gitignore 2025-12-01 20:36:31 +01:00
Uzair Aftab
9da87b933d
perf: use stack buffer and direct write syscall in print_system_info
Eliminates ~1KB stdout buffering allocation by using Cursor<&mut [u8]>
and libc::write instead of format!() + stdout().write_all()
2025-11-30 19:05:05 +01:00
Uzair Aftab
8376e9d323
perf: use libc to fetch env vars 2025-11-30 18:01:11 +01:00
8c32f5f408
nix: build with Mold linker on x86_64-linux
Some checks are pending
Rust / Test on aarch64-unknown-linux-gnu (push) Waiting to run
Rust / Test on x86_64-unknown-linux-gnu (push) Waiting to run
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I771c36297577aba189058a2183ec2b4a6a6a6964
2025-11-30 17:26:50 +03:00
3ad14a95a6
meta: build with Mold linker on x86_64 Linux
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Id51e62dda1ec2ba895ffdbbd25c2f1256a6a6964
2025-11-30 17:26:49 +03:00
raf
7683e6dfcf
Merge pull request #26 from NotAShelf/dependabot/github_actions/actions/download-artifact-6
chore(deps): bump actions/download-artifact from 4 to 6
2025-11-30 17:03:18 +03:00
raf
96b02c9ed9
Merge pull request #27 from NotAShelf/dependabot/github_actions/actions/upload-artifact-5
chore(deps): bump actions/upload-artifact from 4 to 5
2025-11-30 17:03:11 +03:00
raf
6666644d3a
Merge pull request #28 from NotAShelf/dependabot/github_actions/actions/checkout-6
chore(deps): bump actions/checkout from 4 to 6
2025-11-30 17:03:04 +03:00
ff8af87747
chore: tag 0.4.12
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I54a5f2181efdd42af2fda7cebb88484f6a6a6964
2025-11-30 17:02:31 +03:00
dependabot[bot]
d2a981b070
chore(deps): bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-30 14:01:31 +00:00
dependabot[bot]
5c23cb2665
chore(deps): bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-30 14:01:27 +00:00
dependabot[bot]
5196ced487
chore(deps): bump actions/download-artifact from 4 to 6
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-30 14:01:24 +00:00
raf
858b2487c4
Merge pull request #25 from NotAShelf/notashelf/push-orrzkvopklqm
treewide: bump deps and update docs
2025-11-30 17:00:52 +03:00
fa8b6b9d68
ci: set up cross comp for Rust builds
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I154d2afb05088e22882985bcd4536f026a6a6964
2025-11-30 16:57:12 +03:00
16a1d5fe3f
ci: set up dependabot for GH actions
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I890272e62db5824a3d866748375d1a9f6a6a6964
2025-11-30 16:55:09 +03:00
8d97a9e8ec
ci: streamline workflows; remove tagged release workflow
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I1432980533dee11b64a53d2ad2f2094d6a6a6964
2025-11-30 15:38:48 +03:00
48c3807148
docs: update hotpath usage instructions
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I8a056b6814bef36e74b4aca56d4aa2686a6a6964
2025-11-30 15:16:03 +03:00
09da1c27d2
chore: bump hotpath
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I1ae1d9b3d25ff220bee51660efc598b76a6a6964
2025-11-30 15:16:02 +03:00
c1a4bc24f4
docs: add note about handwritten assembly
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6920a416e6169a84469514bae0f207426a6a6964
2025-11-30 15:16:01 +03:00
c3a6d1c93c
treewide: fix commonmark formatting in Rustdoc
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ie1a13221aded56f903156fdb35abe2ac6a6a6964
2025-11-30 15:16:00 +03:00
6d6ee838bf
chore: bump criterion
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I5623347364c5ed71c99676029f4261026a6a6964
2025-11-30 15:15:59 +03:00
raf
5480d2d824
Merge pull request #23 from NotAShelf/notashelf/push-orrzkvopklqm
Some checks failed
hotpath-comment.yml / Merge pull request #23 from NotAShelf/notashelf/push-orrzkvopklqm (push) Failing after 0s
Rust / build (push) Has been cancelled
treewide: more speed = more good
2025-11-18 00:11:28 +03:00
353b78e688
ci: fix hotpath-comment usage
Some checks failed
hotpath-comment.yml / ci: fix hotpath-comment usage (push) Failing after 0s
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Idc163948f012efb07fc7a6a952af54b36a6a6964
2025-11-18 00:08:00 +03:00
6f8d1ffa83
nix: include benches in source filter; fix build
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Id78ee5f62a5168feef09b5f8713b107c6a6a6964
2025-11-18 00:07:59 +03:00
07afedd0cc
docs: update binary size
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I1a9189f90666d7efc010a7255c287bd86a6a6964
2025-11-18 00:07:55 +03:00
75132ff172
treewide: remove nix dependency, add custom syscall wrappers & UtsName
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ib880f4bafe9d3bbc944af4b9125256366a6a6964
2025-11-17 21:29:15 +03:00
f8a0070986
uptime: optimize uptime calculation w/ inline assembly and custom itoa
lol, lmao even.

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ie22fbd2e9c2be5740b493bdc81caafb36a6a6964
2025-11-17 21:29:14 +03:00
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
9d8905354e
expand comment strings
Some checks failed
Rust / build (push) Has been cancelled
80 chars is good.

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a696460754266ff5810a73f98ce9e6eb0c35b
2025-08-04 21:04:32 +03:00
14d8f9390d
chore: bump criterion
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a6964e55c9925511d83d6be9b2ba001613983
2025-08-04 21:04:31 +03:00
7da5fe97cc
nix: point to nixos-unstable for the nixpkgs input
Oops.

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a6964eab43b655668d65abe532ed710538670
2025-08-04 21:04:30 +03:00
bc03fd73a0
0.4.9
Some checks failed
Rust / build (push) Has been cancelled
2025-06-22 03:11:45 +03:00
f20f1e33eb
chore: bump dependencies 2025-06-22 03:04:40 +03:00
61d4b7377f
colors: remove lazy_static dependency and use LazyLock for COLORS 2025-06-22 02:53:21 +03:00
f7d7c73073
chore: bump nix crate; 0.4.8
Some checks failed
Rust / build (push) Has been cancelled
2025-05-02 12:50:41 +03:00
raf
28e156b9a7
Merge pull request #17 from ErrorNoInternet/main
treewide: clean up a few things
2025-05-02 09:49:13 +00:00
ErrorNoInternet
b814b2bacf
refactor: return write error 2025-04-15 03:46:16 -04:00
ErrorNoInternet
0233cdc0fc
perf: use MaybeUninit for libc buffers 2025-04-15 03:46:16 -04:00
ErrorNoInternet
88c9ff5e13
perf: don't collect args into a Vec 2025-04-15 03:46:16 -04:00
ErrorNoInternet
9713138e94
perf: pre-allocate strings 2025-04-15 03:46:15 -04:00
ErrorNoInternet
c139026704
refactor: clean up a few things
I've decided to keep lazy_static for now as std::sync::LazyLock seems to
have a slightly bigger impact on binary size.
2025-04-15 03:46:15 -04:00
raf
671ce6b8e5
Merge pull request #16 from NotAShelf/no-nix
Some checks failed
Rust / build (push) Has been cancelled
uptime: get rid of nix crate
2025-04-10 05:15:11 +00:00
8df950d340
bump deps; add libc crate 2025-04-10 08:12:19 +03:00
e9aa9a2bbe
uptime: get rid of nix crate 2025-03-30 22:33:16 +03:00
a3b87f8f83
flake: bump nixpkgs
Some checks failed
Rust / build (push) Has been cancelled
2025-03-30 21:48:46 +03:00
9c897b5960
nix: enable parallel building; clean up devshell
Some checks failed
Rust / build (push) Has been cancelled
2025-01-26 01:24:16 +03:00
774442eb63
0.4.6
Some checks failed
Rust / build (push) Has been cancelled
2025-01-26 01:08:44 +03:00
d0f88b179c
further optimize uptime retrival
Try and minimize expensive operations (e.g., divisions and allocations) to hopefully get a
*consistent* measurable performance improvement. In my testing this brings the get_current
function duration from > 1.2600 µs to < 1.2400 µs.
2025-01-26 01:06:02 +03:00
592fb58474
get rid of color_eyre
Should never have added it. Annoyingly long compile times for no reason...
2025-01-26 00:57:02 +03:00
SomeEmptyBox
4fff13a51f
Update memory icon (#15)
Some checks failed
Rust / build (push) Has been cancelled
Makes memory icon bigger by using CPU icon from Nerdfonts instead of the memory icon.
2024-12-19 17:02:32 +00:00
ea8280ef77
docs: update benchmarks to reflect recent improvements
Faster and faster we go.
2024-12-19 20:00:37 +03:00
4b7836d572
append newline to write_all output
Fixes a small bug that resulted in terminal artifacts. My bad, gang.
2024-12-19 19:13:16 +03:00
c97fa33aec
0.4.3
Moar speed, NO_COLOR support
2024-12-19 18:47:04 +03:00
fd18e9d244
release: conditionally improve performance for get_os_pretty_name
It is difficult to get completely accurate benchmarks, given how small
the numbers we are dealing with are, but this seems to point at an
overall trend of *slightly* faster execution. The change minimizes
unnecessary memory allocations and string manipulations, to help ensure
more performant line reading and immediate return upon finding the
PRETTY_NAME without additional, redundant operations.
2024-12-19 18:30:33 +03:00
e19abcedae
docs: update readme; mention NO_COLOR changes 2024-12-19 18:14:23 +03:00
a96effb875
bump version 2024-12-19 18:14:15 +03:00
065216af7c
colors: respect NO_COLOR spec
Microfetch will now respect the NO_COLOR environment variable if it has
been passed to the program. The performance overhead of this operation
is almost none. In addition, the main function has been updated to lock
stdout.
2024-12-19 17:21:56 +03:00
1b0d15a24f
docs: mention nerdfonts requirement; update benchmarks section
Some checks failed
Rust / build (push) Has been cancelled
2024-12-10 21:26:14 +03:00
3960b37089
faster release parsing
Some checks are pending
Rust / build (push) Waiting to run
Around 20% faster when you think about it, but results vary because of hardware race conditions.
2024-12-09 17:30:57 +03:00
raf
8f5bfcbd05
implement --version for checking program version (#10)
Some checks failed
Rust / build (push) Has been cancelled
2024-10-29 16:45:55 +00:00
24 changed files with 2517 additions and 501 deletions

4
.cargo/config.toml Normal file
View file

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

14
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,14 @@
version: 2
updates:
# Update Cargo deps
- package-ecosystem: cargo
directory: "/"
schedule:
interval: "weekly"
# Update used workflows
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily

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

@ -0,0 +1,62 @@
name: hotpath-comment
on:
workflow_run:
workflows: ["hotpath-profile"]
types:
- completed
permissions:
contents: read
pull-requests: write
jobs:
comment:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Download profiling results
uses: actions/download-artifact@v6
with:
name: hotpath-results
path: /tmp/
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- 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:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
HEAD_METRICS=$(cat /tmp/head_timing.json)
BASE_METRICS=$(cat /tmp/base_timing.json)
PR_NUMBER=$(cat /tmp/pr_number.txt)
hotpath profile-pr \
--head-metrics "$HEAD_METRICS" \
--base-metrics "$BASE_METRICS" \
--github-token "$GH_TOKEN" \
--pr-number "$PR_NUMBER"
- name: Post allocation comparison comment
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
HEAD_METRICS=$(cat /tmp/head_alloc.json)
BASE_METRICS=$(cat /tmp/base_alloc.json)
PR_NUMBER=$(cat /tmp/pr_number.txt)
hotpath profile-pr \
--head-metrics "$HEAD_METRICS" \
--base-metrics "$BASE_METRICS" \
--github-token "$GH_TOKEN" \
--pr-number "$PR_NUMBER"

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

@ -0,0 +1,65 @@
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@v6
with:
fetch-depth: 0
- 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"' > /tmp/head_timing.json
- name: Run allocation profiling on HEAD
env:
HOTPATH_JSON: "true"
run: |
cargo run --features='hotpath,hotpath-alloc' 2>&1 | grep '^{"hotpath_profiling_mode"' > /tmp/head_alloc.json
- name: Checkout base branch
run: |
git checkout ${{ 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"' > /tmp/base_timing.json || echo '{}' > /tmp/base_timing.json
- name: Run allocation profiling on base
env:
HOTPATH_JSON: "true"
run: |
cargo run --features='hotpath,hotpath-alloc' 2>&1 | grep '^{"hotpath_profiling_mode"' > /tmp/base_alloc.json || echo '{}' > /tmp/base_alloc.json
- name: Save PR number
run: |
echo '${{ github.event.pull_request.number }}' > /tmp/pr_number.txt
- name: Upload profiling results
uses: actions/upload-artifact@v5
with:
name: hotpath-results
path: |
/tmp/head_timing.json
/tmp/head_alloc.json
/tmp/base_timing.json
/tmp/base_alloc.json
/tmp/pr_number.txt
retention-days: 1

View file

@ -10,12 +10,45 @@ env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
test:
name: Test on ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
steps:
- name: "Checkout"
uses: actions/checkout@v4
- name: "Build with Cargo"
run: cargo build --verbose
uses: actions/checkout@v6
- name: "Setup Rust toolchain"
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.target }}
- name: "Install cross-compilation tools"
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
- name: "Configure linker for aarch64"
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
mkdir -p .cargo
cat >> .cargo/config.toml << EOF
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
EOF
- name: "Build"
run: cargo build --verbose --target ${{ matrix.target }}
- name: "Run tests"
if: matrix.target == 'x86_64-unknown-linux-gnu'
run: cargo test --verbose --target ${{ matrix.target }}

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target
result*
/.direnv

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

1374
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
[package]
name = "microfetch"
version = "0.4.0"
edition = "2021"
name = "microfetch"
version = "0.4.12"
edition = "2024"
[lib]
name = "microfetch_lib"
@ -12,28 +12,57 @@ name = "microfetch"
path = "src/main.rs"
[dependencies]
nix = { version = "0.29", features = ["fs", "hostname", "feature"] }
color-eyre = { version = "0.6", default-features = false }
hotpath = { optional = true, version = "0.8.0" }
libc = "0.2.178"
[dev-dependencies]
criterion = "0.5"
criterion = "0.8.1"
[features]
hotpath = [ "dep:hotpath", "hotpath/hotpath" ]
hotpath-alloc = [ "hotpath/hotpath-alloc" ]
hotpath-off = [ "hotpath/hotpath-off" ]
[[bench]]
name = "benchmark"
harness = false
name = "benchmark"
[profile.dev]
opt-level = 3
opt-level = 1
[profile.release]
strip = true
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
lto = true
opt-level = "s"
panic = "abort"
strip = true
[profile.profiler]
inherits = "release"
debug = true
debug = true
inherits = "release"
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"

215
README.md
View file

@ -1,17 +1,30 @@
<!-- markdownlint-disable MD013 MD033 MD041 -->
<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://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">
<img src="https://img.shields.io/github/stars/notashelf/microfetch?label=stars&color=DEA584" alt="stars">
</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="#benchmarks">Benchmarks</a><br/>
<a href="#installation">Installation</a>
<br/>
</div>
Stupidly simple, laughably fast fetch tool. Written in Rust for speed and ease
of maintainability. Runs in a _fraction of a millisecond_ and displays _most_ of
the nonsense you'd see posted on r/unixporn or other internet communities. Aims
to replace [fastfetch](https://github.com/fastfetch-cli/fastfetch) on my
personal system, but [probably not yours](#customizing). Though, you are more
than welcome to use it on your system: it's pretty [fast...](#benchmarks)
## Synopsis
[fastfetch]: https://github.com/fastfetch-cli/fastfetch
Stupidly small and simple, laughably fast and pretty fetch tool. Written in Rust
for speed and ease of maintainability. Runs in a _fraction of a millisecond_ and
displays _most_ of the nonsense you'd see posted on r/unixporn or other internet
communities. Aims to replace [fastfetch] on my personal system, but
[probably not yours](#customizing). Though, you are more than welcome to use it
on your system: it is pretty _[fast](#benchmarks)_...
<p align="center">
<img
@ -26,6 +39,7 @@ than welcome to use it on your system: it's pretty [fast...](#benchmarks)
- Fast
- Really fast
- Minimal dependencies
- Tiny binary (~370kb [^1])
- Actually really fast
- Cool NixOS logo (other, inferior, distros are not supported)
- Reliable detection of following info:
@ -34,40 +48,90 @@ than welcome to use it on your system: it's pretty [fast...](#benchmarks)
- Name
- Version
- Architecture
- Current shell (from $SHELL, trimmed if store path)
- Current shell (from `$SHELL`, trimmed if store path)
- Current Desktop (DE/WM/Compositor and display backend)
- Memory Usage/Total Memory
- Storage Usage/Total Storage (for `/` only)
- Shell Colors
- 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 indicates, a very fast fetch tool written in C, however,
I am not interested in any of its additional features and I very much dislike
the defaults. Microfetch is a 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. Why? Because I can.
[Rube-Goldmark Machine]: https://en.wikipedia.org/wiki/Rube_Goldberg_machine
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?
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.
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
Microfetch's performance is mostly hardware-dependant, however, the overall
trend seems to be < 2ms on any modern (2015 and after) CPU. Below are the
benchmarks with Hyperfine on my desktop system.
Below are the benchmarks that I've used to back up my claims of Microfetch's
speed. It is fast, it is _very_ fast and that is the point of its existence. It
_could_ be faster, and it will be. Eventually.
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative | Written by raf? |
| :----------- | ----------: | -------: | -------: | -------------: | --------------: |
| `microfetch` | 1.3 ± 0.0 | 1.3 | 1.4 | 1.00 | yes |
| `fastfetch` | 31.9 ± 0.8 | 30.8 | 33.8 | 24.08 ± 0.98 | no |
| `pfetch` | 254.2 ± 4.8 | 246.7 | 264.9 | 191.97 ± 7.10 | no |
| `neofetch` | 735.4 ± 9.5 | 721.1 | 752.8 | 555.48 ± 19.08 | no |
At this point in time, the performance may be sometimes influenced by
hardware-specific race conditions or even your kernel configuration. Which means
that Microfetch's speed may (at times) depend on your hardware setup. However,
even with the worst possible hardware I could find in my house, I've achieved a
nice less-than-1ms invocation time. Which is pretty good. While Microfetch
_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 faster than almost every fetch tool
there is. The only downside of using Rust is introducing more "bloated"
dependency trees and increasing build times. The latter is easily mitigated with
Nix's binary cache, though._
Below are the actual benchmarks with Hyperfine measured on my Desktop system.
The benchmarks were performed under medium system load, and may not be the same
on your system. Please _also_ 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 [µ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
[Criterion.rs]: https://github.com/bheisler/criterion.rs
[Getting Started Guide]: https://bheisler.github.io/criterion.rs/book/getting_started.html
@ -76,11 +140,52 @@ To benchmark individual functions, [Criterion.rs] is used. See Criterion's
[Getting Started Guide] for details or just run `cargo bench` to benchmark all
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
```
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
Microfetch is packaged in [nixpkgs](https://github.com/nixos/nixpkgs). You can
get it through the unstable channel for the time being. The Nix flake can also
be used for bleeding-edge builds.
> [!NOTE]
> You will need a Nerdfonts patched font installed, and for your terminal
> emulator to support said font. Microfetch uses nerdfonts glyphs by default,
> but this can be changed by [patching the program](#customizing).
Microfetch is packaged in [nixpkgs](https://github.com/nixos/nixpkgs). It can be
installed by adding `pkgs.microfetch` to your `environment.systemPackages`.
Additionally, you can try out Microfetch in a Nix shell.
```bash
nix shell nixpkgs#microfetch
```
Or run it directly with `nix run`
```bash
nix run nixpkgs#microfetch
```
Non-Nix users will have to build Microfetch with `cargo`. It is not published
anywhere but I imagine you can use `cargo install --git` to install it from
@ -99,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
@ -121,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
@ -144,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.

View file

@ -1,26 +1,32 @@
use criterion::{criterion_group, criterion_main, Criterion};
use microfetch_lib::colors::print_dots;
use microfetch_lib::desktop::get_desktop_info;
use microfetch_lib::release::{get_os_pretty_name, get_system_info};
use microfetch_lib::system::{
get_memory_usage, get_root_disk_usage, get_shell, get_username_and_hostname,
use criterion::{Criterion, criterion_group, criterion_main};
use microfetch_lib::{
UtsName,
colors::print_dots,
desktop::get_desktop_info,
release::{get_os_pretty_name, get_system_info},
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) {
let utsname = nix::sys::utsname::uname().expect("lol");
c.bench_function("user_info", |b| {
b.iter(|| get_username_and_hostname(&utsname))
});
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("shell", |b| b.iter(get_shell));
let utsname = UtsName::uname().expect("Failed to get uname");
c.bench_function("user_info", |b| {
b.iter(|| get_username_and_hostname(&utsname));
});
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("shell", |b| b.iter(get_shell));
c.bench_function("desktop", |b| b.iter(get_desktop_info));
c.bench_function("uptime", |b| b.iter(get_current));
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("colors", |b| b.iter(print_dots));
c.bench_function("desktop", |b| b.iter(get_desktop_info));
c.bench_function("uptime", |b| b.iter(get_current));
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("colors", |b| b.iter(print_dots));
}
criterion_group!(benches, main_benchmark);

10
flake.lock generated
View file

@ -2,15 +2,19 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1722719969,
"narHash": "sha256-E47qbT/mRtBCSZra+9S9208sp/QnNeOAq7EhHX+eMNE=",
"lastModified": 1743359643,
"narHash": "sha256-RkyJ9a67s0zEIz4O66TyZOIGh4TFZ4dKHKMgnxZCh2I=",
"lastModified": 1763381801,
"narHash": "sha256-325fR0JmHW7B74/gHPv/S9w1Rfj/M2HniwQFUwdrZ9k=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "83a364ced9d5b8a6bdac897bbef6b91e70777b97",
"rev": "ca77b4bc80e558ce59f2712fdb276f90c0ee309a",
"rev": "46931757ea8bdbba25c076697f8e73b8dc39fef5",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}

View file

@ -1,6 +1,6 @@
{
description = "A microscopic fetch script in Rust, for NixOS systems";
inputs.nixpkgs.url = "github:NixOS/nixpkgs";
inputs.nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable";
outputs = {
self,
@ -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: {

View file

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

View file

@ -1,27 +1,31 @@
{
mkShell,
cargo,
rustc,
mold,
clang,
rust-analyzer-unwrapped,
rustfmt,
clippy,
cargo,
rustc,
gcc,
taplo,
rustPlatform,
gnuplot,
}:
mkShell {
name = "microfetch";
strictDeps = true;
nativeBuildInputs = [
cargo
rustc
gcc
mold
clang
rust-analyzer-unwrapped
rustfmt
(rustfmt.override {asNightly = true;})
clippy
taplo
gnuplot # For Criterion.rs plots
gnuplot # for Criterion.rs plots
];
env.RUST_SRC_PATH = "${rustPlatform.rustLibSrc}";

View file

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

View file

@ -1,28 +1,42 @@
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_result = std::env::var("XDG_SESSION_TYPE");
// Capitalize the first letter of the display backend value
let mut display_backend = display_backend_result.unwrap_or_default();
if let Some(c) = display_backend.as_mut_str().get_mut(0..1) {
c.make_ascii_uppercase();
}
// Trim "none+" from the start of desktop_env if present
// Use "Unknown" if desktop_env is empty or has an error
let desktop_env = match desktop_env {
Err(_) => "Unknown".to_string(),
Ok(s) => s.trim_start_matches("none+").to_string(),
};
// Handle the case where display_backend might be empty after capitalization
let display_backend = if display_backend.is_empty() {
"Unknown"
// Retrieve the environment variables and handle Result types
let desktop_str = unsafe {
let ptr = libc::getenv(c"XDG_CURRENT_DESKTOP".as_ptr());
if ptr.is_null() {
"Unknown"
} else {
&display_backend
let s = CStr::from_ptr(ptr).to_str().unwrap_or("Unknown");
s.strip_prefix("none+").unwrap_or(s)
}
.to_string();
};
format!("{desktop_env} ({display_backend})")
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 + ")"
// Capitalize first char needs temporary allocation only if backend exists
let mut result =
String::with_capacity(desktop_str.len() + backend_str.len() + 3);
result.push_str(desktop_str);
result.push_str(" (");
// Capitalize first character of backend
if let Some(first_char) = backend_str.chars().next() {
let _ = write!(result, "{}", first_char.to_ascii_uppercase());
result.push_str(&backend_str[first_char.len_utf8()..]);
}
result.push(')');
result
}

View file

@ -1,5 +1,46 @@
pub mod colors;
pub mod desktop;
pub mod release;
pub mod syscall;
pub mod system;
pub mod uptime;
use std::mem::MaybeUninit;
/// Wrapper for `libc::utsname` with safe accessor methods
pub struct UtsName(libc::utsname);
impl UtsName {
/// Calls `uname` syscall and returns a `UtsName` wrapper
///
/// # Errors
///
/// Returns an error if the `uname` syscall fails
pub fn uname() -> Result<Self, std::io::Error> {
let mut uts = MaybeUninit::uninit();
if unsafe { libc::uname(uts.as_mut_ptr()) } != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(Self(unsafe { uts.assume_init() }))
}
#[must_use]
pub const fn nodename(&self) -> &std::ffi::CStr {
unsafe { std::ffi::CStr::from_ptr(self.0.nodename.as_ptr()) }
}
#[must_use]
pub const fn sysname(&self) -> &std::ffi::CStr {
unsafe { std::ffi::CStr::from_ptr(self.0.sysname.as_ptr()) }
}
#[must_use]
pub const fn release(&self) -> &std::ffi::CStr {
unsafe { std::ffi::CStr::from_ptr(self.0.release.as_ptr()) }
}
#[must_use]
pub const fn machine(&self) -> &std::ffi::CStr {
unsafe { std::ffi::CStr::from_ptr(self.0.machine.as_ptr()) }
}
}

View file

@ -1,79 +1,112 @@
mod colors;
mod desktop;
mod release;
mod syscall;
mod system;
mod uptime;
use std::io::Write;
use std::io::{self, Cursor, Write};
use crate::colors::{print_dots, BLUE, CYAN, RESET};
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;
pub use microfetch_lib::UtsName;
use color_eyre::Report;
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,
};
fn main() -> Result<(), Report> {
color_eyre::install()?;
let utsname = nix::sys::utsname::uname()?;
#[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 = 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(),
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)?;
}
print_system_info(&fields);
Ok(())
Ok(())
}
// Struct to hold all the fields we need to print
// helps avoid clippy warnings about argument count
// and makes it easier to pass around, though its
// not like we need to
// 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
// easier to pass data around. Though, it is not like we really need to.
struct Fields {
user_info: String,
os_name: String,
kernel_version: String,
shell: String,
uptime: String,
desktop: String,
memory_usage: String,
storage: String,
colors: String,
user_info: String,
os_name: String,
kernel_version: String,
shell: String,
uptime: String,
desktop: String,
memory_usage: String,
storage: String,
colors: String,
}
fn print_system_info(fields: &Fields) {
let Fields {
user_info,
os_name,
kernel_version,
shell,
uptime,
desktop,
memory_usage,
storage,
colors,
} = fields;
#[cfg_attr(feature = "hotpath", hotpath::measure)]
fn print_system_info(
fields: &Fields,
) -> Result<(), Box<dyn std::error::Error>> {
use crate::colors::COLORS;
let _ = std::io::stdout().write_all(format!(
"
{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}Storage (/){RESET} {storage}
{CYAN} {BLUE} {CYAN} {BLUE}Colors{RESET} {colors}
").as_bytes());
let Fields {
user_info,
os_name,
kernel_version,
shell,
uptime,
desktop,
memory_usage,
storage,
colors,
} = fields;
let cyan = COLORS.cyan;
let blue = COLORS.blue;
let reset = COLORS.reset;
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}Storage (/){reset} {storage}
{cyan} {blue} {cyan} {blue}Colors{reset} {colors}\n\n"
)?;
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(())
}

View file

@ -1,27 +1,69 @@
use color_eyre::Result;
use nix::sys::utsname::UtsName;
use std::{
fs::File,
io::{self, Read},
};
use std::{fmt::Write as _, io};
pub fn get_system_info(utsname: &UtsName) -> nix::Result<String> {
Ok(format!(
"{} {} ({})",
utsname.sysname().to_str().unwrap_or("Unknown"),
utsname.release().to_str().unwrap_or("Unknown"),
utsname.machine().to_str().unwrap_or("Unknown")
))
use crate::{UtsName, syscall::read_file_fast};
#[must_use]
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_system_info(utsname: &UtsName) -> String {
let sysname = utsname.sysname().to_str().unwrap_or("Unknown");
let release = utsname.release().to_str().unwrap_or("Unknown");
let machine = utsname.machine().to_str().unwrap_or("Unknown");
// Pre-allocate capacity: sysname + " " + release + " (" + machine + ")"
let capacity = sysname.len() + 1 + release.len() + 2 + machine.len() + 1;
let mut result = String::with_capacity(capacity);
write!(result, "{sysname} {release} ({machine})").unwrap();
result
}
/// 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> {
let mut os_release_content = String::with_capacity(1024);
File::open("/etc/os-release")?.read_to_string(&mut os_release_content)?;
// Fast byte-level scanning for PRETTY_NAME=
const PREFIX: &[u8] = b"PRETTY_NAME=";
let pretty_name = os_release_content
.lines()
.find(|line| line.starts_with("PRETTY_NAME="))
.map(|line| line.trim_start_matches("PRETTY_NAME=").trim_matches('"'));
let mut buffer = [0u8; 1024];
Ok(pretty_name.unwrap_or("Unknown").to_string())
// Use fast syscall-based file reading
let bytes_read = read_file_fast("/etc/os-release", &mut buffer)?;
let content = &buffer[..bytes_read];
let mut offset = 0;
while offset < content.len() {
let remaining = &content[offset..];
// Find newline or end
let line_end = remaining
.iter()
.position(|&b| b == b'\n')
.unwrap_or(remaining.len());
let line = &remaining[..line_end];
if line.starts_with(PREFIX) {
let value = &line[PREFIX.len()..];
// Strip quotes if present
let trimmed = if value.len() >= 2
&& value[0] == b'"'
&& value[value.len() - 1] == b'"'
{
&value[1..value.len() - 1]
} else {
value
};
// Convert to String - should be valid UTF-8
return Ok(String::from_utf8_lossy(trimmed).into_owned());
}
offset += line_end + 1;
}
Ok("Unknown".to_owned())
}

210
src/syscall.rs Normal file
View file

@ -0,0 +1,210 @@
//! Incredibly fast syscall wrappers for using inline assembly. Serves the
//! purposes of completely bypassing Rust's standard library in favor of
//! handwritten Assembly. Is this a good idea? No. Is it fast? Yeah, but only
//! marginally. Either way it serves a purpose and I will NOT accept criticism.
//! What do you mean I wasted two whole hours to make the program only 100µs
//! faster?
//!
//! Supports `x86_64` and `aarch64` architectures. Riscv support will be
//! implemented when and ONLY WHEN I can be bothered to work on it.
use std::io;
/// Direct syscall to open a file
///
/// # Returns
///
/// File descriptor or -1 on error
///
/// # Safety
///
/// The caller must ensure:
///
/// - `path` points to a valid null-terminated C string
/// - The pointer remains valid for the duration of the syscall
#[inline]
#[must_use]
pub unsafe fn sys_open(path: *const u8, flags: i32) -> i32 {
#[cfg(target_arch = "x86_64")]
unsafe {
let fd: i64;
std::arch::asm!(
"syscall",
in("rax") 2i64, // SYS_open
in("rdi") path,
in("rsi") flags,
in("rdx") 0i32, // mode (not used for reading)
lateout("rax") fd,
lateout("rcx") _,
lateout("r11") _,
options(nostack)
);
#[allow(clippy::cast_possible_truncation)]
{
fd as i32
}
}
#[cfg(target_arch = "aarch64")]
unsafe {
let fd: i64;
std::arch::asm!(
"svc #0",
in("x8") 56i64, // SYS_openat
in("x0") -100i32, // AT_FDCWD
in("x1") path,
in("x2") flags,
in("x3") 0i32, // mode
lateout("x0") fd,
options(nostack)
);
#[allow(clippy::cast_possible_truncation)]
{
fd as i32
}
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
{
compile_error!("Unsupported architecture for inline assembly syscalls");
}
}
/// Direct syscall to read from a file descriptor
///
/// # Returns n
///
/// Number of bytes read or -1 on error
///
/// # Safety
///
/// The caller must ensure:
/// - `buf` points to a valid writable buffer of at least `count` bytes
/// - `fd` is a valid open file descriptor
#[inline]
pub unsafe fn sys_read(fd: i32, buf: *mut u8, count: usize) -> isize {
#[cfg(target_arch = "x86_64")]
unsafe {
let ret: i64;
std::arch::asm!(
"syscall",
in("rax") 0i64, // SYS_read
in("rdi") fd,
in("rsi") buf,
in("rdx") count,
lateout("rax") ret,
lateout("rcx") _,
lateout("r11") _,
options(nostack)
);
#[allow(clippy::cast_possible_truncation)]
{
ret as isize
}
}
#[cfg(target_arch = "aarch64")]
unsafe {
let ret: i64;
std::arch::asm!(
"svc #0",
in("x8") 63i64, // SYS_read
in("x0") fd,
in("x1") buf,
in("x2") count,
lateout("x0") ret,
options(nostack)
);
#[allow(clippy::cast_possible_truncation)]
{
ret as isize
}
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
{
compile_error!("Unsupported architecture for inline assembly syscalls");
}
}
/// Direct syscall to close a file descriptor
///
/// # Safety
///
/// The caller must ensure `fd` is a valid open file descriptor
#[inline]
#[must_use]
pub unsafe fn sys_close(fd: i32) -> i32 {
#[cfg(target_arch = "x86_64")]
unsafe {
let ret: i64;
std::arch::asm!(
"syscall",
in("rax") 3i64, // SYS_close
in("rdi") fd,
lateout("rax") ret,
lateout("rcx") _,
lateout("r11") _,
options(nostack)
);
#[allow(clippy::cast_possible_truncation)]
{
ret as i32
}
}
#[cfg(target_arch = "aarch64")]
unsafe {
let ret: i64;
std::arch::asm!(
"svc #0",
in("x8") 57i64, // SYS_close
in("x0") fd,
lateout("x0") ret,
options(nostack)
);
#[allow(clippy::cast_possible_truncation)]
{
ret as i32
}
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
{
compile_error!("Unsupported architecture for inline assembly syscalls");
}
}
/// Read entire file using direct syscalls. This avoids libc overhead and can be
/// significantly faster for small files.
///
/// # Errors
///
/// Returns an error if the file cannot be opened or read
#[inline]
pub fn read_file_fast(path: &str, buffer: &mut [u8]) -> io::Result<usize> {
const O_RDONLY: i32 = 0;
// Use stack-allocated buffer for null-terminated path (max 256 bytes)
let path_bytes = path.as_bytes();
if path_bytes.len() >= 256 {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Path too long"));
}
let mut path_buf = [0u8; 256];
path_buf[..path_bytes.len()].copy_from_slice(path_bytes);
// XXX: Already zero-terminated since array is initialized to zeros
unsafe {
let fd = sys_open(path_buf.as_ptr(), O_RDONLY);
if fd < 0 {
return Err(io::Error::last_os_error());
}
let bytes_read = sys_read(fd, buffer.as_mut_ptr(), buffer.len());
let _ = sys_close(fd);
if bytes_read < 0 {
return Err(io::Error::last_os_error());
}
#[allow(clippy::cast_sign_loss)]
{
Ok(bytes_read as usize)
}
}
}

View file

@ -1,84 +1,184 @@
use color_eyre::Result;
use nix::sys::{statvfs::statvfs, utsname::UtsName};
use std::{ffi::CStr, fmt::Write as _, io, mem::MaybeUninit};
use std::{
env,
fs::File,
io::{self, Read},
};
use crate::colors::{CYAN, GREEN, RED, RESET, YELLOW};
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("unknown_user".to_string());
let hostname = utsname
.nodename()
.to_str()
.unwrap_or("unknown_host")
.to_string();
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");
format!("{YELLOW}{username}{RED}@{GREEN}{hostname}")
let capacity = COLORS.yellow.len()
+ username.len()
+ COLORS.red.len()
+ 1
+ COLORS.green.len()
+ hostname.len()
+ COLORS.reset.len();
let mut result = String::with_capacity(capacity);
result.push_str(COLORS.yellow);
result.push_str(username);
result.push_str(COLORS.red);
result.push('@');
result.push_str(COLORS.green);
result.push_str(hostname);
result.push_str(COLORS.reset);
result
}
#[must_use]
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_shell() -> String {
let shell_path = env::var("SHELL").unwrap_or("unknown_shell".to_string());
let shell_name = shell_path.rsplit('/').next().unwrap_or("unknown_shell");
shell_name.to_string()
}
pub fn get_root_disk_usage() -> Result<String, io::Error> {
let vfs = statvfs("/")?;
let block_size = vfs.block_size() as u64;
let total_blocks = vfs.blocks();
let available_blocks = vfs.blocks_available();
let total_size = block_size * total_blocks;
let used_size = total_size - (block_size * available_blocks);
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 usage = (used_size as f64 / total_size as f64) * 100.0;
Ok(format!(
"{used_size:.2} GiB / {total_size:.2} GiB ({CYAN}{usage:.0}%{RESET})"
))
}
pub fn get_memory_usage() -> Result<String, io::Error> {
#[inline(always)]
fn parse_memory_info() -> Result<(f64, f64), io::Error> {
let mut total_memory_kb = 0.0;
let mut available_memory_kb = 0.0;
let mut meminfo = String::with_capacity(2048);
File::open("/proc/meminfo")?.read_to_string(&mut meminfo)?;
for line in meminfo.lines() {
let mut split = line.split_whitespace();
match split.next().unwrap_or_default() {
"MemTotal:" => total_memory_kb = split.next().unwrap_or("0").parse().unwrap_or(0.0),
"MemAvailable:" => {
available_memory_kb = split.next().unwrap_or("0").parse().unwrap_or(0.0);
// MemTotal comes before MemAvailable, stop parsing
break;
}
_ => (),
}
}
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))
unsafe {
let ptr = libc::getenv(c"SHELL".as_ptr());
if ptr.is_null() {
return "unknown_shell".into();
}
let (used_memory, total_memory) = parse_memory_info()?;
let percentage_used = (used_memory / total_memory * 100.0).round() as u64;
Ok(format!(
"{used_memory:.2} GiB / {total_memory:.2} GiB ({CYAN}{percentage_used}%{RESET})"
))
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.
///
/// # 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> {
let mut vfs = MaybeUninit::uninit();
let path = b"/\0";
if unsafe { libc::statvfs(path.as_ptr().cast(), vfs.as_mut_ptr()) } != 0 {
return Err(io::Error::last_os_error());
}
let vfs = unsafe { vfs.assume_init() };
let block_size = vfs.f_bsize;
let total_blocks = vfs.f_blocks;
let available_blocks = vfs.f_bavail;
let total_size = block_size * total_blocks;
let used_size = total_size - (block_size * available_blocks);
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 usage = (used_size / total_size) * 100.0;
let mut result = String::with_capacity(64);
write!(
result,
"{used_size:.2} GiB / {total_size:.2} GiB ({cyan}{usage:.0}%{reset})",
cyan = COLORS.cyan,
reset = COLORS.reset,
)
.unwrap();
Ok(result)
}
/// Fast integer parsing without stdlib overhead
#[inline]
fn parse_u64_fast(s: &[u8]) -> u64 {
let mut result = 0u64;
for &byte in s {
if byte.is_ascii_digit() {
result = result * 10 + u64::from(byte - b'0');
} else {
break;
}
}
result
}
/// Gets the system memory usage information.
///
/// # Errors
///
/// Returns an error if `/proc/meminfo` cannot be read.
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_memory_usage() -> Result<String, io::Error> {
#[cfg_attr(feature = "hotpath", hotpath::measure)]
fn parse_memory_info() -> Result<(f64, f64), io::Error> {
let mut total_memory_kb = 0u64;
let mut available_memory_kb = 0u64;
let mut buffer = [0u8; 1024];
// Use fast syscall-based file reading
let bytes_read = read_file_fast("/proc/meminfo", &mut buffer)?;
let meminfo = &buffer[..bytes_read];
// Fast scanning for MemTotal and MemAvailable
let mut offset = 0;
let mut found_total = false;
let mut found_available = false;
while offset < meminfo.len() && (!found_total || !found_available) {
let remaining = &meminfo[offset..];
// Find newline or end
let line_end = remaining
.iter()
.position(|&b| b == b'\n')
.unwrap_or(remaining.len());
let line = &remaining[..line_end];
if line.starts_with(b"MemTotal:") {
// Skip "MemTotal:" and whitespace
let mut pos = 9;
while pos < line.len() && line[pos].is_ascii_whitespace() {
pos += 1;
}
total_memory_kb = parse_u64_fast(&line[pos..]);
found_total = true;
} else if line.starts_with(b"MemAvailable:") {
// Skip "MemAvailable:" and whitespace
let mut pos = 13;
while pos < line.len() && line[pos].is_ascii_whitespace() {
pos += 1;
}
available_memory_kb = parse_u64_fast(&line[pos..]);
found_available = true;
}
offset += line_end + 1;
}
#[allow(clippy::cast_precision_loss)]
let total_memory_gb = total_memory_kb as f64 / 1024.0 / 1024.0;
#[allow(clippy::cast_precision_loss)]
let available_memory_gb = available_memory_kb as f64 / 1024.0 / 1024.0;
let used_memory_gb = total_memory_gb - available_memory_gb;
Ok((used_memory_gb, total_memory_gb))
}
let (used_memory, total_memory) = parse_memory_info()?;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let percentage_used = (used_memory / total_memory * 100.0).round() as u64;
let mut result = String::with_capacity(64);
write!(
result,
"{used_memory:.2} GiB / {total_memory:.2} GiB \
({cyan}{percentage_used}%{reset})",
cyan = COLORS.cyan,
reset = COLORS.reset,
)
.unwrap();
Ok(result)
}

View file

@ -1,26 +1,114 @@
use color_eyre::Result;
use nix::sys::sysinfo::sysinfo;
use std::io;
use std::{io, mem::MaybeUninit};
pub fn get_current() -> Result<String, io::Error> {
let info = sysinfo().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let uptime_seconds = info.uptime().as_secs_f64();
/// Faster integer to string conversion without the formatting overhead.
#[inline]
fn itoa(mut n: u64, buf: &mut [u8]) -> &str {
if n == 0 {
return "0";
}
let total_minutes = (uptime_seconds / 60.0).round() as u64;
let days = total_minutes / (60 * 24);
let hours = (total_minutes % (60 * 24)) / 60;
let minutes = total_minutes % 60;
let mut i = buf.len();
while n > 0 {
i -= 1;
buf[i] = b'0' + (n % 10) as u8;
n /= 10;
}
let mut parts = Vec::with_capacity(3);
if days > 0 {
parts.push(format!("{days} days"));
}
if hours > 0 || days > 0 {
parts.push(format!("{hours} hours"));
}
if minutes > 0 || hours > 0 || days > 0 {
parts.push(format!("{minutes} minutes"));
}
Ok(parts.join(", "))
unsafe { std::str::from_utf8_unchecked(&buf[i..]) }
}
/// Direct `sysinfo` syscall using inline assembly
///
/// # Safety
///
/// This function uses inline assembly to make a direct syscall.
/// The caller must ensure the sysinfo pointer is valid.
#[inline]
unsafe fn sys_sysinfo(info: *mut libc::sysinfo) -> i64 {
#[cfg(target_arch = "x86_64")]
{
let ret: i64;
unsafe {
std::arch::asm!(
"syscall",
in("rax") 99_i64, // __NR_sysinfo
in("rdi") info,
out("rcx") _,
out("r11") _,
lateout("rax") ret,
options(nostack)
);
}
ret
}
#[cfg(target_arch = "aarch64")]
{
let ret: i64;
unsafe {
std::arch::asm!(
"svc #0",
in("x8") 179_i64, // __NR_sysinfo
in("x0") info,
lateout("x0") ret,
options(nostack)
);
}
ret
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
{
unsafe { libc::sysinfo(info) as i64 }
}
}
/// Gets the current system uptime.
///
/// # Errors
///
/// Returns an error if the system uptime cannot be retrieved.
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_current() -> Result<String, io::Error> {
let uptime_seconds = {
let mut info = MaybeUninit::uninit();
if unsafe { sys_sysinfo(info.as_mut_ptr()) } != 0 {
return Err(io::Error::last_os_error());
}
#[allow(clippy::cast_sign_loss)]
unsafe {
info.assume_init().uptime as u64
}
};
let days = uptime_seconds / 86400;
let hours = (uptime_seconds / 3600) % 24;
let minutes = (uptime_seconds / 60) % 60;
let mut result = String::with_capacity(32);
let mut buf = [0u8; 20]; // Enough for u64::MAX
if days > 0 {
result.push_str(itoa(days, &mut buf));
result.push_str(if days == 1 { " day" } else { " days" });
}
if hours > 0 {
if !result.is_empty() {
result.push_str(", ");
}
result.push_str(itoa(hours, &mut buf));
result.push_str(if hours == 1 { " hour" } else { " hours" });
}
if minutes > 0 {
if !result.is_empty() {
result.push_str(", ");
}
result.push_str(itoa(minutes, &mut buf));
result.push_str(if minutes == 1 { " minute" } else { " minutes" });
}
if result.is_empty() {
result.push_str("less than a minute");
}
Ok(result)
}