You've already forked neo64fetch
mirror of
https://github.com/neoarz/neo64fetch.git
synced 2026-02-08 22:33:26 +01:00
feat: image support
This commit is contained in:
938
Cargo.lock
generated
938
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -6,9 +6,11 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
colored = "3.0.0"
|
colored = "3.0.0"
|
||||||
display-info = "0.5.7"
|
display-info = "0.5.7"
|
||||||
|
image = "0.25.9"
|
||||||
libc = "0.2.179"
|
libc = "0.2.179"
|
||||||
objc2 = "0.6.3"
|
objc2 = "0.6.3"
|
||||||
objc2-app-kit = "0.3.2"
|
objc2-app-kit = "0.3.2"
|
||||||
objc2-foundation = "0.3.2"
|
objc2-foundation = "0.3.2"
|
||||||
plist = "1.8.0"
|
plist = "1.8.0"
|
||||||
sysinfo = "0.37.2"
|
sysinfo = "0.37.2"
|
||||||
|
base64 = "0.22.1"
|
||||||
|
|||||||
BIN
assets/logo.png
Normal file
BIN
assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
@@ -45,7 +45,6 @@ fn model_to_name(model: &str) -> Option<String> {
|
|||||||
"15,10" => "MacBook Pro (14-inch, M3 Max, 2023)",
|
"15,10" => "MacBook Pro (14-inch, M3 Max, 2023)",
|
||||||
"15,8" => "MacBook Pro (14-inch, M3 Pro, 2023)",
|
"15,8" => "MacBook Pro (14-inch, M3 Pro, 2023)",
|
||||||
"15,6" | "15,7" => "MacBook Pro (16-inch, M2 Max, 2023)",
|
"15,6" | "15,7" => "MacBook Pro (16-inch, M2 Max, 2023)",
|
||||||
"15,4" | "15,5" => "MacBook Pro (16-inch, M2 Pro, 2023)",
|
|
||||||
"15,3" => "MacBook Pro (14-inch, M2 Max, 2023)",
|
"15,3" => "MacBook Pro (14-inch, M2 Max, 2023)",
|
||||||
"14,10" | "14,6" => "MacBook Pro (16-inch, M2 Pro/Max, 2023)",
|
"14,10" | "14,6" => "MacBook Pro (16-inch, M2 Pro/Max, 2023)",
|
||||||
"14,9" | "14,5" => "MacBook Pro (14-inch, M2 Pro/Max, 2023)",
|
"14,9" | "14,5" => "MacBook Pro (14-inch, M2 Pro/Max, 2023)",
|
||||||
|
|||||||
85
src/main.rs
85
src/main.rs
@@ -7,7 +7,7 @@ use sysinfo::System;
|
|||||||
mod helpers;
|
mod helpers;
|
||||||
mod output;
|
mod output;
|
||||||
|
|
||||||
use output::colors;
|
use output::{colors, image};
|
||||||
|
|
||||||
struct Stats {
|
struct Stats {
|
||||||
// Neoarz[at]Mac
|
// Neoarz[at]Mac
|
||||||
@@ -38,14 +38,15 @@ struct Stats {
|
|||||||
locale: String,
|
locale: String,
|
||||||
|
|
||||||
// Extra fields
|
// Extra fields
|
||||||
architecture: String,
|
architecture: String, // appended to os
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
|
||||||
|
fn get_system_stats() -> Stats {
|
||||||
let mut sys = System::new_all();
|
let mut sys = System::new_all();
|
||||||
sys.refresh_all();
|
sys.refresh_all();
|
||||||
|
|
||||||
let stats = Stats {
|
Stats {
|
||||||
hostname: System::host_name().unwrap_or_else(|| "<unknown>".to_owned()),
|
hostname: System::host_name().unwrap_or_else(|| "<unknown>".to_owned()),
|
||||||
// This would be the real username of the system but I just want to print out Neoarz for my case
|
// This would be the real username of the system but I just want to print out Neoarz for my case
|
||||||
// Uncoment the line below to use the real username
|
// Uncoment the line below to use the real username
|
||||||
@@ -78,40 +79,60 @@ fn main() {
|
|||||||
ip: helpers::ip::get_ip_info(),
|
ip: helpers::ip::get_ip_info(),
|
||||||
battery: helpers::battery::get_battery_info(),
|
battery: helpers::battery::get_battery_info(),
|
||||||
locale: helpers::locale::get_locale_info(),
|
locale: helpers::locale::get_locale_info(),
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn print_stats(stats: &Stats, offset: usize) {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
|
||||||
// user@host
|
// user@host
|
||||||
println!("{}", colors::title(&stats.username, &stats.hostname));
|
lines.push(colors::title(&stats.username, &stats.hostname));
|
||||||
|
|
||||||
// separator
|
// separator
|
||||||
println!("{}", colors::separator(stats.username.len() + stats.hostname.len() + 1));
|
lines.push(colors::separator(stats.username.len() + stats.hostname.len() + 1));
|
||||||
|
|
||||||
// info
|
// info
|
||||||
println!("{}", colors::info("OS", &format!("{} {}", stats.os, stats.architecture)));
|
lines.push(colors::info("OS", &format!("{} {}", stats.os, stats.architecture)));
|
||||||
println!("{}", colors::info("Host", &stats.host));
|
lines.push(colors::info("Host", &stats.host));
|
||||||
println!("{}", colors::info("Kernel", &stats.kernel));
|
lines.push(colors::info("Kernel", &stats.kernel));
|
||||||
println!("{}", colors::info("Uptime", &stats.uptime));
|
lines.push(colors::info("Uptime", &stats.uptime));
|
||||||
println!("{}", colors::info("Packages", &stats.packages));
|
lines.push(colors::info("Packages", &stats.packages));
|
||||||
println!("{}", colors::info("Shell", &stats.shell));
|
lines.push(colors::info("Shell", &stats.shell));
|
||||||
println!("{}", colors::info("Display", &stats.display));
|
lines.push(colors::info("Display", &stats.display));
|
||||||
println!("{}", colors::info("DE", &stats.desktop_env));
|
// lines.push(colors::info("DE", &stats.desktop_env));
|
||||||
println!("{}", colors::info("WM", &stats.window_manager));
|
// lines.push(colors::info("WM", &stats.window_manager));
|
||||||
println!("{}", colors::info("WM Theme", &stats.window_manager_theme));
|
// lines.push(colors::info("WM Theme", &stats.window_manager_theme));
|
||||||
println!("{}", colors::info("Font", &stats.font));
|
lines.push(colors::info("Font", &stats.font));
|
||||||
println!("{}", colors::info("Cursor", &stats.cursor));
|
lines.push(colors::info("Cursor", &stats.cursor));
|
||||||
println!("{}", colors::info("Terminal", &stats.terminal));
|
lines.push(colors::info("Terminal", &stats.terminal));
|
||||||
println!("{}", colors::info("Terminal Font", &stats.terminal_font));
|
lines.push(colors::info("Terminal Font", &stats.terminal_font));
|
||||||
println!("{}", colors::info("CPU", &stats.cpu));
|
lines.push(colors::info("CPU", &stats.cpu));
|
||||||
println!("{}", colors::info("GPU", &stats.gpu));
|
lines.push(colors::info("GPU", &stats.gpu));
|
||||||
println!("{}", colors::info("Memory", &stats.memory));
|
lines.push(colors::info("Memory", &stats.memory));
|
||||||
println!("{}", colors::info("Swap", &stats.swap));
|
lines.push(colors::info("Swap", &stats.swap));
|
||||||
println!("{}", colors::info("Disk (/)", &stats.storage));
|
lines.push(colors::info("Disk (/)", &stats.storage));
|
||||||
// Don't wanna show print this lolol
|
// lines.push(colors::info("Local IP", &stats.ip));
|
||||||
// println!("{}", colors::info("Local IP", &stats.ip));
|
lines.push(colors::info(&format!("Battery {}", stats.battery.0), &stats.battery.1));
|
||||||
println!("{}", colors::info(&format!("Battery {}", stats.battery.0), &stats.battery.1));
|
// lines.push(colors::info("Locale", &stats.locale));
|
||||||
println!("{}", colors::info("Locale", &stats.locale));
|
|
||||||
|
|
||||||
// color blocks
|
// color blocks
|
||||||
println!();
|
lines.push(String::new());
|
||||||
println!("{}", colors::color_blocks());
|
for line in colors::color_blocks().lines() {
|
||||||
|
lines.push(line.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
for line in lines {
|
||||||
|
image::offset_println!(offset, "{}", line);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let stats = get_system_stats();
|
||||||
|
let (offset, img_rows) = image::print_image_and_setup("assets/logo.png", 700);
|
||||||
|
// ^^^ size of the image change it here
|
||||||
|
print_stats(&stats, offset);
|
||||||
|
image::finish_printing(offset, 24, img_rows);
|
||||||
|
}
|
||||||
203
src/output/image.rs
Normal file
203
src/output/image.rs
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
// Kitty Graphics Protocol implementation for terminal image display
|
||||||
|
//
|
||||||
|
// Inspired ~~stolen~~ by swiftfetch's implementation:
|
||||||
|
// https://github.com/Ly-sec/swiftfetch/blob/main/src/display.rs
|
||||||
|
//
|
||||||
|
// Images are base64-encoded and chunked copying what swiftfetch does
|
||||||
|
// Compatible terminals: Any terminals which use Kitty protocol, like Ghostty, Kitty, Wezterm
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
|
||||||
|
use image::{GenericImageView, ImageFormat};
|
||||||
|
use libc::{ioctl, winsize, STDOUT_FILENO, TIOCGWINSZ};
|
||||||
|
use std::env;
|
||||||
|
use std::io::{Cursor, Write};
|
||||||
|
use std::mem;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
// Terminal cell metrics fallback values (pixels per character cell)
|
||||||
|
// See: https://github.com/Ly-sec/swiftfetch/blob/main/src/display.rs#L417-L418
|
||||||
|
const DEFAULT_CHAR_WIDTH: f32 = 10.0;
|
||||||
|
const DEFAULT_CHAR_HEIGHT: f32 = 20.0;
|
||||||
|
|
||||||
|
// Horizontal spacing between image and text (in terminal columns)
|
||||||
|
// See: https://github.com/Ly-sec/swiftfetch/blob/main/src/display.rs#L419
|
||||||
|
const DEFAULT_GAP_COLUMNS: usize = 2;
|
||||||
|
|
||||||
|
// Maximum bytes per Kitty protocol chunk
|
||||||
|
// See: https://github.com/Ly-sec/swiftfetch/blob/main/src/display.rs#L557
|
||||||
|
const CHUNK_SIZE: usize = 4096;
|
||||||
|
|
||||||
|
// Detects Kitty Graphics Protocol support via environment variables.
|
||||||
|
//
|
||||||
|
// See: https://github.com/Ly-sec/swiftfetch/blob/main/src/display.rs#L422-L460
|
||||||
|
//
|
||||||
|
// Returns false for unsupported terminals
|
||||||
|
// Right now it just doenst print anything
|
||||||
|
pub fn terminal_supports_kitty() -> bool {
|
||||||
|
if matches!(
|
||||||
|
env::var("NEO64FETCH_FORCE_KITTY"),
|
||||||
|
Ok(v) if v == "1" || v.eq_ignore_ascii_case("true")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if env::var("KITTY_WINDOW_ID").is_ok() { return true; }
|
||||||
|
if env::var("WEZTERM_PANE").is_ok() { return true; }
|
||||||
|
|
||||||
|
if let Ok(term_program) = env::var("TERM_PROGRAM") {
|
||||||
|
let t = term_program.to_lowercase();
|
||||||
|
if t.contains("kitty") || t.contains("wezterm") || t.contains("ghostty") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(term) = env::var("TERM") {
|
||||||
|
if term.to_lowercase().contains("kitty") { return true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queries terminal for character cell dimensions using ioctl(TIOCGWINSZ).
|
||||||
|
//
|
||||||
|
// See: https://github.com/Ly-sec/swiftfetch/blob/main/src/display.rs#L462-L477
|
||||||
|
fn terminal_cell_metrics() -> (f32, f32) {
|
||||||
|
unsafe {
|
||||||
|
let mut ws: winsize = mem::zeroed();
|
||||||
|
if ioctl(STDOUT_FILENO, TIOCGWINSZ, &mut ws) == 0
|
||||||
|
&& ws.ws_col > 0 && ws.ws_row > 0
|
||||||
|
&& ws.ws_xpixel > 0 && ws.ws_ypixel > 0
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
ws.ws_xpixel as f32 / ws.ws_col as f32,
|
||||||
|
ws.ws_ypixel as f32 / ws.ws_row as f32,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(DEFAULT_CHAR_WIDTH, DEFAULT_CHAR_HEIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads, resizes, and displays an image via Kitty Graphics Protocol.
|
||||||
|
// Loading Logic:
|
||||||
|
// See: https://github.com/Ly-sec/swiftfetch/blob/main/src/display.rs#L479-L527
|
||||||
|
//
|
||||||
|
// Resize logic:
|
||||||
|
// See: https://github.com/Ly-sec/swiftfetch/blob/main/src/display.rs#L530-L545
|
||||||
|
//
|
||||||
|
// Transmission logic:
|
||||||
|
// See: https://github.com/Ly-sec/swiftfetch/blob/main/src/display.rs#L547-L578
|
||||||
|
//
|
||||||
|
// Returns (column_offset, total_rows) for side-by-side text printing.
|
||||||
|
// Returns (0, 0) on failure; unsupported terminal or image load error (skill issue)
|
||||||
|
pub fn print_image_and_setup(path: &str, target_height: u32) -> (usize, usize) {
|
||||||
|
if !terminal_supports_kitty() {
|
||||||
|
return (0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and proportionally resize to target height
|
||||||
|
let image = match image::open(Path::new(path)) {
|
||||||
|
Ok(img) => {
|
||||||
|
let ratio = target_height as f32 / img.height() as f32;
|
||||||
|
let w = ((img.width() as f32 * ratio).round().max(1.0)) as u32;
|
||||||
|
img.resize_exact(w, target_height, image::imageops::FilterType::Lanczos3)
|
||||||
|
}
|
||||||
|
Err(_) => return (0, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (width, height) = image.dimensions();
|
||||||
|
|
||||||
|
// Kitty protocol requires PNG format, even for JPEG/WebP sources
|
||||||
|
// See: https://github.com/Ly-sec/swiftfetch/blob/main/src/display.rs#L495-L501
|
||||||
|
let mut png_bytes = Vec::new();
|
||||||
|
if image.write_to(&mut Cursor::new(&mut png_bytes), ImageFormat::Png).is_err() {
|
||||||
|
return (0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let encoded = BASE64.encode(&png_bytes);
|
||||||
|
let mut output = String::new();
|
||||||
|
let mut start = 0;
|
||||||
|
let mut first = true;
|
||||||
|
|
||||||
|
while start < encoded.len() {
|
||||||
|
let end = (start + CHUNK_SIZE).min(encoded.len());
|
||||||
|
let chunk = &encoded[start..end];
|
||||||
|
let more = if end < encoded.len() { 1 } else { 0 };
|
||||||
|
|
||||||
|
if first {
|
||||||
|
output.push_str(&format!("\x1b_Ga=T,f=100,s={},v={},m={};", width, height, more));
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
output.push_str(&format!("\x1b_Gm={};", more));
|
||||||
|
}
|
||||||
|
output.push_str(chunk);
|
||||||
|
output.push_str("\x1b\\");
|
||||||
|
start = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert pixel dimensions to terminal columns/rows
|
||||||
|
// See: https://github.com/Ly-sec/swiftfetch/blob/main/src/display.rs#L503-L511
|
||||||
|
let (char_w, char_h) = terminal_cell_metrics();
|
||||||
|
let cols = ((width as f32 / char_w).ceil() as usize).max(1) + DEFAULT_GAP_COLUMNS;
|
||||||
|
let rows = ((height as f32 / char_h).ceil() as usize).max(1);
|
||||||
|
let padding_top = 0;
|
||||||
|
let total_rows = rows + padding_top;
|
||||||
|
|
||||||
|
for _ in 0..total_rows {
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// use cursor positioning to place image
|
||||||
|
// swiftfetch uses a different approach with save/restore but lowk didnt work for me
|
||||||
|
// https://github.com/Ly-sec/swiftfetch/blob/main/src/display.rs#L225-L253
|
||||||
|
print!("\x1b[{}A", total_rows);
|
||||||
|
|
||||||
|
if padding_top > 0 {
|
||||||
|
print!("\x1b[{}B", padding_top);
|
||||||
|
}
|
||||||
|
|
||||||
|
print!("\x1b[s");
|
||||||
|
print!("{}", output);
|
||||||
|
std::io::stdout().flush().ok();
|
||||||
|
|
||||||
|
print!("\x1b[u");
|
||||||
|
|
||||||
|
if padding_top > 0 {
|
||||||
|
print!("\x1b[{}A", padding_top);
|
||||||
|
}
|
||||||
|
std::io::stdout().flush().ok();
|
||||||
|
|
||||||
|
(cols, total_rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prints text at a horizontal offset for side-by-side layout with image.
|
||||||
|
//
|
||||||
|
// swiftfetch uses space padding instead of cursor movement:
|
||||||
|
// See: https://github.com/Ly-sec/swiftfetch/blob/main/src/display.rs#L236-L245
|
||||||
|
pub fn print_with_offset(offset: usize, text: &str) {
|
||||||
|
if offset > 0 {
|
||||||
|
print!("\r\x1b[{}C{}\n", offset, text);
|
||||||
|
} else {
|
||||||
|
println!("{}", text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fills remaining vertical space if text is shorter than image height.
|
||||||
|
//
|
||||||
|
// Called after all info lines are printed to make sure the image is not cut off.
|
||||||
|
// See: https://github.com/Ly-sec/swiftfetch/blob/main/src/display.rs#L338-L348
|
||||||
|
pub fn finish_printing(offset: usize, lines_printed: usize, image_rows: usize) {
|
||||||
|
if offset > 0 && lines_printed < image_rows {
|
||||||
|
for _ in 0..(image_rows - lines_printed) {
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! offset_println {
|
||||||
|
($offset:expr, $($arg:tt)*) => {
|
||||||
|
$crate::output::image::print_with_offset($offset, &format!($($arg)*))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use offset_println;
|
||||||
@@ -1 +1,2 @@
|
|||||||
pub mod colors;
|
pub mod colors;
|
||||||
|
pub mod image;
|
||||||
|
|||||||
Reference in New Issue
Block a user