///|
pub fn build_command() -> @clap.Command {
@clap.Command(
"ffetch",
about="Print system information that looks like fastfetch output",
flags=[@clap.FlagArg("no-logo", about="Do not print a logo")],
options=[
@clap.OptionArg("model", about=model_about()),
@clap.OptionArg("user", short='u', about="user name"),
@clap.OptionArg("host", short='H', about="host name"),
@clap.OptionArg("os", short='o', about="OS"),
@clap.OptionArg("device", short='d', about="device model"),
@clap.OptionArg("kernel", short='k', about="kernel"),
@clap.OptionArg("uptime", short='t', about="uptime hours,mins, e.g. 3,12"),
@clap.OptionArg("shell", short='s', about="shell"),
@clap.OptionArg(
"resolution",
short='r',
about="resolution width,height, e.g. 3456,2234",
),
@clap.OptionArg("de", short='e', about="desktop environment"),
@clap.OptionArg("wm", short='w', about="window manager"),
@clap.OptionArg("wm-theme", about="WM theme"),
@clap.OptionArg("terminal", short='m', about="terminal"),
@clap.OptionArg("term-font", about="terminal font"),
@clap.OptionArg(
"cpu",
short='c',
about="CPU name,cores,clock GHz, e.g. \"Apple M3 Max,16,4.05\"",
),
@clap.OptionArg(
"gpu",
short='g',
about="GPU name[,vram GB], e.g. \"Apple M3 Max\" or \"RTX 4090,24\"",
),
@clap.OptionArg(
"memory",
short='M',
about="memory used,total in GiB, e.g. 46.3,128",
),
@clap.OptionArg(
"logo",
short='l',
about="logo name: macos, linux, arch, ubuntu, debian, fedora, nixos, windows, alpine, android, artix, deepin, endeavouros, gentoo, manjaro, mx, opensuse, pop, raspbian, rocky, slackware, suse, termux, none",
),
@clap.OptionArg(
"color",
short='C',
about="logo color: auto (use logo palette) or black, red, green, yellow, blue, magenta, cyan, white, none",
default_values=["auto"],
),
],
)
}
///|
/// Value provided by the user on the command line, if any.
/// Distinct from the model defaults because the argparse default_values
/// are unset for field options.
fn override_value(matches : @clap.Matches, name : String) -> String? {
matches.values.get(name).bind(arr => arr.get(0))
}
///|
fn ansi_code(color : String) -> String {
match color {
"black" => "30"
"red" => "31"
"green" => "32"
"yellow" => "33"
"blue" => "34"
"magenta" => "35"
"cyan" => "36"
"white" => "37"
_ => ""
}
}
///|
/// Render the fake system-info output for the parsed command line.
pub fn render(matches : @clap.Matches) -> String {
let model = override_value(matches, "model")
.bind(model_by_name)
.unwrap_or(default_model())
let info = render_info(matches, model)
let no_logo = matches.flags.get("no-logo").unwrap_or(false)
let logo = {
guard !no_logo else { None }
logo_by_name(override_value(matches, "logo").unwrap_or(model.logo))
}
match logo {
None => render_info_only(info)
Some(logo) =>
render_with_logo(
info,
decode_logo(logo, override_value(matches, "color").unwrap_or("")),
)
}
}
///|
fn render_info(matches : @clap.Matches, model : DeviceModel) -> Array[String] {
let user = override_value(matches, "user").unwrap_or("kokic")
let host = override_value(matches, "host").unwrap_or(model.host)
let header = "\{user}@\{host}"
let dashline = "-".repeat(header.char_length())
[
header,
dashline,
..field_line("OS", override_value(matches, "os").unwrap_or(model.os)),
..field_line(
"Host",
override_value(matches, "device").unwrap_or(model.device),
),
..field_line(
"Kernel",
override_value(matches, "kernel").unwrap_or(model.kernel),
),
..field_line(
"Uptime",
structured_field(matches, "uptime", model.uptime.render(), s => {
parse_uptime(s).map(u => u.render())
}),
),
..field_line(
"Shell",
override_value(matches, "shell").unwrap_or(model.shell),
),
..field_line(
"Resolution",
structured_field(matches, "resolution", model.resolution.render(), s => {
parse_resolution(s).map(r => r.render())
}),
),
..field_line("DE", override_value(matches, "de").unwrap_or(model.de)),
..field_line("WM", override_value(matches, "wm").unwrap_or(model.wm)),
..field_line(
"WM Theme",
override_value(matches, "wm-theme").unwrap_or(model.wm_theme),
),
..field_line(
"Terminal",
override_value(matches, "terminal").unwrap_or(model.terminal),
),
..field_line(
"Terminal Font",
override_value(matches, "term-font").unwrap_or(model.term_font),
),
..field_line(
"CPU",
structured_field(matches, "cpu", model.cpu.render(), s => {
parse_cpu(s).map(c => c.render())
}),
),
..field_line(
"GPU",
structured_field(matches, "gpu", model.gpu.render(), s => {
parse_gpu(s).map(g => g.render())
}),
),
..field_line(
"Memory",
structured_field(matches, "memory", model.memory.render(), s => {
parse_memory(s).map(m => m.render())
}),
),
]
}
///|
/// The rendered value of a structured field: an explicit override when it
/// parses, otherwise the model default. An empty override hides the line,
/// like other fields.
fn structured_field(
matches : @clap.Matches,
name : String,
default : String,
parse : (String) -> String?,
) -> String {
match override_value(matches, name) {
Some("") => ""
Some(s) => parse(s).unwrap_or(default)
None => default
}
}
///|
fn parse_int_part(s : StringView) -> Int? {
Some(@string.parse_int(s)) catch {
_ => None
}
}
///|
fn parse_float_part(s : StringView) -> Float? {
Some(Float::from_double(@string.parse_double(s))) catch {
_ => None
}
}
///|
/// Parse a `used,total` pair in GiB, e.g. `"46.3,128"`.
fn parse_memory(s : String) -> Memory? {
let parts = s.split(",").to_array()
if parts.length() != 2 {
return None
}
guard parse_float_part(parts[0]) is Some(used) else { return None }
guard parse_float_part(parts[1]) is Some(total) else { return None }
Some({ used, total })
}
///|
/// Parse a `width,height` pair, e.g. `"3456,2234"`.
fn parse_resolution(s : String) -> Resolution? {
let parts = s.split(",").to_array()
if parts.length() != 2 {
return None
}
guard parse_int_part(parts[0]) is Some(width) else { return None }
guard parse_int_part(parts[1]) is Some(height) else { return None }
Some({ width, height })
}
///|
/// Parse an `hours,mins` pair, e.g. `"3,12"`.
fn parse_uptime(s : String) -> Uptime? {
let parts = s.split(",").to_array()
if parts.length() != 2 {
return None
}
guard parse_int_part(parts[0]) is Some(hours) else { return None }
guard parse_int_part(parts[1]) is Some(mins) else { return None }
Some({ hours, mins })
}
///|
/// Parse `name,cores,clock` in GHz, e.g. `"Apple M3 Max,16,4.05"`.
fn parse_cpu(s : String) -> Cpu? {
let parts = s.split(",").to_array()
if parts.length() != 3 {
return None
}
guard parse_int_part(parts[1]) is Some(cores) else { return None }
guard parse_float_part(parts[2]) is Some(clock) else { return None }
Some({ name: parts[0].to_owned(), cores, clock })
}
///|
/// Parse `name` or `name,vram` in GB, e.g. `"Apple M3 Max"` or `"RTX 4090,24"`.
fn parse_gpu(s : String) -> Gpu? {
let parts = s.split(",").to_array()
match parts.length() {
1 => Some({ name: parts[0].to_owned(), vram: 0 })
2 => {
guard parse_int_part(parts[1]) is Some(vram) else { return None }
Some({ name: parts[0].to_owned(), vram })
}
_ => None
}
}
///|
fn render_info_only(info : Array[String]) -> String {
let buf = StringBuilder()
for line in info {
buf.write_string(line)
buf.write_string("\n")
}
buf.to_string()
}
///|
fn render_with_logo(info : Array[String], rendered : DecodedLogo) -> String {
let buf = StringBuilder()
let width = rendered.max_width
let rows = rendered.lines.length().max(info.length())
for i in 0.. Array[String] {
guard !value.is_empty() else { [] }
["\{label}: \{value}"]
}
///|
priv struct DecodedLogo {
lines : Array[(String, Int)]
max_width : Int
}
///|
/// The palette entry used to color a logo cell: the forced color wins,
/// otherwise the palette color at `index`, unless colors are disabled.
fn resolve_code(
palette : Array[String],
forced_code : String,
no_color : Bool,
index : Int,
) -> String {
if no_color {
""
} else if !forced_code.is_empty() {
forced_code
} else {
palette.get(index).unwrap_or("")
}
}
///|
fn decode_logo(logo : Logo, color_mode : String) -> DecodedLogo {
let no_color = color_mode == "none"
let forced_code = if color_mode == "auto" || color_mode == "none" {
""
} else {
ansi_code(color_mode)
}
let mut carry = resolve_code(logo.palette, forced_code, no_color, 0)
let lines : Array[(String, Int)] = []
let mut max_width = 0
for line in logo.lines {
let decoded = decode_logo_line(
line,
logo.palette,
forced_code,
carry,
no_color,
)
carry = decoded.carry
lines.push((decoded.text, decoded.width))
max_width = max_width.max(decoded.width)
}
{ lines, max_width }
}
///|
priv struct DecodedLogoLine {
text : String
width : Int
carry : String
}
///|
fn write_ansi(buf : StringBuilder, code : String) -> Unit {
buf.write_string("\u{1B}[")
buf.write_string(code)
buf.write_string("m")
}
///|
fn decode_logo_line(
line : String,
palette : Array[String],
forced_code : String,
start_carry : String,
no_color : Bool,
) -> DecodedLogoLine {
let buf = StringBuilder()
let mut carry = start_carry
let mut width = 0
let mut index = 0
if !carry.is_empty() {
write_ansi(buf, carry)
}
let chars = line.to_array()
while index < chars.length() {
let c = chars[index]
if c == '$' && index + 1 < chars.length() {
let n = chars[index + 1]
if n == '$' {
buf.write_char('$')
width += 1
index += 2
continue
}
if n >= '1' && n <= '9' {
let code = resolve_code(
palette,
forced_code,
no_color,
n.to_int() - '1'.to_int(),
)
carry = code
if !code.is_empty() {
write_ansi(buf, code)
}
index += 2
continue
}
}
buf.write_char(c)
width += 1
index += 1
}
if !carry.is_empty() {
buf.write_string("\u{1B}[0m")
}
{ text: buf.to_string(), width, carry }
}