Skip to main content

artifactize_core/review/
runs.rs

1//! What a declared Human tool runs, read from the saved Human definition for display.
2//!
3//! A command tool keeps its shell-quoted declaration. A builtin tool names its action and
4//! target in words, the target as a repository-relative logical path with `/`, so a reviewer
5//! sees what Enter does without reading the declaration.
6
7use crate::{
8    store::definitions::{Artifact, HumanTool},
9    types::ArtifactName,
10};
11use std::collections::BTreeMap;
12
13use crate::config::Builtin;
14
15/// The repository root shown as a target, when an Artifact sits at the root.
16const ROOT: &str = ".";
17
18/// What a tool runs, placeholders resolved to logical paths where the definition allows.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum Runs {
21    /// The declared command and args, shell-quoted, placeholders unresolved.
22    Command(String),
23    /// Open a file or folder in the desktop's default application.
24    Open(String),
25    /// Open an HTTP(S) URL in the browser.
26    OpenUrl(String),
27    /// Print a UTF-8 file.
28    Read(String),
29    /// List a folder.
30    List(String),
31    /// Print one section of a Markdown file.
32    Section { file: String, heading: String },
33    /// Print `<program> <subcommand...> --help`.
34    Help(String),
35}
36
37impl Runs {
38    /// The declaration of one Human tool of `owner`, within the definition's Artifacts.
39    pub(super) fn parse(
40        artifacts: &BTreeMap<ArtifactName, Artifact>,
41        owner: &ArtifactName,
42        tool: &HumanTool,
43    ) -> Self {
44        let (builtin, command, args) = match tool {
45            HumanTool::Command(tool) => (None, tool.command.as_str(), tool.args.as_slice()),
46            HumanTool::Builtin(tool) => (Some(tool.builtin), "", tool.args.as_slice()),
47        };
48        let args: Vec<&str> = args.iter().map(String::as_str).collect();
49        let target = || {
50            args.first()
51                .map(|arg| logical(artifacts, owner, arg))
52                .unwrap_or_default()
53        };
54        match (builtin, args.as_slice()) {
55            (Some(Builtin::Open), [url]) if artifactize_tools::builtin::is_url(url) => {
56                Self::OpenUrl((*url).to_owned())
57            }
58            (Some(Builtin::Open), [_]) => Self::Open(target()),
59            (Some(Builtin::Read), [_]) => Self::Read(target()),
60            (Some(Builtin::List), [_]) => Self::List(target()),
61            (Some(Builtin::Section), [_, heading]) => Self::Section {
62                file: target(),
63                heading: (*heading).to_owned(),
64            },
65            (Some(Builtin::Help), [_, ..]) => Self::Help(super::shell(args.iter().copied())),
66            _ => {
67                let program = builtin
68                    .map(|builtin| {
69                        match builtin {
70                            Builtin::Read => "read",
71                            Builtin::List => "list",
72                            Builtin::Glob => "glob",
73                            Builtin::Grep => "grep",
74                            Builtin::ViewImage => "view_image",
75                            Builtin::Section => "section",
76                            Builtin::Help => "help",
77                            Builtin::Open => "open",
78                            Builtin::Graph => "graph",
79                        }
80                        .to_owned()
81                    })
82                    .unwrap_or_else(|| crate::platform::path_text(std::path::Path::new(command)));
83                Self::Command(super::shell(
84                    std::iter::once(program.as_str()).chain(args.iter().copied()),
85                ))
86            }
87        }
88    }
89
90    /// The action in one line: `$ command args`, or a builtin's verb and target.
91    pub fn line(&self) -> String {
92        match self {
93            Self::Command(command) => format!("$ {command}"),
94            Self::Open(path) => format!("opens {path} in its default app"),
95            Self::OpenUrl(url) => format!("opens {url} in the browser"),
96            Self::Read(path) => format!("prints {path}"),
97            Self::List(path) => format!("lists {}", folder(path)),
98            Self::Section { file, heading } => format!("prints section \"{heading}\" of {file}"),
99            Self::Help(program) => format!("prints {program} --help"),
100        }
101    }
102}
103
104/// A folder target ends in `/`, so it reads as a folder.
105fn folder(path: &str) -> String {
106    if path.ends_with('/') {
107        path.to_owned()
108    } else {
109        format!("{path}/")
110    }
111}
112
113/// A builtin target as a repository-relative logical path: `{artifactPath}[/path]`,
114/// `{name}[/path]` or a path in the owner. A target the definition cannot place is shown as
115/// declared.
116fn logical(
117    artifacts: &BTreeMap<ArtifactName, Artifact>,
118    owner: &ArtifactName,
119    arg: &str,
120) -> String {
121    let (id, rest) = match arg.strip_prefix('{').and_then(|arg| arg.split_once('}')) {
122        Some(("artifactPath", rest)) => (owner.as_str(), rest),
123        Some((name, rest)) => (name, rest),
124        None => (owner.as_str(), arg),
125    };
126    let rest = rest.strip_prefix('/').unwrap_or(rest);
127    let Some(artifact) = artifacts.get(id) else {
128        return arg.to_owned();
129    };
130    let Some(path) = artifact.path.value() else {
131        return arg.to_owned();
132    };
133    let path = crate::platform::path_text(path);
134    let base =
135        if rest.is_empty() || artifact.kind.value() != Some(&crate::config::ArtifactKind::File) {
136            path.as_str()
137        } else {
138            path.rsplit_once('/').map_or("", |(parent, _)| parent)
139        };
140    match (base, rest) {
141        ("", "") => ROOT.to_owned(),
142        ("", rest) => rest.to_owned(),
143        (base, "") => base.to_owned(),
144        (base, rest) => format!("{base}/{rest}"),
145    }
146}