use crate::{Block, Color, Environment, Icon, IconMode, Pretty, Style, WithStyle as _};
use rustix::fs::{Access, Stat};
use std::{
ffi::OsString,
os::unix::ffi::OsStringExt as _,
path::{Path, PathBuf},
};
enum State {
Writeable,
Readable,
Moved,
Deleted,
NoAccess,
}
impl Icon for State {
fn icon(&self, mode: IconMode) -> &'static str {
use IconMode::*;
match self {
Self::Writeable => "",
Self::Readable => match mode {
Text => "readonly ",
Icons | MinimalIcons => " ",
},
Self::Deleted => match mode {
Text => "deleted ",
Icons | MinimalIcons => " ",
},
Self::Moved => match mode {
Text => "moved ",
Icons | MinimalIcons => " ",
},
Self::NoAccess => match mode {
Text => "forbidden ",
Icons | MinimalIcons => " ",
},
}
}
}
impl Pretty for State {
fn pretty(&self, f: &mut std::fmt::Formatter, mode: IconMode) -> std::fmt::Result {
f.with_style(Color::RED, Style::ITALIC, |f| {
write!(f, "{}", self.icon(mode))
})
}
}
fn get_cwd_if_deleted() -> Option<PathBuf> {
let mut cwd = std::fs::read_link("/proc/self/cwd")
.ok()?
.into_os_string()
.into_vec();
cwd.truncate(cwd.strip_suffix(b" (deleted)")?.len());
Some(PathBuf::from(OsString::from_vec(cwd)))
}
fn ensure_work_dir_not_moved(work_dir: &Path, stat_dot: Stat) -> std::io::Result<bool> {
let stat_pwd = rustix::fs::stat(work_dir)?;
Ok(
((stat_dot.st_dev, stat_dot.st_ino) == (stat_pwd.st_dev, stat_pwd.st_ino))
&& std::env::var_os("PWD").is_none_or(|pwd| pwd == work_dir),
)
}
fn get_state(work_dir: &mut PathBuf) -> State {
let Ok(stat_dot) = rustix::fs::stat(".") else {
return State::NoAccess;
};
if 0 == stat_dot.st_nlink {
if let Some(cwd) = get_cwd_if_deleted() {
*work_dir = cwd;
}
return State::Deleted;
}
if !matches!(ensure_work_dir_not_moved(work_dir, stat_dot), Ok(true)) {
return State::Moved;
}
match rustix::fs::access(&*work_dir, Access::WRITE_OK) {
Ok(()) => State::Writeable,
Err(_) => State::Readable,
}
}
pub struct Workdir {
work_dir: PathBuf,
git_tree: Option<PathBuf>,
current_home: Option<(PathBuf, String)>,
state: State,
}
super::register_block!(Workdir);
impl Block for Workdir {
fn new(environ: &Environment) -> Option<Self> {
let mut work_dir = environ.work_dir.clone();
let git_tree = environ.git_tree.clone();
let current_home = environ.current_home.clone();
let state = get_state(&mut work_dir);
Some(Workdir {
work_dir,
git_tree,
current_home,
state,
})
}
}
impl Pretty for Workdir {
fn pretty(&self, f: &mut std::fmt::Formatter<'_>, mode: IconMode) -> std::fmt::Result {
let (middle, highlighted) = match (&self.git_tree, &self.current_home) {
(Some(git_root), Some((home_root, _))) => {
if home_root.starts_with(git_root) {
(None, self.work_dir.strip_prefix(home_root).ok())
} else {
(
git_root.strip_prefix(home_root).ok(),
self.work_dir.strip_prefix(git_root).ok(),
)
}
}
(Some(git_root), None) => (
Some(git_root.as_path()),
self.work_dir.strip_prefix(git_root).ok(),
),
(None, Some((home_root, _))) => (self.work_dir.strip_prefix(home_root).ok(), None),
(None, None) => (Some(self.work_dir.as_path()), None),
};
let middle = match middle {
Some(p) if p.as_os_str().is_empty() => None,
otherwise => otherwise,
};
write!(f, "{}", crate::icon::display(&self.state, mode))?;
if let Some((_, user)) = &self.current_home {
f.with_style(Color::YELLOW, Style::BOLD, |f| write!(f, "~{user}"))?;
}
if self.current_home.is_some() && middle.is_some() {
write!(f, "/")?;
}
if let Some(middle) = middle {
write!(f, "{}", middle.display())?;
}
if let Some(high) = highlighted {
f.with_style(Color::CYAN, Style::empty(), |f| {
write!(f, "/{}", high.display())
})?;
}
Ok(())
}
}