Compare commits

..

1 commit

Author SHA1 Message Date
0e770da264 chore: tag 0.4.10
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a2158a305d5f249b52c8b21dc5aaca86a6a6964
2025-11-17 17:42:15 +03:00
15 changed files with 174 additions and 792 deletions

View file

@ -1,49 +0,0 @@
name: Hotpath Comment
on:
workflow_run:
workflows: ["Hotpath Profile"]
types:
- completed
permissions:
pull-requests: write
jobs:
comment:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Download profiling results
uses: actions/download-artifact@v4
with:
name: hotpath-results
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- name: Read PR number
id: pr
run: echo "number=$(cat pr_number.txt)" >> $GITHUB_OUTPUT
- name: Setup Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install hotpath CLI
run: cargo install hotpath
- name: Post timing comparison comment
run: |
hotpath profile-pr \
--head-metrics head-timing.json \
--base-metrics base-timing.json \
--github-token ${{ secrets.GITHUB_TOKEN }} \
--pr-number ${{ steps.pr.outputs.number }}
- name: Post allocation comparison comment
run: |
hotpath profile-pr \
--head-metrics head-alloc.json \
--base-metrics base-alloc.json \
--github-token ${{ secrets.GITHUB_TOKEN }} \
--pr-number ${{ steps.pr.outputs.number }}

View file

@ -1,63 +0,0 @@
name: Hotpath Profile
on:
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
jobs:
profile:
runs-on: ubuntu-latest
steps:
- name: Checkout PR HEAD
uses: actions/checkout@v4
- name: Setup Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Run timing profiling on HEAD
env:
HOTPATH_JSON: "true"
run: |
cargo run --features='hotpath' 2>&1 | grep '^{"hotpath_profiling_mode"' > head-timing.json
- name: Run allocation profiling on HEAD
env:
HOTPATH_JSON: "true"
run: |
cargo run --features='hotpath,hotpath-alloc-count-total' 2>&1 | grep '^{"hotpath_profiling_mode"' > head-alloc.json
- name: Checkout base branch
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: Run timing profiling on base
env:
HOTPATH_JSON: "true"
run: |
cargo run --features='hotpath' 2>&1 | grep '^{"hotpath_profiling_mode"' > base-timing.json
- name: Run allocation profiling on base
env:
HOTPATH_JSON: "true"
run: |
cargo run --features='hotpath,hotpath-alloc-count-total' 2>&1 | grep '^{"hotpath_profiling_mode"' > base-alloc.json
- name: Save PR number
run: echo "${{ github.event.number }}" > pr_number.txt
- name: Upload profiling results
uses: actions/upload-artifact@v4
with:
name: hotpath-results
path: |
head-timing.json
head-alloc.json
base-timing.json
base-alloc.json
pr_number.txt
retention-days: 1

27
Cargo.lock generated
View file

@ -143,6 +143,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chunked_transfer"
version = "1.5.0"
@ -684,7 +690,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@ -726,9 +732,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.177"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "libredox"
@ -766,11 +772,12 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "microfetch"
version = "0.4.11"
version = "0.4.10"
dependencies = [
"criterion",
"hotpath",
"libc",
"nix",
]
[[package]]
@ -783,6 +790,18 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "num-conv"
version = "0.1.0"

View file

@ -1,6 +1,6 @@
[package]
name = "microfetch"
version = "0.4.11"
version = "0.4.10"
edition = "2024"
[lib]
@ -12,8 +12,9 @@ name = "microfetch"
path = "src/main.rs"
[dependencies]
hotpath = { optional = true, version = "0.6.0" }
libc = "0.2.177"
hotpath = { optional = true, version = "0.6" }
libc = "0.2.175"
nix = { default-features = false, features = [ "fs", "hostname", "feature" ], version = "0.30.1" }
[dev-dependencies]
criterion = "0.7"

144
README.md
View file

@ -1,34 +1,17 @@
<!-- 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/stars/notashelf/microfetch?label=stars&color=DEA584" alt="stars">
<!-- <img src="https://img.shields.io/github/v/release/notashelf/microfetch?display_name=tag&color=DEA584"> -->
<img src="https://img.shields.io/github/stars/notashelf/microfetch?label=stars&color=DEA584">
</div>
<div 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>
<h1 align="center">Microfetch</h1>
## Synopsis
[fastfetch]: https://github.com/fastfetch-cli/fastfetch
Stupidly small and simple, laughably fast and pretty fetch tool. Written in Rust
for speed and ease of maintainability. Runs in a _fraction of a millisecond_ and
displays _most_ of the nonsense you'd see posted on r/unixporn or other internet
communities. Aims to replace [fastfetch] on my personal system, but
[probably not yours](#customizing). Though, you are more than welcome to use it
on your system: it is pretty _[fast](#benchmarks)_...
Stupidly simple, laughably fast fetch tool. Written in Rust for speed and ease
of maintainability. Runs in a _fraction of a millisecond_ and displays _most_ of
the nonsense you'd see posted on r/unixporn or other internet communities. Aims
to replace [fastfetch](https://github.com/fastfetch-cli/fastfetch) on my
personal system, but [probably not yours](#customizing). Though, you are more
than welcome to use it on your system: it's pretty [fast](#benchmarks)...
<p align="center">
<img
@ -43,7 +26,6 @@ on your system: it is pretty _[fast](#benchmarks)_...
- Fast
- Really fast
- Minimal dependencies
- Tiny binary (~370kb)
- Actually really fast
- Cool NixOS logo (other, inferior, distros are not supported)
- Reliable detection of following info:
@ -62,64 +44,39 @@ on your system: it is pretty _[fast](#benchmarks)_...
## Motivation
Fastfetch, as its name probably hinted, is a very fast fetch tool written in C.
However, I am not interested in _any_ of its additional features, and I'm not
interested in its configuration options. Sure I can _configure_ it when I
dislike the defaults, but how often would I really change the configuration...
Fastfetch, as its name indicates, a very fast fetch tool written in C, however,
I am not interested in any of its additional features, such as customization,
and I very much dislike the defaults. Microfetch is my response to this problem,
a _very fast_ fetch tool that you would normally write in Bash and put in your
`~/.bashrc` but actually _really_ fast because it opts-out of all customization
options provided by Fastfetch, and is written in Rust. Why? Because I can, and
because I prefer Rust for "structured" Bash scripts.
Microfetch is my response to this problem. It is an _even faster_ fetch tool
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.
I cannot re-iterate it enough, Microfetch is _annoyingly fast_.
## Benchmarks
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.
The performance may be sometimes influenced by hardware-specific race
conditions, or even your kernel configuration meaning it may (at times) depend
on your hardware. However, the overall trend appears to be less than 1.3ms on
any modern (2015 and after) CPU that I own. Below are the benchmarks with
Hyperfine on my desktop system. Please note that those benchmarks will not be
always kept up to date, but I will try to update the numbers as I make
Microfetch faster.
At this point in time, the performance may be sometimes influenced by
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.
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative | Written by raf? |
| :----------- | -----------: | -------: | -------: | -------------: | --------------: |
| `microfetch` | 1.0 ± 0.1 | 0.9 | 1.7 | 1.00 | yes |
| `fastfetch` | 48.6 ± 1.6 | 45.8 | 61.3 | 46.65 ± 4.75 | no |
| `pfetch` | 206.0 ± 4.5 | 198.0 | 241.4 | 197.50 ± 19.53 | no |
| `neofetch` | 689.1 ± 29.1 | 637.7 | 811.2 | 660.79 ± 69.56 | no |
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.
As far as I'm concerned, Microfetch is significantly faster than every other
fetch tool that I have tried. The only downsides of using Rust for the project
(in exchange for speed and maintainability) is the slightly "bloated" dependency
trees, and the increased build times. The latter is very easily mitigated with
Nix's binary cache. Since Microfetch is already in Nixpkgs, you are recommended
to use it to utilize the binary cache properly
### Benchmarking Individual Functions
@ -130,38 +87,11 @@ 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-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
> [!NOTE]
> You will need a Nerdfonts patched font installed, and for your terminal
> emulator to support said font. Microfetch uses nerdfonts glyphs by default,
> but this can be changed by [patching the program](#customizing).
> emulator to support said font. Microfetch uses nerdfonts glyphs by default.
Microfetch is packaged in [nixpkgs](https://github.com/nixos/nixpkgs). It can be
installed by adding `pkgs.microfetch` to your `environment.systemPackages`.

View file

@ -1,6 +1,5 @@
use criterion::{Criterion, criterion_group, criterion_main};
use microfetch_lib::{
UtsName,
colors::print_dots,
desktop::get_desktop_info,
release::{get_os_pretty_name, get_system_info},
@ -14,7 +13,7 @@ use microfetch_lib::{
};
fn main_benchmark(c: &mut Criterion) {
let utsname = UtsName::uname().expect("Failed to get uname");
let utsname = nix::sys::utsname::uname().expect("lol");
c.bench_function("user_info", |b| {
b.iter(|| get_username_and_hostname(&utsname));
});

View file

@ -20,7 +20,6 @@ in
(fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /src))
(s + /Cargo.lock)
(s + /Cargo.toml)
(s + /benches)
];
};

View file

@ -37,7 +37,7 @@ impl Colors {
}
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();
Colors::new(is_no_color)
});
@ -45,37 +45,14 @@ pub static COLORS: LazyLock<Colors> = LazyLock::new(|| {
#[must_use]
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn print_dots() -> String {
// Pre-calculate capacity: 6 color codes + " " (glyph + 2 spaces) per color
const GLYPH: &str = "";
let capacity = COLORS.blue.len()
+ COLORS.cyan.len()
+ COLORS.green.len()
+ COLORS.yellow.len()
+ COLORS.red.len()
+ COLORS.magenta.len()
+ COLORS.reset.len()
+ (GLYPH.len() + 2) * 6;
let mut result = String::with_capacity(capacity);
result.push_str(COLORS.blue);
result.push_str(GLYPH);
result.push_str(" ");
result.push_str(COLORS.cyan);
result.push_str(GLYPH);
result.push_str(" ");
result.push_str(COLORS.green);
result.push_str(GLYPH);
result.push_str(" ");
result.push_str(COLORS.yellow);
result.push_str(GLYPH);
result.push_str(" ");
result.push_str(COLORS.red);
result.push_str(GLYPH);
result.push_str(" ");
result.push_str(COLORS.magenta);
result.push_str(GLYPH);
result.push_str(" ");
result.push_str(COLORS.reset);
result
format!(
"{} {} {} {} {} {} {}",
COLORS.blue,
COLORS.cyan,
COLORS.green,
COLORS.yellow,
COLORS.red,
COLORS.magenta,
COLORS.reset,
)
}

View file

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

View file

@ -1,45 +1,5 @@
pub mod colors;
pub mod desktop;
pub mod release;
pub mod syscall;
pub mod system;
pub mod uptime;
use std::mem::MaybeUninit;
/// Wrapper for `libc::utsname` with safe accessor methods
pub struct UtsName(libc::utsname);
impl UtsName {
/// Calls `uname` syscall and returns a `UtsName` wrapper
///
/// # Errors
/// Returns an error if the uname syscall fails
pub fn uname() -> Result<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,14 +1,11 @@
mod colors;
mod desktop;
mod release;
mod syscall;
mod system;
mod uptime;
use std::io::{Write, stdout};
pub use microfetch_lib::UtsName;
use crate::{
colors::print_dots,
desktop::get_desktop_info,
@ -27,7 +24,7 @@ fn main() -> Result<(), Box<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 utsname = nix::sys::utsname::uname()?;
let fields = Fields {
user_info: get_username_and_hostname(&utsname),
os_name: get_os_pretty_name()?,

View file

@ -1,69 +1,37 @@
use std::{fmt::Write as _, io};
use std::{
fs::File,
io::{self, BufRead, BufReader},
};
use crate::{UtsName, syscall::read_file_fast};
use nix::sys::utsname::UtsName;
#[must_use]
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_system_info(utsname: &UtsName) -> String {
let sysname = utsname.sysname().to_str().unwrap_or("Unknown");
let release = utsname.release().to_str().unwrap_or("Unknown");
let machine = utsname.machine().to_str().unwrap_or("Unknown");
// Pre-allocate capacity: sysname + " " + release + " (" + machine + ")"
let capacity = sysname.len() + 1 + release.len() + 2 + machine.len() + 1;
let mut result = String::with_capacity(capacity);
write!(result, "{sysname} {release} ({machine})").unwrap();
result
format!(
"{} {} ({})",
utsname.sysname().to_str().unwrap_or("Unknown"),
utsname.release().to_str().unwrap_or("Unknown"),
utsname.machine().to_str().unwrap_or("Unknown")
)
}
/// Gets the pretty name of the OS from `/etc/os-release`.
///
/// # Errors
///
/// Returns an error if `/etc/os-release` cannot be read.
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_os_pretty_name() -> Result<String, io::Error> {
// Fast byte-level scanning for PRETTY_NAME=
const PREFIX: &[u8] = b"PRETTY_NAME=";
let file = File::open("/etc/os-release")?;
let reader = BufReader::new(file);
let mut buffer = [0u8; 1024];
// Use fast syscall-based file reading
let bytes_read = read_file_fast("/etc/os-release", &mut buffer)?;
let content = &buffer[..bytes_read];
let mut offset = 0;
while offset < content.len() {
let remaining = &content[offset..];
// Find newline or end
let line_end = remaining
.iter()
.position(|&b| b == b'\n')
.unwrap_or(remaining.len());
let line = &remaining[..line_end];
if line.starts_with(PREFIX) {
let value = &line[PREFIX.len()..];
// Strip quotes if present
let trimmed = if value.len() >= 2
&& value[0] == b'"'
&& value[value.len() - 1] == b'"'
for line in reader.lines() {
let line = line?;
if let Some(pretty_name) = line.strip_prefix("PRETTY_NAME=") {
if let Some(trimmed) = pretty_name
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
{
&value[1..value.len() - 1]
} else {
value
};
// Convert to String - should be valid UTF-8
return Ok(String::from_utf8_lossy(trimmed).into_owned());
return Ok(trimmed.to_owned());
}
return Ok(pretty_name.to_owned());
}
offset += line_end + 1;
}
Ok("Unknown".to_owned())
}

View file

@ -1,203 +0,0 @@
//! Incredibly fast syscall wrappers for using inline assembly. Serves the
//! purposes of completely bypassing Rust's standard library in favor of
//! handwritten Assembly. Is this a good idea? No. Is it fast? Yeah, but only
//! marginally. Either way it serves a purpose and I will NOT accept criticism.
//! What do you mean I wasted two whole hours to make the program only 100µs
//! faster?
//!
//! Supports `x86_64` and `aarch64` architectures. Riscv support will be
//! implemented when and ONLY WHEN I can be bothered to work on it.
use std::io;
/// Direct syscall to open a file
/// Returns file descriptor or -1 on error
///
/// # Safety
///
/// The caller must ensure:
/// - `path` points to a valid null-terminated C string
/// - The pointer remains valid for the duration of the syscall
#[inline]
#[must_use]
pub unsafe fn sys_open(path: *const u8, flags: i32) -> i32 {
#[cfg(target_arch = "x86_64")]
unsafe {
let fd: i64;
std::arch::asm!(
"syscall",
in("rax") 2i64, // SYS_open
in("rdi") path,
in("rsi") flags,
in("rdx") 0i32, // mode (not used for reading)
lateout("rax") fd,
lateout("rcx") _,
lateout("r11") _,
options(nostack)
);
#[allow(clippy::cast_possible_truncation)]
{
fd as i32
}
}
#[cfg(target_arch = "aarch64")]
unsafe {
let fd: i64;
std::arch::asm!(
"svc #0",
in("x8") 56i64, // SYS_openat
in("x0") -100i32, // AT_FDCWD
in("x1") path,
in("x2") flags,
in("x3") 0i32, // mode
lateout("x0") fd,
options(nostack)
);
#[allow(clippy::cast_possible_truncation)]
{
fd as i32
}
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
{
compile_error!("Unsupported architecture for inline assembly syscalls");
}
}
/// Direct syscall to read from a file descriptor
/// Returns number of bytes read or -1 on error
///
/// # Safety
///
/// The caller must ensure:
/// - `buf` points to a valid writable buffer of at least `count` bytes
/// - `fd` is a valid open file descriptor
#[inline]
pub unsafe fn sys_read(fd: i32, buf: *mut u8, count: usize) -> isize {
#[cfg(target_arch = "x86_64")]
unsafe {
let ret: i64;
std::arch::asm!(
"syscall",
in("rax") 0i64, // SYS_read
in("rdi") fd,
in("rsi") buf,
in("rdx") count,
lateout("rax") ret,
lateout("rcx") _,
lateout("r11") _,
options(nostack)
);
#[allow(clippy::cast_possible_truncation)]
{
ret as isize
}
}
#[cfg(target_arch = "aarch64")]
unsafe {
let ret: i64;
std::arch::asm!(
"svc #0",
in("x8") 63i64, // SYS_read
in("x0") fd,
in("x1") buf,
in("x2") count,
lateout("x0") ret,
options(nostack)
);
#[allow(clippy::cast_possible_truncation)]
{
ret as isize
}
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
{
compile_error!("Unsupported architecture for inline assembly syscalls");
}
}
/// Direct syscall to close a file descriptor
///
/// # Safety
///
/// The caller must ensure `fd` is a valid open file descriptor
#[inline]
#[must_use]
pub unsafe fn sys_close(fd: i32) -> i32 {
#[cfg(target_arch = "x86_64")]
unsafe {
let ret: i64;
std::arch::asm!(
"syscall",
in("rax") 3i64, // SYS_close
in("rdi") fd,
lateout("rax") ret,
lateout("rcx") _,
lateout("r11") _,
options(nostack)
);
#[allow(clippy::cast_possible_truncation)]
{
ret as i32
}
}
#[cfg(target_arch = "aarch64")]
unsafe {
let ret: i64;
std::arch::asm!(
"svc #0",
in("x8") 57i64, // SYS_close
in("x0") fd,
lateout("x0") ret,
options(nostack)
);
#[allow(clippy::cast_possible_truncation)]
{
ret as i32
}
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
{
compile_error!("Unsupported architecture for inline assembly syscalls");
}
}
/// Read entire file using direct syscalls. This avoids libc overhead and can be
/// significantly faster for small files.
///
/// # Errors
///
/// Returns an error if the file cannot be opened or read
#[inline]
pub fn read_file_fast(path: &str, buffer: &mut [u8]) -> io::Result<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,31 +1,29 @@
use std::{env, fmt::Write as _, io, mem::MaybeUninit};
use std::{
env,
fs::File,
io::{self, Read},
};
use crate::{UtsName, colors::COLORS, syscall::read_file_fast};
use nix::sys::{statvfs::statvfs, utsname::UtsName};
use crate::colors::COLORS;
#[must_use]
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_username_and_hostname(utsname: &UtsName) -> String {
let username = env::var("USER").unwrap_or_else(|_| "unknown_user".to_owned());
let hostname = utsname.nodename().to_str().unwrap_or("unknown_host");
let capacity = COLORS.yellow.len()
+ username.len()
+ COLORS.red.len()
+ 1
+ COLORS.green.len()
+ hostname.len()
+ COLORS.reset.len();
let mut result = String::with_capacity(capacity);
result.push_str(COLORS.yellow);
result.push_str(&username);
result.push_str(COLORS.red);
result.push('@');
result.push_str(COLORS.green);
result.push_str(hostname);
result.push_str(COLORS.reset);
result
let hostname = utsname
.nodename()
.to_str()
.unwrap_or("unknown_host")
.to_owned();
format!(
"{yellow}{username}{red}@{green}{hostname}{reset}",
yellow = COLORS.yellow,
red = COLORS.red,
green = COLORS.green,
reset = COLORS.reset,
)
}
#[must_use]
@ -33,34 +31,16 @@ pub fn get_username_and_hostname(utsname: &UtsName) -> String {
pub fn get_shell() -> String {
let shell_path =
env::var("SHELL").unwrap_or_else(|_| "unknown_shell".to_owned());
// Find last '/' and get the part after it, avoiding allocation
shell_path
.rsplit('/')
.next()
.unwrap_or("unknown_shell")
.to_owned()
let shell_name = shell_path.rsplit('/').next().unwrap_or("unknown_shell");
shell_name.to_owned()
}
/// Gets the root disk usage information.
///
/// # Errors
///
/// Returns an error if the filesystem information cannot be retrieved.
#[cfg_attr(feature = "hotpath", hotpath::measure)]
#[allow(clippy::cast_precision_loss)]
pub fn get_root_disk_usage() -> Result<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 vfs = statvfs("/")?;
let block_size = vfs.block_size() as u64;
let total_blocks = vfs.blocks();
let available_blocks = vfs.blocks_available();
let total_size = block_size * total_blocks;
let used_size = total_size - (block_size * available_blocks);
@ -69,107 +49,53 @@ pub fn get_root_disk_usage() -> Result<String, io::Error> {
let used_size = used_size as f64 / (1024.0 * 1024.0 * 1024.0);
let usage = (used_size / total_size) * 100.0;
let mut result = String::with_capacity(64);
write!(
result,
Ok(format!(
"{used_size:.2} GiB / {total_size:.2} GiB ({cyan}{usage:.0}%{reset})",
cyan = COLORS.cyan,
reset = COLORS.reset,
)
.unwrap();
Ok(result)
))
}
/// Fast integer parsing without stdlib overhead
#[inline]
fn parse_u64_fast(s: &[u8]) -> u64 {
let mut result = 0u64;
for &byte in s {
if byte.is_ascii_digit() {
result = result * 10 + u64::from(byte - b'0');
} else {
break;
}
}
result
}
/// Gets the system memory usage information.
///
/// # Errors
///
/// Returns an error if `/proc/meminfo` cannot be read.
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_memory_usage() -> Result<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; 2048];
let mut total_memory_kb = 0.0;
let mut available_memory_kb = 0.0;
let mut meminfo = String::with_capacity(2048);
// Use fast syscall-based file reading
let bytes_read = read_file_fast("/proc/meminfo", &mut buffer)?;
let meminfo = &buffer[..bytes_read];
File::open("/proc/meminfo")?.read_to_string(&mut meminfo)?;
// Fast scanning for MemTotal and MemAvailable
let mut offset = 0;
let mut found_total = false;
let mut found_available = false;
while offset < meminfo.len() && (!found_total || !found_available) {
let remaining = &meminfo[offset..];
// Find newline or end
let line_end = remaining
.iter()
.position(|&b| b == b'\n')
.unwrap_or(remaining.len());
let line = &remaining[..line_end];
if line.starts_with(b"MemTotal:") {
// Skip "MemTotal:" and whitespace
let mut pos = 9;
while pos < line.len() && line[pos].is_ascii_whitespace() {
pos += 1;
}
total_memory_kb = parse_u64_fast(&line[pos..]);
found_total = true;
} else if line.starts_with(b"MemAvailable:") {
// Skip "MemAvailable:" and whitespace
let mut pos = 13;
while pos < line.len() && line[pos].is_ascii_whitespace() {
pos += 1;
}
available_memory_kb = parse_u64_fast(&line[pos..]);
found_available = true;
for line in meminfo.lines() {
let mut split = line.split_whitespace();
match split.next().unwrap_or_default() {
"MemTotal:" => {
total_memory_kb = split.next().unwrap_or("0").parse().unwrap_or(0.0)
},
"MemAvailable:" => {
available_memory_kb =
split.next().unwrap_or("0").parse().unwrap_or(0.0);
// MemTotal comes before MemAvailable, stop parsing
break;
},
_ => (),
}
offset += line_end + 1;
}
#[allow(clippy::cast_precision_loss)]
let total_memory_gb = total_memory_kb as f64 / 1024.0 / 1024.0;
#[allow(clippy::cast_precision_loss)]
let available_memory_gb = available_memory_kb as f64 / 1024.0 / 1024.0;
let total_memory_gb = total_memory_kb / 1024.0 / 1024.0;
let available_memory_gb = available_memory_kb / 1024.0 / 1024.0;
let used_memory_gb = total_memory_gb - available_memory_gb;
Ok((used_memory_gb, total_memory_gb))
}
let (used_memory, total_memory) = parse_memory_info()?;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let percentage_used = (used_memory / total_memory * 100.0).round() as u64;
let mut result = String::with_capacity(64);
write!(
result,
Ok(format!(
"{used_memory:.2} GiB / {total_memory:.2} GiB \
({cyan}{percentage_used}%{reset})",
cyan = COLORS.cyan,
reset = COLORS.reset,
)
.unwrap();
Ok(result)
))
}

View file

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