qcli/actions/
workspace.rs1use std::fs;
4use std::io;
5use std::path::{Path, PathBuf};
6
7use super::{Action, Outcome, Preview, edit, list, read, run, search};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Workspace {
12 root: PathBuf,
13}
14
15impl Workspace {
16 pub fn new(root: &Path) -> io::Result<Self> {
22 let root = fs::canonicalize(root)?;
23 if !root.is_dir() {
24 return Err(io::Error::new(io::ErrorKind::NotADirectory, "the workspace must be a folder"));
25 }
26 Ok(Self { root })
27 }
28
29 #[must_use]
31 pub fn root(&self) -> &Path {
32 &self.root
33 }
34
35 pub fn preview(&self, action: &Action) -> Result<Preview, String> {
42 match action {
43 Action::Read { path } | Action::List { path } | Action::Search { path, .. } => {
44 self.resolve(path).map(|_| Preview::Plain)
45 }
46 Action::Edit { path, old_text, new_text } => {
47 edit::plan(self, path, old_text, new_text).map(|plan| plan.preview())
48 }
49 Action::Run { command } => Ok(Preview::Command { command: command.clone() }),
50 }
51 }
52
53 pub fn perform(&self, action: &Action, cancelled: &dyn Fn() -> bool) -> Outcome {
57 let result = match action {
58 Action::Read { path } => read::read(self, path),
59 Action::List { path } => list::list(self, path),
60 Action::Search { pattern, path } => search::search(self, pattern, path),
61 Action::Edit { path, old_text, new_text } => edit::edit(self, path, old_text, new_text),
62 Action::Run { command } => return run::run(self, command, cancelled, run::LIMIT),
63 };
64 match result {
65 Ok(text) => Outcome::done(text),
66 Err(text) => Outcome::failed(text),
67 }
68 }
69
70 pub(super) fn resolve(&self, path: &str) -> Result<PathBuf, String> {
77 let given = Path::new(path);
78 let joined = if given.is_absolute() { given.to_path_buf() } else { self.root.join(given) };
79 let mut existing = joined.as_path();
80 let mut missing = Vec::new();
81 while fs::symlink_metadata(existing).is_err() {
82 match (existing.file_name(), existing.parent()) {
85 (Some(name), Some(parent)) => {
86 missing.push(name);
87 existing = parent;
88 }
89 _ => {
90 return Err(format!("`{path}` cannot be resolved; use a plain path inside the working folder."));
91 }
92 }
93 }
94 let real = fs::canonicalize(existing).map_err(|_| {
95 format!("`{path}` goes through a link that leads nowhere; use a path inside the working folder.")
96 })?;
97 let real = missing.iter().rev().fold(real, |acc, name| acc.join(name));
98 if real.starts_with(&self.root) {
99 Ok(real)
100 } else {
101 Err(format!("`{path}` is outside the working folder; only paths inside it can be used."))
102 }
103 }
104
105 pub(super) fn relative(&self, real: &Path) -> String {
107 let relative = real.strip_prefix(&self.root).unwrap_or(real);
108 if relative.as_os_str().is_empty() { ".".to_owned() } else { relative.to_string_lossy().into_owned() }
109 }
110}
111
112pub(super) fn io_problem(shown: &str, error: &io::Error) -> String {
114 match error.kind() {
115 io::ErrorKind::NotFound => format!("`{shown}` does not exist."),
116 io::ErrorKind::PermissionDenied => format!("`{shown}` cannot be used: permission denied."),
117 _ => format!("`{shown}` cannot be used: {error}."),
118 }
119}