Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ serde_json = "1.0"
find_folder = "0.3.0"
directories = "1.0"
structopt = "0.2"
regex = "1"
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Edit `config.json` in your configuration path to change spaceruns behaviour.

## Cool & shiny things to implement

* [ ] Auto-generate a form by placeholders in the command (e.g. `${'My Field Name': default val}`)
* [x] Auto-generate a form by placeholders in the command (e.g. `${'My Field Name': default val}`)
* [ ] Context sensitive commands
* [ ] Prev focused window. I could have added a `"class": "^chromium"` to the commands config.
This option will only show if chromium was focused previous to spacerun being opened.
Expand All @@ -65,10 +65,11 @@ Edit `config.json` in your configuration path to change spaceruns behaviour.
(Also solvable by using a CLI interface (not yet implemented), but not as cool?)
* [ ] Server mode, instance is constantly running in background so the JSON won't need to be parsed
each time the window should be displayed.
* [ ] Breadcrumbs, showing the path you went down.
* [x] Breadcrumbs, showing the path you went down.
* [ ] Show name and description of the current tree as a title / subtitle of the window
* [ ] Radial menu instead of list as option (Because radial menus are awesome!)
* [ ] Unicode / emoticons / ligatures / FontAwesome support
* [ ] dmenu parameter mode (similar to rofi)
* [ ] More key-value pairs for command leafs!
* [ ] "description" to find / understand your nodes & commands, even after a long night.
* [ ] "clip" copying a string to clipboard.
Expand Down
5 changes: 5 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
"shortcut": "f",
"name": "firefox",
"cmd": "firefox"
},
{
"shortcut": "h",
"name": "Example templating form",
"cmd": "echo {{text}}; echo \"===========\n\"; {{postcmd}}"
}
]
}
Expand Down
113 changes: 110 additions & 3 deletions src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
use std::collections::HashMap;
use std::fmt::{self, Display};
use std::str::FromStr;

use regex::{Regex, RegexBuilder};
use serde::de;
use serde_derive::Deserialize;

use crate::bindings::Shortcut;
Expand All @@ -6,15 +12,15 @@ use crate::bindings::Shortcut;
pub struct CommandNode {
pub shortcut: Shortcut,
pub name: String,
pub cmd: Option<String>,
pub cmd: Option<CommandTask>,
pub children: Vec<Command>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct CommandLeaf {
pub shortcut: Shortcut,
pub name: String,
pub cmd: String,
pub cmd: CommandTask,
}

#[derive(Debug, Clone, Deserialize)]
Expand All @@ -24,6 +30,107 @@ pub enum Command {
Leaf(CommandLeaf),
}

#[derive(Copy, Clone)]
pub struct CommandTaskParseError;
impl Display for CommandTaskParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Failed to parse a command task")
}
}
impl fmt::Debug for CommandTaskParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Failed to parse a command task")
}
}

/**
* The executable part of a command.
*/
#[derive(Debug, Clone)]
pub struct CommandTask {
pub base: String,
pub variables: Vec<CommandTaskVariable>,
}

impl Display for CommandTask {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.base)
}
}

#[derive(Debug)]
struct CommandTaskReplaceValuesError;
impl std::error::Error for CommandTaskReplaceValuesError {}
impl fmt::Display for CommandTaskReplaceValuesError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Could not replace the placeholders in the CommandTask string"
)
}
}

impl FromStr for CommandTask {
type Err = CommandTaskParseError;

fn from_str(value: &str) -> Result<Self, Self::Err> {
let template_regex = Regex::new(r"\{\{(.+?)\}\}").unwrap();

let variables = template_regex
.captures_iter(value)
.map(|template_data| {
CommandTaskVariable {
name: template_data[1].into(),
// TODO Add default value parsing
default_value: None,
}
})
.collect();

Ok(CommandTask {
base: value.into(),
variables: variables,
})
}
}

impl CommandTask {
pub fn to_executable_string(
&self,
variables: &HashMap<String, String>,
) -> Result<String, Box<std::error::Error>> {
let mut output = self.base.clone();
for task_variable in &self.variables {
let value = variables
.get(&task_variable.name)
.ok_or(CommandTaskReplaceValuesError)?;
let match_string = format!("\\{{\\{{{}\\}}\\}}", task_variable.name);
let re = RegexBuilder::new(&match_string).build()?;
output = re.replace_all(&output, value.as_str()).to_string();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

du kannst als replacer eine FnMut(&Captures) -> String angeben, d.h. du könntest alle variablen auf einmal abhandeln (und die gleiche regex wie weiter oben verwenden)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, in that case how would I return the error from the closure?
E.g.

    pub fn to_executable_string(<···>) -> Result<String, Box<std::error::Error>> {
        let template_regex = Regex::new(r"\{\{(.+?)\}\}").unwrap();
        output = template_regex
            .replace_all(&output, |caps: &Captures| {
                variables.get(&caps[0]).ok_or(CommandTaskReplaceValuesError)? // <- Error
            })
            .to_string();
    // <···>

Does not compile because the error now gets returned inside the closure/.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you can't... you could first check that all the variables have values, and then do the actual replacing, though

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it actually possible that variables are missing from the hash map though?

}
Ok(output)
}
}

impl<'de> de::Deserialize<'de> for CommandTask {
fn deserialize<D>(deserializer: D) -> Result<CommandTask, D::Error>
where
D: de::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
FromStr::from_str(&s).map_err(de::Error::custom)
}
}

/**
* A variable value in a command task, to be filled in e.g. by a form
*/
#[derive(Debug, Clone, Deserialize)]
pub struct CommandTaskVariable {
pub name: String,
pub default_value: Option<String>,
}

/**
* Easily displayable command
*/
Expand Down Expand Up @@ -55,7 +162,7 @@ impl From<CommandLeaf> for CommandDisplay {
fn from(node: CommandLeaf) -> Self {
CommandDisplay {
shortcut: node.shortcut,
name: node.name,
name: node.name.to_string(),
}
}
}
Expand Down
Loading