#![warn(
clippy::cargo,
clippy::as_underscore,
clippy::clone_on_ref_ptr,
clippy::default_numeric_fallback,
clippy::format_push_string,
clippy::host_endian_bytes,
clippy::if_then_some_else_none,
clippy::let_underscore_must_use,
clippy::map_err_ignore,
clippy::missing_assert_message,
clippy::multiple_unsafe_ops_per_block,
clippy::renamed_function_params,
clippy::shadow_same,
clippy::shadow_unrelated,
clippy::string_lit_chars_any,
clippy::undocumented_unsafe_blocks,
clippy::unnecessary_safety_comment,
clippy::unnecessary_safety_doc,
clippy::unseparated_literal_suffix,
clippy::unused_result_ok,
clippy::unused_trait_names,
clippy::verbose_file_reads
)]
#![allow(clippy::multiple_crate_versions)]
mod block;
mod chassis;
mod ext;
mod file;
mod icon;
mod style;
mod virt;
mod workgroup;
use crate::{
block::{Block, create_blocks},
chassis::Chassis,
icon::{Icon, IconMode, Pretty},
style::{Color, Style, WithStyle, horizontal_absolute},
};
use argh::FromArgs;
use pwd::Passwd;
use rustix::{
fd::{FromRawFd as _, OwnedFd},
fs::{Mode, OFlags},
};
use shell_quote::QuoteInto as _;
use std::{
fmt::Write as _,
io::Write as _,
os::{fd::AsRawFd as _, unix::ffi::OsStrExt as _},
path::PathBuf,
time::Duration,
};
use unicode_width::UnicodeWidthStr as _;
fn readline_width(s: &str) -> usize {
let mut res = s.width();
for (i, c) in s.bytes().enumerate() {
match c {
b'\x01' => res += i,
b'\x02' => res -= i + 1,
_ => {}
}
}
res
}
#[derive(FromArgs)]
struct Arguments {
#[argh(subcommand)]
command: Option<Command>,
}
#[derive(FromArgs)]
#[argh(subcommand)]
enum Command {
Colorize(Colorize),
WorkgroupCreate(WorkgroupCreate),
Chain(Chain),
Run(Run),
Env(Env),
}
#[derive(FromArgs)]
#[argh(subcommand, name = "env")]
struct Env {}
#[derive(FromArgs)]
#[argh(subcommand, name = "chain")]
struct Chain {}
#[derive(FromArgs)]
#[argh(subcommand, name = "create")]
struct WorkgroupCreate {}
#[derive(FromArgs)]
#[argh(subcommand, name = "colorize")]
struct Colorize {
#[argh(option)]
what: String,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "run")]
struct Run {
#[argh(option)]
return_code: Option<u8>,
#[argh(option)]
jobs_count: Option<usize>,
#[argh(option)]
elapsed_time: Option<u64>,
#[argh(option)]
control_fd: Option<i32>,
#[argh(option)]
mode: Option<String>,
#[argh(option)]
left: Option<String>,
#[argh(option)]
middle: Option<String>,
#[argh(option)]
right: Option<String>,
#[argh(option)]
bottom: Option<String>,
}
pub struct Environment {
pub ret_code: Option<u8>,
pub jobs_count: Option<usize>,
pub elapsed_time: Option<Duration>,
pub work_dir: PathBuf,
pub git_tree: Option<PathBuf>,
pub user: String,
pub host: String,
pub current_home: Option<(PathBuf, String)>,
pub left: Vec<String>,
pub middle: Vec<String>,
pub right: Vec<String>,
pub bottom: Vec<String>,
}
fn parse_block_names(s: Option<String>) -> Vec<String> {
s.unwrap_or_default()
.split(',')
.filter(|&s| !s.is_empty())
.map(|s| s.into())
.collect()
}
impl From<Run> for Environment {
fn from(other: Run) -> Environment {
let ret_code = other.return_code;
let jobs_count = other.jobs_count;
let elapsed_time = other.elapsed_time.map(Duration::from_micros);
let work_dir = std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from(std::env::var("PWD").unwrap()));
let git_tree = file::upfind(&work_dir, ".git").map(|dg| dg.parent().unwrap().to_path_buf());
let user = Passwd::current_user()
.map(|entry| entry.name)
.or_else(|| std::env::var("USER").ok())
.unwrap_or_else(|| format!("<user{}>", rustix::process::getuid().as_raw()));
let host = rustix::system::uname()
.nodename()
.to_string_lossy()
.into_owned();
let current_home = file::find_current_home(&work_dir, &user);
Environment {
ret_code,
jobs_count,
elapsed_time,
work_dir,
git_tree,
user,
host,
current_home,
left: parse_block_names(other.left),
middle: parse_block_names(other.middle),
right: parse_block_names(other.right),
bottom: parse_block_names(other.bottom),
}
}
}
fn bash_quote(data: &[u8]) -> String {
let mut out = String::new();
shell_quote::Bash::quote_into(data, &mut out);
out
}
fn main() {
let exec = std::fs::read_link("/proc/self/exe").map_or_else(
|_| "statusline".into(),
|pb| bash_quote(pb.into_os_string().as_bytes()),
);
let args: Arguments = argh::from_env();
let Some(command) = args.command else {
let ver = env!("CARGO_PKG_VERSION");
let apply_me = format!("source <({exec} env)");
let apply_me_quot = bash_quote(apply_me.as_bytes());
println!("[statusline {ver}]: colorful PS1 for Bash");
println!(">> https://codeberg.org/sylfn/statusline");
println!("Use `--help` to see advanced usage");
println!("Simple install:");
println!(" echo {apply_me_quot} >> ~/.bashrc");
println!(" source ~/.bashrc");
println!("Test now:");
println!(" {apply_me}");
return;
};
match command {
Command::Colorize(Colorize { what }) => {
let mut res = String::new();
res.with_style(Color::of(&what), Style::BOLD, |f| write!(f, "{what}"))
.unwrap();
println!("{res}");
}
Command::WorkgroupCreate(_) => {
workgroup::key_create().expect("Could not create workgroup key");
}
Command::Env(_) => println!("{}", include_str!("shell.sh").replace("@sl@", &exec)),
Command::Chain(_) => {
let Ok(key) = workgroup::key_load() else {
return;
};
let mut ssh_chain = workgroup::chain_open(Some(&key));
ssh_chain.push(
rustix::system::uname()
.nodename()
.to_string_lossy()
.into_owned(),
);
println!("{}", workgroup::chain_seal(&ssh_chain, &key));
}
Command::Run(run) => run_statusline(run),
}
}
fn run_statusline(run: Run) {
if let Some(fd) = run.control_fd {
let fd = unsafe { OwnedFd::from_raw_fd(fd) };
unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_SETOWN, rustix::process::getpid()) };
rustix::fs::fcntl_setfl(fd, OFlags::ASYNC).unwrap();
}
let mode = match run.mode.as_deref() {
Some("text") => IconMode::Text,
Some("minimal") => IconMode::MinimalIcons,
_ => IconMode::Icons,
};
let environ: Environment = run.into();
let mut left = create_blocks(&environ.left, &environ);
let middle = create_blocks(&environ.middle, &environ);
let right = create_blocks(&environ.right, &environ);
let bottom = create_blocks(&environ.bottom, &environ);
print_statusline(mode, &environ, &mut left, &middle, &right, &bottom);
}
fn print_statusline(
mode: IconMode,
environ: &Environment,
left: &mut [Box<dyn Block>],
middle: &[Box<dyn Block>],
right: &[Box<dyn Block>],
bottom: &[Box<dyn Block>],
) {
let middle = pretty(middle, mode);
let right = pretty(right, mode);
let bottom = pretty(bottom, mode);
let cont = if let IconMode::Text = mode {
">"
} else {
"\u{f105}"
};
let terminal_width: usize = terminal_size::terminal_size()
.map_or(80, |(w, _h)| w.0)
.into();
let right_length = readline_width(&right);
let right_formatted = format!(
"{}{right}",
horizontal_absolute(terminal_width.saturating_sub(right_length))
);
let left_formatted = pretty(left, mode);
let three_line_mode =
readline_width(&left_formatted) + readline_width(&middle) + right_length + 16
>= terminal_width;
let prologue = crate::style::prologue(three_line_mode);
let epilogue = crate::style::epilogue();
let eprint_top_part = |top| {
if three_line_mode {
eprint!("{prologue}{top}{right_formatted}\n{cont} {middle}{epilogue}");
} else {
eprint!("{prologue}{top} {middle}{right_formatted}{epilogue}");
}
};
let title = make_title(environ);
eprint!("{title}");
if three_line_mode {
eprint!("\n\n\n");
} else {
eprint!("\n\n");
}
eprint_top_part(left_formatted);
print!("{bottom} ");
std::io::stdout().flush().unwrap();
rustix::stdio::dup2_stdout(rustix::fs::open("/dev/null", OFlags::RDWR, Mode::empty()).unwrap())
.unwrap();
for block in &mut *left {
block.extend();
}
eprint_top_part(pretty(left, mode));
}
fn make_title(env: &Environment) -> String {
let pwd = if let Some((home, user)) = &env.current_home {
let wd = env.work_dir.strip_prefix(home).unwrap_or(&env.work_dir);
if wd.as_os_str().is_empty() {
format_args!("~{}", *user)
} else {
format_args!("~{}/{}", *user, wd.display())
}
} else {
format_args!("{}", env.work_dir.display())
};
crate::style::title(&format!("{}@{}: {}", env.user, env.host, pwd))
}
fn pretty(line: &[Box<dyn Block>], mode: IconMode) -> String {
let mut res = String::new();
for block in line {
let prev_len = res.len();
write!(res, "{}", crate::icon::display(block.as_ref(), mode)).unwrap();
if prev_len != res.len() {
res.push(' ');
}
}
res.pop();
res
}