Skip to content

Commit

Permalink
feat: add script execution support
Browse files Browse the repository at this point in the history
- Added a new feature to execute scripts with a configurable interpreter.
- Updated README.md to include information about the new script feature.
- Modified src/main.rs to handle script execution and added related command-line options.
- Added validation for script configurations and default script shell.
- Improved error handling and debugging options.

Signed-off-by: Chmouel Boudjnah <[email protected]>
  • Loading branch information
chmouel committed Dec 11, 2024
1 parent f8127ce commit 44abbf9
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 25 deletions.
80 changes: 76 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ Raffi is a launcher for the [Fuzzel](https://codeberg.org/dnkl/fuzzel) utility
that uses a YAML configuration file
to define the commands to be executed.

## Features

- Launch applications with custom configurations.
- Support for icons.
- Script execution with a configurable interpreter.

## Installation

### [Binaries](https://github.com/chmouel/raffi/releases)
Expand Down Expand Up @@ -45,6 +51,14 @@ yay -S raffi-bin
nix-shell -p raffi
```

To install Raffi from source, clone the repository and build it using Cargo:

```sh
git clone https://github.com/chmouel/raffi.git
cd raffi
cargo build --release
```

## Usage

You can launch Raffi directly, and it will run the binary and arguments as defined in the [configuration](#configuration).
Expand All @@ -57,6 +71,22 @@ Icon paths are automatically searched on your system and cached. To refresh the
cache, use the `-r/--refresh-cache` option. If you want to have fuzzel running
faster you can use the option `-I/--disable-icons` to disable them.

### Command-line Options

```sh
raffi [OPTIONS]
```

Options:

- `--help`: Print help message.
- `--version`: Print version.
- `--configfile <FILE>`: Specify the config file location.
- `--print-only`: Print the command to stdout, do not run it.
- `--refresh-cache`: Refresh the icon cache.
- `--no-icons`: Do not show icons.
- `--default-script-shell <SHELL>`: Default shell when using scripts (default: `bash`).

### Sway

Here is an example of how to use Raffi with Sway:
Expand Down Expand Up @@ -118,8 +148,38 @@ firefox:
try to use the binary name (optional). Icons are searched in
`/usr/share/icons`, `/usr/share/pixmaps`, `$HOME/.local/share/icons`, or
`$XDG_DATA_HOME` if set and matched to the icon name. The icons paths are
cached for optimisation, use the `-r` option to refresh it. You can also
cached for optimization, use the `-r` option to refresh it. You can also
specify a full path to the icon.
- **script**: See below for more information.
- **disabled**: If set to `true`, the entry will be disabled.

### Script Feature

You can define a script to be executed instead of a binary. The script will be executed using the default script shell `bash` unless you specify another one in `--default-script-shell`.

Here is an example configuration with a script:

```yaml
hello_script:
script: |
echo "hello world and show me your env"
env
description: "Hello Script"
icon: "script"
```

If you want a script running, for example, with `python3`, you can specify it like this:

```yaml
hello_script:
binary: python3
script: |
import os
print("hello world and show me your env")
print(os.environ)
description: "Hello Script"
icon: "script"
```

### Conditions

Expand All @@ -132,7 +192,7 @@ There is limited support for conditions, allowing you to run a command only if a

#### Example

Here is an example of how to use conditions, this will only display the entry
Here is an example of how to use conditions. This will only display the entry
if the `DESKTOP_SESSION` environment variable is set to `GNOME` and the
`WAYLAND_DISPLAY` environment variable is set and the `firefox` binary exists
in the PATH:
Expand All @@ -145,9 +205,21 @@ ifexist: firefox

See the file located in [examples/raffi.yaml](./examples/raffi.yaml) for a more comprehensive example.

## Copyright
## Troubleshooting

### Common Issues

- **Binary not found**: Ensure that the binary specified in the configuration file exists in the PATH.
- **Invalid configuration**: Verify that the YAML configuration file is correctly formatted and all required fields are provided.
- **Icons not displayed**: Ensure that the icon paths are correct and refresh the icon cache using the `--refresh-cache` option if necessary.

### Debugging

Use the `--print-only` option to print the command that will be executed. This can help identify issues with the configuration or command execution.

## License

[Apache-2.0](./LICENSE)
This project is licensed under the MIT License.

## Authors

Expand Down
87 changes: 66 additions & 21 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ struct RaffiConfig {
ifenvnotset: Option<String>,
ifexist: Option<String>,
disabled: Option<bool>,
script: Option<String>,
}

/// Represents the top-level configuration structure.
Expand All @@ -47,6 +48,12 @@ struct Args {
refresh_cache: bool,
#[options(help = "do not show icons", short = "I")]
no_icons: bool,
#[options(
help = "default shell when using scripts",
default = "bash",
short = "P"
)]
default_script_shell: String,
}

/// Get the icon mapping from system directories.
Expand Down Expand Up @@ -78,7 +85,7 @@ fn get_icon_map() -> Result<HashMap<String, String>> {
}

/// Read the configuration file and return a list of RaffiConfig.
fn read_config(filename: &str) -> Result<Vec<RaffiConfig>> {
fn read_config(filename: &str, args: &Args) -> Result<Vec<RaffiConfig>> {
let file = File::open(filename).context(format!("cannot open config file {}", filename))?;
let config: Config =
serde_yaml::from_reader(file).context(format!("cannot parse config file {}", filename))?;
Expand All @@ -88,7 +95,7 @@ fn read_config(filename: &str) -> Result<Vec<RaffiConfig>> {
if value.is_mapping() {
let mut mc: RaffiConfig = serde_yaml::from_value(value.clone())
.context("cannot parse config entry".to_string())?;
if mc.disabled.unwrap_or(false) || !is_valid_config(&mut mc) {
if mc.disabled.unwrap_or(false) || !is_valid_config(&mut mc, args) {
continue;
}
rafficonfigs.push(mc);
Expand All @@ -98,8 +105,13 @@ fn read_config(filename: &str) -> Result<Vec<RaffiConfig>> {
}

/// Validate the RaffiConfig based on various conditions.
fn is_valid_config(mc: &mut RaffiConfig) -> bool {
if let Some(binary) = &mc.binary {
fn is_valid_config(mc: &mut RaffiConfig, args: &Args) -> bool {
if let Some(_script) = &mc.script {
if !find_binary(mc.binary.as_deref().unwrap_or(&args.default_script_shell)) {
return false;
}
mc.binary = Some(args.default_script_shell.clone());
} else if let Some(binary) = &mc.binary {
if !find_binary(binary) {
return false;
}
Expand Down Expand Up @@ -233,10 +245,45 @@ fn make_fuzzel_input(rafficonfigs: &[RaffiConfig], no_icons: bool) -> Result<Str
Ok(ret)
}

/// Execute the chosen command or script.
fn execute_chosen_command(mc: &RaffiConfig, args: &Args, interpreter: &str) -> Result<()> {
if args.print_only {
if let Some(script) = &mc.script {
println!("#!/usr/bin/env {}\n{}", interpreter, script);
} else {
println!(
"{} {}",
mc.binary.as_deref().context("Binary not found")?,
mc.args.as_deref().unwrap_or(&[]).join(" ")
);
}
return Ok(());
}
if let Some(script) = &mc.script {
let mut temp_script =
tempfile::NamedTempFile::new().context("Failed to create temp script file")?;
writeln!(temp_script, "#!/usr/bin/env {}\n{}", interpreter, script)
.context("Failed to write to temp script file")?;
let mut child = Command::new("/usr/bin/env")
.arg(interpreter)
.arg(temp_script.path())
.spawn()
.context("cannot launch script")?;
child.wait().context("cannot wait for child")?;
} else {
let mut child = Command::new(mc.binary.as_deref().context("Binary not found")?)
.args(mc.args.as_deref().unwrap_or(&[]))
.spawn()
.context("cannot launch binary")?;
child.wait().context("cannot wait for child")?;
}
Ok(())
}

/// Main function to execute the program logic.
fn main() -> Result<()> {
let args = Args::parse_args_default_or_exit();
let configfile = args.configfile.unwrap_or_else(|| {
let configfile = args.configfile.clone().unwrap_or_else(|| {
format!(
"{}/raffi/raffi.yaml",
std::env::var("XDG_CONFIG_HOME").unwrap_or_else(|_| format!(
Expand All @@ -247,11 +294,10 @@ fn main() -> Result<()> {
});

if args.refresh_cache {
let icon_map = get_icon_map()?;
save_to_cache_file(&icon_map)?;
refresh_icon_cache()?;
}

let rafficonfigs = read_config(&configfile)?;
let rafficonfigs = read_config(&configfile, &args)?;
let inputs = make_fuzzel_input(&rafficonfigs, args.no_icons)?;
let ret = run_fuzzel_with_input(&inputs)?;
let chosen = ret
Expand All @@ -266,20 +312,19 @@ fn main() -> Result<()> {
.as_deref()
.unwrap_or_else(|| mc.binary.as_deref().unwrap_or("unknown"));
if description == chosen {
if args.print_only {
println!(
"{} {}",
mc.binary.as_deref().context("Binary not found")?,
mc.args.as_deref().unwrap_or(&[]).join(" ")
);
return Ok(());
}
let mut child = Command::new(mc.binary.as_deref().context("Binary not found")?)
.args(mc.args.as_deref().unwrap_or(&[]))
.spawn()
.context("cannot launch binary")?;
child.wait().context("cannot wait for child")?;
let interpreter = mc
.binary
.clone()
.unwrap_or_else(|| args.default_script_shell.clone());
execute_chosen_command(&mc, &args, &interpreter)?;
}
}
Ok(())
}

/// Refresh the icon cache.
fn refresh_icon_cache() -> Result<()> {
let icon_map = get_icon_map()?;
save_to_cache_file(&icon_map)?;
Ok(())
}

0 comments on commit 44abbf9

Please sign in to comment.