mirror of
https://github.com/NotAShelf/microfetch.git
synced 2026-04-12 21:07:41 +00:00
treewide: break into multiple crates
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ifabe7af523ece1354b301b1a321659ee6a6a6964
This commit is contained in:
parent
928452faba
commit
1408ca9f38
16 changed files with 348 additions and 394 deletions
12
crates/asm/Cargo.toml
Normal file
12
crates/asm/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "microfetch-asm"
|
||||
description = "Inline assembly syscall helpers for microfetch"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
rust-version.workspace = true
|
||||
publish = false
|
||||
license.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
429
crates/asm/src/lib.rs
Normal file
429
crates/asm/src/lib.rs
Normal file
|
|
@ -0,0 +1,429 @@
|
|||
//! 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 that:
|
||||
///
|
||||
/// - `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 write to a file descriptor
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Number of bytes written or -1 on error
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that:
|
||||
///
|
||||
/// - `buf` points to a valid readable buffer of at least `count` bytes
|
||||
/// - `fd` is a valid open file descriptor
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub unsafe fn sys_write(fd: i32, buf: *const u8, count: usize) -> isize {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
unsafe {
|
||||
let ret: i64;
|
||||
std::arch::asm!(
|
||||
"syscall",
|
||||
in("rax") 1i64, // SYS_write
|
||||
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") 64i64, // SYS_write
|
||||
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 that `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");
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw buffer for the `uname(2)` syscall.
|
||||
///
|
||||
/// Linux ABI hasfive fields of `[i8; 65]`: sysname, nodename, release, version,
|
||||
/// machine. The `domainname` field (GNU extension, `[i8; 65]`) follows but is
|
||||
/// not used, nor any useful to us here.
|
||||
#[repr(C)]
|
||||
#[allow(dead_code)]
|
||||
pub struct UtsNameBuf {
|
||||
pub sysname: [i8; 65],
|
||||
pub nodename: [i8; 65],
|
||||
pub release: [i8; 65],
|
||||
pub version: [i8; 65],
|
||||
pub machine: [i8; 65],
|
||||
pub domainname: [i8; 65], // GNU extension, included for correct struct size
|
||||
}
|
||||
|
||||
/// Direct `uname(2)` syscall
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// 0 on success, negative on error
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that `buf` points to a valid `UtsNameBuf`.
|
||||
#[inline]
|
||||
#[allow(dead_code)]
|
||||
pub unsafe fn sys_uname(buf: *mut UtsNameBuf) -> i32 {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
unsafe {
|
||||
let ret: i64;
|
||||
std::arch::asm!(
|
||||
"syscall",
|
||||
in("rax") 63i64, // SYS_uname
|
||||
in("rdi") buf,
|
||||
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") 160i64, // SYS_uname
|
||||
in("x0") buf,
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw buffer for the `statfs(2)` syscall.
|
||||
///
|
||||
/// Linux ABI (`x86_64` and `aarch64`): the fields we use are at the same
|
||||
/// offsets on both architectures. Only the fields needed for disk usage are
|
||||
/// declared; the remainder of the 120-byte struct is covered by `_pad`.
|
||||
#[repr(C)]
|
||||
pub struct StatfsBuf {
|
||||
pub f_type: i64,
|
||||
pub f_bsize: i64,
|
||||
pub f_blocks: u64,
|
||||
pub f_bfree: u64,
|
||||
pub f_bavail: u64,
|
||||
pub f_files: u64,
|
||||
pub f_ffree: u64,
|
||||
pub f_fsid: [i32; 2],
|
||||
pub f_namelen: i64,
|
||||
pub f_frsize: i64,
|
||||
pub f_flags: i64,
|
||||
|
||||
#[allow(clippy::pub_underscore_fields, reason = "This is not a public API")]
|
||||
pub _pad: [i64; 4],
|
||||
}
|
||||
|
||||
/// Direct `statfs(2)` syscall
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// 0 on success, negative errno on error
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that:
|
||||
///
|
||||
/// - `path` points to a valid null-terminated string
|
||||
/// - `buf` points to a valid `StatfsBuf`
|
||||
#[inline]
|
||||
pub unsafe fn sys_statfs(path: *const u8, buf: *mut StatfsBuf) -> i32 {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
unsafe {
|
||||
let ret: i64;
|
||||
std::arch::asm!(
|
||||
"syscall",
|
||||
in("rax") 137i64, // SYS_statfs
|
||||
in("rdi") path,
|
||||
in("rsi") buf,
|
||||
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") 43i64, // SYS_statfs
|
||||
in("x0") path,
|
||||
in("x1") buf,
|
||||
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;
|
||||
|
||||
// We use stack-allocated buffer for null-terminated path. The maximum
|
||||
// is 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
28
crates/lib/Cargo.toml
Normal file
28
crates/lib/Cargo.toml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "microfetch-lib"
|
||||
description = "Microfetch library crate - exports all functionality including main function"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
hotpath = { optional = true, version = "0.13.0" }
|
||||
microfetch-asm.workspace = true
|
||||
|
||||
[features]
|
||||
hotpath = [ "dep:hotpath", "hotpath/hotpath" ]
|
||||
hotpath-alloc = [ "hotpath/hotpath-alloc" ]
|
||||
hotpath-off = [ "hotpath/hotpath-off" ]
|
||||
|
||||
[dev-dependencies]
|
||||
criterion.workspace = true
|
||||
|
||||
[[bench]]
|
||||
harness = false
|
||||
name = "benchmark"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
33
crates/lib/benches/benchmark.rs
Normal file
33
crates/lib/benches/benchmark.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
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,
|
||||
};
|
||||
|
||||
fn main_benchmark(c: &mut Criterion) {
|
||||
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));
|
||||
}
|
||||
|
||||
criterion_group!(benches, main_benchmark);
|
||||
criterion_main!(benches);
|
||||
81
crates/lib/src/colors.rs
Normal file
81
crates/lib/src/colors.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
use std::sync::LazyLock;
|
||||
|
||||
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(|| {
|
||||
// Only presence matters; value is irrelevant per the NO_COLOR spec
|
||||
let is_no_color = std::env::var_os("NO_COLOR").is_some();
|
||||
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
|
||||
}
|
||||
37
crates/lib/src/desktop.rs
Normal file
37
crates/lib/src/desktop.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
use std::{ffi::OsStr, fmt::Write};
|
||||
|
||||
#[must_use]
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure)]
|
||||
pub fn get_desktop_info() -> String {
|
||||
let desktop_os = std::env::var_os("XDG_CURRENT_DESKTOP");
|
||||
let session_os = std::env::var_os("XDG_SESSION_TYPE");
|
||||
|
||||
let desktop_raw = desktop_os
|
||||
.as_deref()
|
||||
.and_then(OsStr::to_str)
|
||||
.unwrap_or("Unknown");
|
||||
let desktop_str = desktop_raw.strip_prefix("none+").unwrap_or(desktop_raw);
|
||||
|
||||
let session_raw = session_os.as_deref().and_then(OsStr::to_str).unwrap_or("");
|
||||
let backend_str = if session_raw.is_empty() {
|
||||
"Unknown"
|
||||
} else {
|
||||
session_raw
|
||||
};
|
||||
|
||||
// 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
|
||||
}
|
||||
160
crates/lib/src/lib.rs
Normal file
160
crates/lib/src/lib.rs
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
pub mod colors;
|
||||
pub mod desktop;
|
||||
pub mod release;
|
||||
pub mod system;
|
||||
pub mod uptime;
|
||||
|
||||
use std::{
|
||||
ffi::CStr,
|
||||
io::{self, Cursor, Write},
|
||||
mem::MaybeUninit,
|
||||
};
|
||||
|
||||
pub use microfetch_asm as syscall;
|
||||
pub use microfetch_asm::{
|
||||
StatfsBuf,
|
||||
UtsNameBuf,
|
||||
read_file_fast,
|
||||
sys_close,
|
||||
sys_open,
|
||||
sys_read,
|
||||
sys_statfs,
|
||||
sys_uname,
|
||||
sys_write,
|
||||
};
|
||||
|
||||
/// Wrapper for `utsname` with safe accessor methods
|
||||
pub struct UtsName(UtsNameBuf);
|
||||
|
||||
impl UtsName {
|
||||
/// Calls `uname(2)` 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 { sys_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) -> &CStr {
|
||||
unsafe { CStr::from_ptr(self.0.nodename.as_ptr().cast()) }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn sysname(&self) -> &CStr {
|
||||
unsafe { CStr::from_ptr(self.0.sysname.as_ptr().cast()) }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn release(&self) -> &CStr {
|
||||
unsafe { CStr::from_ptr(self.0.release.as_ptr().cast()) }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn machine(&self) -> &CStr {
|
||||
unsafe { CStr::from_ptr(self.0.machine.as_ptr().cast()) }
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure)]
|
||||
fn print_system_info(
|
||||
fields: &Fields,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let Fields {
|
||||
user_info,
|
||||
os_name,
|
||||
kernel_version,
|
||||
shell,
|
||||
uptime,
|
||||
desktop,
|
||||
memory_usage,
|
||||
storage,
|
||||
colors,
|
||||
} = fields;
|
||||
|
||||
let cyan = colors::COLORS.cyan;
|
||||
let blue = colors::COLORS.blue;
|
||||
let reset = colors::COLORS.reset;
|
||||
|
||||
let mut buf = [0u8; 2048];
|
||||
let mut cursor = Cursor::new(&mut buf[..]);
|
||||
|
||||
write!(
|
||||
cursor,
|
||||
"
|
||||
{blue} ▟█▖ {cyan}▝█▙ ▗█▛ {user_info} ~{reset}
|
||||
{blue} ▗▄▄▟██▄▄▄▄▄{cyan}▝█▙█▛ {blue}▖ {cyan} {blue}System{reset} {os_name}
|
||||
{blue} ▀▀▀▀▀▀▀▀▀▀▀▘{cyan}▝██ {blue}▟█▖ {cyan} {blue}Kernel{reset} {kernel_version}
|
||||
{cyan} ▟█▛ {cyan}▝█▘{blue}▟█▛ {cyan} {blue}Shell{reset} {shell}
|
||||
{cyan}▟█████▛ {blue}▟█████▛ {cyan} {blue}Uptime{reset} {uptime}
|
||||
{cyan} ▟█▛{blue}▗█▖ {blue}▟█▛ {cyan} {blue}Desktop{reset} {desktop}
|
||||
{cyan} ▝█▛ {blue}██▖{cyan}▗▄▄▄▄▄▄▄▄▄▄▄ {cyan} {blue}Memory{reset} {memory_usage}
|
||||
{cyan} ▝ {blue}▟█▜█▖{cyan}▀▀▀▀▀██▛▀▀▘ {cyan} {blue}Storage (/){reset} {storage}
|
||||
{blue} ▟█▘ ▜█▖ {cyan}▝█▛ {cyan} {blue}Colors{reset} {colors}\n\n"
|
||||
)?;
|
||||
|
||||
let len =
|
||||
usize::try_from(cursor.position()).expect("cursor position fits usize");
|
||||
// Direct syscall to avoid stdout buffering allocation
|
||||
let written = unsafe { sys_write(1, buf.as_ptr(), len) };
|
||||
if written < 0 {
|
||||
return Err(io::Error::last_os_error().into());
|
||||
}
|
||||
#[allow(clippy::cast_sign_loss)] // non-negative verified by the guard above
|
||||
if written as usize != len {
|
||||
return Err(
|
||||
io::Error::new(io::ErrorKind::WriteZero, "partial write to stdout")
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Main entry point for microfetch - can be called by the binary crate
|
||||
/// or by other consumers of the library
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if any system call fails
|
||||
#[cfg_attr(feature = "hotpath", hotpath::main)]
|
||||
pub fn run() -> 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: system::get_username_and_hostname(&utsname),
|
||||
os_name: release::get_os_pretty_name()?,
|
||||
kernel_version: release::get_system_info(&utsname),
|
||||
shell: system::get_shell(),
|
||||
desktop: desktop::get_desktop_info(),
|
||||
uptime: uptime::get_current()?,
|
||||
memory_usage: system::get_memory_usage()?,
|
||||
storage: system::get_root_disk_usage()?,
|
||||
colors: colors::print_dots(),
|
||||
};
|
||||
print_system_info(&fields)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
69
crates/lib/src/release.rs
Normal file
69
crates/lib/src/release.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
use std::{fmt::Write as _, io};
|
||||
|
||||
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> {
|
||||
// Fast byte-level scanning for PRETTY_NAME=
|
||||
const PREFIX: &[u8] = b"PRETTY_NAME=";
|
||||
|
||||
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'"'
|
||||
{
|
||||
&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())
|
||||
}
|
||||
183
crates/lib/src/system.rs
Normal file
183
crates/lib/src/system.rs
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
use std::{ffi::OsStr, fmt::Write as _, io, mem::MaybeUninit};
|
||||
|
||||
use crate::{
|
||||
UtsName,
|
||||
colors::COLORS,
|
||||
syscall::{StatfsBuf, read_file_fast, sys_statfs},
|
||||
};
|
||||
|
||||
#[must_use]
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure)]
|
||||
pub fn get_username_and_hostname(utsname: &UtsName) -> String {
|
||||
let username_os = std::env::var_os("USER");
|
||||
let username = username_os
|
||||
.as_deref()
|
||||
.and_then(OsStr::to_str)
|
||||
.unwrap_or("unknown_user");
|
||||
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
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure)]
|
||||
pub fn get_shell() -> String {
|
||||
let shell_os = std::env::var_os("SHELL");
|
||||
let shell = shell_os.as_deref().and_then(OsStr::to_str).unwrap_or("");
|
||||
let start = shell.rfind('/').map_or(0, |i| i + 1);
|
||||
if shell.is_empty() {
|
||||
"unknown_shell".into()
|
||||
} else {
|
||||
shell[start..].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::<StatfsBuf>::uninit();
|
||||
let path = b"/\0";
|
||||
|
||||
if unsafe { sys_statfs(path.as_ptr(), vfs.as_mut_ptr()) } != 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
let vfs = unsafe { vfs.assume_init() };
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
let block_size = vfs.f_bsize as u64;
|
||||
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_gb = total_memory_kb as f64 / 1024.0 / 1024.0;
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let available_gb = available_memory_kb as f64 / 1024.0 / 1024.0;
|
||||
let used_memory_gb = total_gb - available_gb;
|
||||
|
||||
Ok((used_memory_gb, total_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)
|
||||
}
|
||||
141
crates/lib/src/uptime.rs
Normal file
141
crates/lib/src/uptime.rs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
use std::{io, mem::MaybeUninit};
|
||||
|
||||
/// 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 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..]) }
|
||||
}
|
||||
|
||||
/// Raw buffer for the `sysinfo(2)` syscall.
|
||||
///
|
||||
/// In the Linux ABI `uptime` is a `long` at offset 0. The remaining fields are
|
||||
/// not needed, but are declared to give the struct its correct size (112 bytes
|
||||
/// on 64-bit Linux).
|
||||
///
|
||||
/// The layout matches the kernel's `struct sysinfo` *exactly*:
|
||||
/// `mem_unit` ends at offset 108, then 4 bytes of implicit padding to 112.
|
||||
#[repr(C)]
|
||||
struct SysInfo {
|
||||
uptime: i64,
|
||||
loads: [u64; 3],
|
||||
totalram: u64,
|
||||
freeram: u64,
|
||||
sharedram: u64,
|
||||
bufferram: u64,
|
||||
totalswap: u64,
|
||||
freeswap: u64,
|
||||
procs: u16,
|
||||
_pad: u16,
|
||||
_pad2: u32, // alignment padding to reach 8-byte boundary for totalhigh
|
||||
totalhigh: u64,
|
||||
freehigh: u64,
|
||||
mem_unit: u32,
|
||||
// 4 bytes implicit trailing padding to reach 112 bytes total; no field
|
||||
// needed
|
||||
}
|
||||
|
||||
/// Direct `sysinfo(2)` syscall using inline assembly
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure the sysinfo pointer is valid.
|
||||
#[inline]
|
||||
unsafe fn sys_sysinfo(info: *mut 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")))]
|
||||
{
|
||||
compile_error!("Unsupported architecture for inline assembly syscalls");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue