use arboard::Clipboard;
use chrono::Local;
use clap::Parser;
use colored::*;
use image::{ImageBuffer, Rgb, RgbImage};
use qrcode::{EcLevel, QrCode};
use std::process;
#[derive(Parser, Debug)]
#[command(
name = "stabiron",
about = "Beautiful terminal QR code generator — optimized for scanning crypto wallet addresses from iPhone",
version,
author
)]
struct Args {
input: Option<String>,
#[arg(short = 'm', long = "message")]
message: Option<String>,
#[arg(long = "clip")]
clip: bool,
#[arg(long = "color", default_value = "white")]
color: String,
#[arg(long = "size", default_value = "medium")]
size: String,
#[arg(long = "label")]
label: Option<String>,
#[arg(long = "no-label")]
no_label: bool,
#[arg(long = "save")]
save: bool,
}
fn colored_block(s: &str, color: &str) -> ColoredString {
match color {
"red" => s.truecolor(220, 50, 50),
"green" => s.truecolor(50, 205, 50),
"blue" => s.truecolor(80, 160, 255),
"gold" => s.truecolor(255, 200, 0),
_ => s.truecolor(255, 255, 255),
}
}
fn rgb_for_color(color: &str) -> [u8; 3] {
match color {
"red" => [220, 50, 50],
"green" => [50, 205, 50],
"blue" => [80, 160, 255],
"gold" => [255, 200, 0],
_ => [255, 255, 255],
}
}
fn scale_for_size(size: &str) -> usize {
match size {
"small" => 1,
"large" => 2,
_ => 1, }
}
fn padding_for_size(size: &str) -> usize {
match size {
"small" => 1,
"large" => 4,
_ => 2,
}
}
fn render_qr(matrix: &[Vec<bool>], size: &str, color: &str, term_width: usize) {
let scale = scale_for_size(size);
let pad = padding_for_size(size);
let qr_h = matrix.len();
let qr_w = if qr_h > 0 { matrix[0].len() } else { 0 };
let total_w = qr_w * scale + pad * 2;
let total_h = qr_h * scale + pad * 2;
if total_w > term_width.saturating_sub(2) && size != "small" {
eprintln!(
"{}",
" Warning: terminal may be too narrow — try --size small".yellow()
);
}
let is_dark = |col: usize, row: usize| -> bool {
if col < pad || row < pad {
return false;
}
let c = (col - pad) / scale;
let r = (row - pad) / scale;
if r >= qr_h || c >= qr_w {
return false;
}
matrix[r][c]
};
let mut lines: Vec<String> = Vec::new();
let mut row = 0usize;
while row < total_h {
let mut line = String::new();
for col in 0..total_w {
let top = is_dark(col, row);
let bot = if row + 1 < total_h {
is_dark(col, row + 1)
} else {
false
};
let ch = match (top, bot) {
(true, true) => "█",
(true, false) => "▀",
(false, true) => "▄",
(false, false) => " ",
};
if top || bot {
line.push_str(&colored_block(ch, color).to_string());
} else {
line.push(' ');
}
}
lines.push(line);
row += 2;
}
for line in &lines {
println!(" {}", line);
}
}
fn save_png(matrix: &[Vec<bool>], color: &str, input: &str) -> Result<String, String> {
let module_px: u32 = 12;
let quiet: u32 = 4 * module_px;
let qr_h = matrix.len() as u32;
let qr_w = if qr_h > 0 { matrix[0].len() as u32 } else { 0 };
let img_w = qr_w * module_px + quiet * 2;
let img_h = qr_h * module_px + quiet * 2;
let fg = rgb_for_color(color);
let mut img: RgbImage = ImageBuffer::new(img_w, img_h);
for px in img.pixels_mut() {
*px = Rgb([255u8, 255u8, 255u8]);
}
for (r, row) in matrix.iter().enumerate() {
for (c, &dark) in row.iter().enumerate() {
if dark {
let x0 = quiet + c as u32 * module_px;
let y0 = quiet + r as u32 * module_px;
for dy in 0..module_px {
for dx in 0..module_px {
img.put_pixel(x0 + dx, y0 + dy, Rgb(fg));
}
}
}
}
}
let ts = Local::now().format("%Y%m%d_%H%M%S");
let filename = format!("stabiron_{}.png", ts);
let _ = input; img.save(&filename)
.map_err(|e| format!("Failed to save PNG: {}", e))?;
Ok(filename)
}
fn qr_to_matrix(code: &QrCode) -> Vec<Vec<bool>> {
let width = code.width();
let colors = code.to_colors();
let mut matrix = vec![vec![false; width]; width];
for row in 0..width {
for col in 0..width {
matrix[row][col] = colors[row * width + col] == qrcode::Color::Dark;
}
}
matrix
}
fn get_term_width() -> usize {
if let Ok(val) = std::env::var("COLUMNS") {
if let Ok(n) = val.parse::<usize>() {
return n;
}
}
120
}
fn main() {
let args = Args::parse();
let input: String = if args.clip {
match Clipboard::new() {
Ok(mut cb) => match cb.get_text() {
Ok(text) if !text.trim().is_empty() => text.trim().to_string(),
Ok(_) => {
eprintln!("{}", " Clipboard is empty. Copy something first.".red());
process::exit(1);
}
Err(e) => {
eprintln!("{}", format!(" Failed to read clipboard: {}", e).red());
process::exit(1);
}
},
Err(e) => {
eprintln!("{}", format!(" Clipboard unavailable: {}", e).red());
process::exit(1);
}
}
} else if let Some(ref m) = args.message {
m.trim().to_string()
} else if let Some(ref i) = args.input {
i.trim().to_string()
} else {
eprintln!(
"{}",
"\n stabiron — terminal QR code generator\n".bright_white().bold()
);
eprintln!(" Usage:");
eprintln!(" stabiron \"<text or address>\"");
eprintln!(" stabiron -m \"<text>\"");
eprintln!(" stabiron --clip (reads from clipboard)");
eprintln!(" stabiron --help (full options)\n");
process::exit(0);
};
if input.is_empty() {
eprintln!("{}", " Input is empty. Nothing to encode.".red());
process::exit(1);
}
let color = args.color.to_lowercase();
let valid_colors = ["white", "red", "green", "blue", "gold"];
if !valid_colors.contains(&color.as_str()) {
eprintln!(
"{}",
format!(
" Unknown color '{}'. Choose: white, red, green, blue, gold",
color
)
.red()
);
process::exit(1);
}
let mut size = args.size.to_lowercase();
if !["small", "medium", "large"].contains(&size.as_str()) {
eprintln!(
"{}",
format!(
" Unknown size '{}'. Choose: small, medium, large",
size
)
.red()
);
process::exit(1);
}
let len = input.len();
if len > 200 && size != "large" {
eprintln!(
"{}",
" Input is long — auto-upgrading to --size large for reliability.".yellow()
);
size = "large".to_string();
}
let code = match QrCode::with_error_correction_level(input.as_bytes(), EcLevel::H) {
Ok(c) => c,
Err(e) => {
eprintln!("{}", format!(" Failed to generate QR code: {}", e).red());
process::exit(1);
}
};
let matrix = qr_to_matrix(&code);
let tw = get_term_width();
if !args.no_label {
println!();
if let Some(ref lbl) = args.label {
println!(" {}", lbl.bright_white().bold());
}
} else {
println!();
}
render_qr(&matrix, &size, &color, tw);
if !args.no_label {
let preview = if input.len() > 48 {
format!("{}…", &input[..47])
} else {
input.clone()
};
println!();
println!(" {}", preview.bright_black());
println!();
} else {
println!();
}
if args.save {
match save_png(&matrix, &color, &input) {
Ok(path) => println!(" {} {}", "Saved:".bright_green().bold(), path),
Err(e) => eprintln!("{}", format!(" {}", e).red()),
}
println!();
}
}