Skip to main content

ltrait_source_task/
lib.rs

1use ltrait::{
2    Action, Source,
3    color_eyre::eyre::Context as _,
4    tokio_stream::{self, StreamExt as _},
5};
6use serde::Deserialize;
7use std::{
8    os::unix::process::CommandExt as _,
9    path::PathBuf,
10    process::{Command, Stdio},
11};
12use thiserror::Error;
13
14#[derive(Debug, Error)]
15pub enum TaskError {
16    #[error("Could not find config directory")]
17    ConfigDir,
18    #[error("failed to read task config file: {0}")]
19    ReadTaskFile(#[source] std::io::Error),
20    #[error("failed to parse the toml: {0}")]
21    Toml(#[source] toml::de::Error),
22}
23
24#[derive(Debug, Clone)]
25pub struct TaskConfig {
26    /// The list of paths to load tasks
27    pub path: Vec<PathBuf>,
28}
29
30/// The default path is ~/.config/yurf/task.toml
31pub fn default_path() -> Result<PathBuf, TaskError> {
32    Ok(dirs::config_dir()
33        .ok_or(TaskError::ConfigDir)?
34        .join("yurf")
35        .join("task.toml"))
36}
37
38#[derive(Debug, Clone)]
39pub struct Task {
40    config: TaskConfig,
41}
42
43#[derive(Deserialize, Debug, Clone)]
44pub struct TaskItem {
45    pub name: String,
46    /// show only if this command returns 0(exit code).
47    /// this command will be executed  when source is evaled.
48    ///
49    /// the command will be executed as shell command("sh -c")
50    pub show_if: Option<String>,
51    /// the action will execute this command.
52    ///
53    /// the command will be executed as shell command("sh -c")
54    pub command: String,
55}
56
57#[derive(Deserialize, Debug)]
58struct TaskFile {
59    task: Vec<TaskItem>,
60}
61
62impl Task {
63    pub fn new(config: TaskConfig) -> Self {
64        Self { config }
65    }
66
67    pub fn create_source(&self) -> Result<Source<TaskItem>, TaskError> {
68        let tasks = self
69            .config
70            .path
71            .clone()
72            .into_iter()
73            .map(|p| Ok(p))
74            .map(|p| p.and_then(|p| std::fs::read_to_string(p).map_err(TaskError::ReadTaskFile)))
75            .map(|p| p.and_then(|p| toml::from_str::<TaskFile>(&p).map_err(TaskError::Toml)))
76            .map(|p| p.map(|t| t.task))
77            .collect::<Result<Vec<_>, _>>()?
78            .into_iter()
79            .flatten();
80
81        let aiter = tokio_stream::iter(tasks).filter(|c| {
82            let cmd = c.show_if.as_ref();
83            if let Some(cmd) = cmd {
84                Command::new("sh")
85                    .arg("-c")
86                    .arg(&cmd)
87                    .stdin(Stdio::null())
88                    .stdout(Stdio::null())
89                    .stderr(Stdio::null())
90                    .process_group(0)
91                    .output()
92                    .map(|o| o.status.code() == Some(0))
93                    .unwrap_or(false)
94            } else {
95                true
96            }
97        });
98
99        Ok(Box::pin(aiter))
100    }
101}
102
103impl Action for Task {
104    type Context = TaskItem;
105
106    fn act(&self, ctx: &Self::Context) -> ltrait::color_eyre::eyre::Result<()> {
107        Command::new("sh")
108            .arg("-c")
109            .arg(&ctx.command)
110            .stdin(Stdio::null())
111            .stdout(Stdio::null())
112            .stderr(Stdio::null())
113            .process_group(0)
114            .spawn()
115            .wrap_err("failed to start the selected app")?;
116
117        Ok(())
118    }
119}