look_at_that/src/main.rs

209 lines
4.9 KiB
Rust

use anyhow::{Context as _, Result, anyhow};
use camino::{Utf8Path, Utf8PathBuf};
use std::{
borrow::Cow,
fs,
io::{Read as _, Write as _},
rc::Rc,
};
fn main() -> Result<()> {
let mut app = App::try_new()?;
app.run()
}
struct App {
is_terminal: bool,
lk: Option<Lk>,
paths: Vec<Cow<'static, Utf8Path>>,
path_index: usize,
}
impl App {
fn try_new() -> Result<Self> {
use std::io::IsTerminal as _;
let is_terminal = std::io::stdout().is_terminal();
let mut args = std::env::args();
let _exe_name = args.next().context("How do we not have an exe name?")?;
let mut paths = vec![];
for arg in args {
paths.push(Cow::Owned(Utf8PathBuf::from(arg)));
}
let paths = if paths.is_empty() {
vec![Cow::Borrowed(Utf8Path::new("."))]
} else {
paths
};
Ok(App {
is_terminal,
lk: None,
paths,
path_index: 0,
})
}
fn poll(&mut self) -> Result<Option<Rc<[u8]>>> {
loop {
if self.path_index >= self.paths.len() {
return Ok(None);
}
let path = &self.paths[self.path_index];
match self.lk.as_mut() {
Some(lk) => {
if let Some(buf) = lk.poll()? {
return Ok(Some(buf));
} else {
self.lk = None;
self.path_index += 1;
}
}
None => {
self.lk = Some(Lk::try_new(path)?);
}
};
}
}
fn run(&mut self) -> Result<()> {
while let Some(buf) = self.poll()? {
std::io::stdout().write_all(&buf)?;
}
Ok(())
}
}
enum Lk {
Cat(Cat),
Ls(Ls),
}
impl Lk {
fn try_new(path: &Utf8Path) -> Result<Self> {
let metadata = fs::metadata(path)?;
if metadata.is_dir() {
Self::try_new_ls(path)
} else {
Self::try_new_cat(path)
}
}
fn try_new_cat(path: &Utf8Path) -> Result<Self> {
Ok(Self::Cat(Cat::try_new(path)?))
}
fn try_new_ls(path: &Utf8Path) -> Result<Self> {
Ok(Self::Ls(Ls::try_new(path)?))
}
// I don't understand why, but using `Rc` here instead of `Cow` satisfies the borrow checker.
fn poll(&mut self) -> Result<Option<Rc<[u8]>>> {
match self {
Lk::Cat(x) => x.poll(),
Lk::Ls(x) => x.poll(),
}
}
}
struct Cat {
f: fs::File,
buf: Vec<u8>,
}
impl Cat {
fn try_new(path: &Utf8Path) -> Result<Self> {
let f = fs::File::open(path)?;
let buf = vec![0u8; 4_096];
Ok(Self { f, buf })
}
fn poll(&mut self) -> Result<Option<Rc<[u8]>>> {
let bytes_read = self.f.read(&mut self.buf)?;
let buf = &self.buf[0..bytes_read];
if bytes_read == 0 {
return Ok(None);
}
Ok(Some(buf.into()))
}
}
struct Ls {
names: Vec<String>,
name_index: usize,
}
impl Ls {
fn try_new(path: &Utf8Path) -> Result<Self> {
let d = fs::read_dir(path)?;
let mut names = vec![];
for entry in d {
let entry = entry?;
names.push(
entry
.file_name()
.into_string()
.map_err(|_| anyhow!("name is not UTF-8"))?,
);
}
names.sort();
Ok(Self {
names,
name_index: 0,
})
}
fn poll(&mut self) -> Result<Option<Rc<[u8]>>> {
if self.name_index >= self.names.len() {
return Ok(None);
}
let name = &self.names[self.name_index];
self.name_index += 1;
Ok(Some(format!("{name}\n").into_bytes().into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test() -> Result<()> {
let dir = tempfile::tempdir()?;
let dir = dir.as_ref();
fs::write(dir.join("alfa.txt"), "alfa\n")?;
fs::write(dir.join("bravo.txt"), "bravo\n")?;
for (input, expected) in [
(vec![dir], b"alfa.txt\nbravo.txt\n".to_vec()),
(
vec![&dir.join("alfa.txt"), &dir.join("bravo.txt")],
b"alfa\nbravo\n".to_vec(),
),
] {
assert_eq!(lk(input)?, expected);
}
Ok(())
}
fn lk(paths: Vec<&Path>) -> Result<Vec<u8>> {
let paths = paths
.iter()
.map(|p| Cow::Owned(Utf8PathBuf::from_path_buf(p.to_path_buf()).unwrap()))
.collect();
let mut app = App {
is_terminal: false,
lk: None,
paths,
path_index: 0,
};
let mut out = vec![];
while let Some(buf) = app.poll()? {
out.write_all(&buf)?;
}
Ok(out)
}
}