mirror of
https://github.com/imgurbot12/rmenu.git
synced 2025-02-12 13:15:07 +01:00
Merge pull request #2 from imgurbot12/feat/script
better scripting and plugin support
This commit is contained in:
commit
641b9eddb8
31 changed files with 1359 additions and 488 deletions
|
@ -5,7 +5,6 @@ members = [
|
||||||
"rmenu-plugin",
|
"rmenu-plugin",
|
||||||
"plugin-run",
|
"plugin-run",
|
||||||
"plugin-desktop",
|
"plugin-desktop",
|
||||||
"plugin-audio",
|
|
||||||
"plugin-network",
|
"plugin-network",
|
||||||
"plugin-window",
|
"plugin-window",
|
||||||
]
|
]
|
||||||
|
|
7
Makefile
7
Makefile
|
@ -17,22 +17,25 @@ install: build deploy
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
mkdir -p ${DEST}
|
mkdir -p ${DEST}
|
||||||
|
cp -vfr themes ${DEST}/themes
|
||||||
|
cp -vfr other-plugins/* ${DEST}/.
|
||||||
cp -vf ./target/release/rmenu ${INSTALL}/rmenu
|
cp -vf ./target/release/rmenu ${INSTALL}/rmenu
|
||||||
|
cp -vf ./target/release/rmenu-build ${INSTALL}/rmenu-build
|
||||||
cp -vf ./target/release/desktop ${DEST}/rmenu-desktop
|
cp -vf ./target/release/desktop ${DEST}/rmenu-desktop
|
||||||
cp -vf ./target/release/run ${DEST}/rmenu-run
|
cp -vf ./target/release/run ${DEST}/rmenu-run
|
||||||
cp -vf ./target/release/audio ${DEST}/rmenu-audio
|
|
||||||
cp -vf ./target/release/network ${DEST}/rmenu-network
|
cp -vf ./target/release/network ${DEST}/rmenu-network
|
||||||
cp -vf ./target/release/window ${DEST}/rmenu-window
|
cp -vf ./target/release/window ${DEST}/rmenu-window
|
||||||
cp -vf ./rmenu/public/config.yaml ${DEST}/config.yaml
|
cp -vf ./rmenu/public/config.yaml ${DEST}/config.yaml
|
||||||
|
ln -sf ${DEST}/themes/dark.css ${DEST}/style.css
|
||||||
|
|
||||||
build: build-rmenu build-plugins
|
build: build-rmenu build-plugins
|
||||||
|
|
||||||
build-rmenu:
|
build-rmenu:
|
||||||
${CARGO} build -p rmenu ${FLAGS}
|
${CARGO} build -p rmenu ${FLAGS}
|
||||||
|
${CARGO} build -p rmenu-plugin --bin rmenu-build ${FLAGS}
|
||||||
|
|
||||||
build-plugins:
|
build-plugins:
|
||||||
${CARGO} build -p run ${FLAGS}
|
${CARGO} build -p run ${FLAGS}
|
||||||
${CARGO} build -p desktop ${FLAGS}
|
${CARGO} build -p desktop ${FLAGS}
|
||||||
${CARGO} build -p audio ${FLAGS}
|
|
||||||
${CARGO} build -p network ${FLAGS}
|
${CARGO} build -p network ${FLAGS}
|
||||||
${CARGO} build -p window ${FLAGS}
|
${CARGO} build -p window ${FLAGS}
|
||||||
|
|
20
README.md
20
README.md
|
@ -1,6 +1,12 @@
|
||||||
RMenu
|
RMenu
|
||||||
------
|
------
|
||||||
|
|
||||||
|
<!--
|
||||||
|
TODO: improve documentation:
|
||||||
|
2. explain exchange format and rmenu-build tool
|
||||||
|
3. include more examples including fullscreen/transparent app-window
|
||||||
|
-->
|
||||||
|
|
||||||
Another customizable Application-Launcher written in Rust
|
Another customizable Application-Launcher written in Rust
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
@ -19,7 +25,7 @@ $ make install
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
RMenu Comes with Two Bultin Plugins: "Desktop Run" aka `drun`.
|
RMenu Comes with Two Builtin Plugins: "Desktop Run" aka `drun`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ rmenu -r run
|
$ rmenu -r run
|
||||||
|
@ -55,4 +61,16 @@ Customize the entire app's appearance with CSS. A few [Example Themes](./themes/
|
||||||
are available as reference. To try them out use: `rmenu --css <my-css-theme>`
|
are available as reference. To try them out use: `rmenu --css <my-css-theme>`
|
||||||
or move the css file to `$HOME/.config/rmenu/style.css`
|
or move the css file to `$HOME/.config/rmenu/style.css`
|
||||||
|
|
||||||
|
### Example Screenshots
|
||||||
|
|
||||||
|
#### Launchpad
|
||||||
|
![launchpad](./screenshots/launchpad.png)
|
||||||
|
|
||||||
|
#### Nord
|
||||||
|
![nord](./screenshots/nord.png)
|
||||||
|
|
||||||
|
#### Dark
|
||||||
|
![dark](./screenshots/dark.png)
|
||||||
|
|
||||||
|
#### Solarized
|
||||||
|
![solzarized](./screenshots/solarized.png)
|
||||||
|
|
28
other-plugins/css/powermenu.css
Normal file
28
other-plugins/css/powermenu.css
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 30px;
|
||||||
|
width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
17
other-plugins/pactl-audio.sh
Executable file
17
other-plugins/pactl-audio.sh
Executable file
|
@ -0,0 +1,17 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
get_sinks() {
|
||||||
|
sinks=`pactl list sinks | grep -e 'Sink' -e 'Name' -e 'Description' | nl -s '>'`
|
||||||
|
default=`pactl get-default-sink`
|
||||||
|
for i in `seq 1 3 $(echo "$sinks" | wc -l)`; do
|
||||||
|
sink=`echo "$sinks" | grep "$i>" | cut -d '#' -f2`
|
||||||
|
name=`echo "$sinks" | grep "$(expr $i + 1)>" | cut -d ':' -f2 | xargs echo -n`
|
||||||
|
desc=`echo "$sinks" | grep "$(expr $i + 2)>" | cut -d ':' -f2 | xargs echo -n`
|
||||||
|
if [ "$name" = "$default" ]; then
|
||||||
|
desc="* $desc"
|
||||||
|
fi
|
||||||
|
rmenu-build entry -n "$desc" -a "`rmenu-build action "pactl set-default-sink $sink"`"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
get_sinks
|
74
other-plugins/powermenu.sh
Executable file
74
other-plugins/powermenu.sh
Executable file
|
@ -0,0 +1,74 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
CSS=`realpath "$(dirname $0)/css/powermenu.css"`
|
||||||
|
SELF=`realpath $0`
|
||||||
|
RMENU=${RMENU:-"rmenu"}
|
||||||
|
|
||||||
|
#: desc => generate options for basic operation
|
||||||
|
main_options() {
|
||||||
|
rmenu-build options \
|
||||||
|
-C $CSS \
|
||||||
|
-n ArrowRight -p ArrowLeft \
|
||||||
|
-w 550 -h 150 -M 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#: desc => generate options for confirm operation
|
||||||
|
confirm_options() {
|
||||||
|
rmenu-build options \
|
||||||
|
-C $CSS \
|
||||||
|
-n ArrowRight -p ArrowLeft \
|
||||||
|
-w 300 -h 150 -M 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#: desc => generate confirmation entries
|
||||||
|
#: usage => $cmd $name?
|
||||||
|
confirm() {
|
||||||
|
cmd=$1
|
||||||
|
name="${2:-"Confirm"}"
|
||||||
|
confirm_options
|
||||||
|
rmenu-build entry -n "Cancel" -I "" -a "`rmenu-build action -m echo "$name Cancelled"`"
|
||||||
|
rmenu-build entry -n "$name" -I "" -a "`rmenu-build action "$cmd"`"
|
||||||
|
}
|
||||||
|
|
||||||
|
#: desc => generate non-confirm entry
|
||||||
|
#: usage => $icon $name $cmd
|
||||||
|
gen_direct() {
|
||||||
|
rmenu-build entry -n "$2" -I "$1" -a "`rmenu-build action "$3"`"
|
||||||
|
}
|
||||||
|
|
||||||
|
#: desc => generate confirmation entry
|
||||||
|
#: usage => $icon $name $cmd
|
||||||
|
gen_confirm() {
|
||||||
|
rmenu-build entry -n "$2" -I "$1" -a "`rmenu-build action "$SELF confirm '$2:$3'"`"
|
||||||
|
}
|
||||||
|
|
||||||
|
#: desc => generate action-entry
|
||||||
|
#: usage => $icon $name $command $do-confirm
|
||||||
|
action() {
|
||||||
|
icon="$1"
|
||||||
|
name="$2"
|
||||||
|
cmd="$3"
|
||||||
|
confirm="$4"
|
||||||
|
[ -z "$confirm" ] \
|
||||||
|
&& gen_direct "$icon" "$name" "$cmd" \
|
||||||
|
|| gen_confirm "$icon" "$name" "$cmd"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
"help")
|
||||||
|
echo "usage: $0 <args...>" && exit 1
|
||||||
|
;;
|
||||||
|
"confirm")
|
||||||
|
name=`echo $2 | cut -d ':' -f1`
|
||||||
|
action=`echo $2 | cut -d ':' -f2`
|
||||||
|
confirm "$action" "$name" | $RMENU
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
[ "$1" != "--no-confirm" ] && confirm="Y"
|
||||||
|
main_options
|
||||||
|
action "⏻" "Shutdown" "systemctl poweroff" "$confirm"
|
||||||
|
action "" "Reboot" "systemctl reboot" "$confirm"
|
||||||
|
action "⏾" "Suspend" "systemctl suspend" "$confirm"
|
||||||
|
action "" "Log Out" "sway exit" "$confirm"
|
||||||
|
;;
|
||||||
|
esac
|
|
@ -1,14 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "audio"
|
|
||||||
version = "0.0.1"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
clap = { version = "4.3.21", features = ["derive"] }
|
|
||||||
env_logger = "0.10.0"
|
|
||||||
log = "0.4.19"
|
|
||||||
rmenu-plugin = { version = "0.0.1", path = "../rmenu-plugin" }
|
|
||||||
serde_json = "1.0.104"
|
|
||||||
thiserror = "1.0.44"
|
|
|
@ -1,43 +0,0 @@
|
||||||
use clap::{Parser, Subcommand};
|
|
||||||
use rmenu_plugin::Entry;
|
|
||||||
|
|
||||||
mod pulse;
|
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
|
||||||
pub enum Commands {
|
|
||||||
ListSinks,
|
|
||||||
SetDefaultSink { sink: u32 },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[command(author, version, about, long_about = None)]
|
|
||||||
#[command(propagate_version = true)]
|
|
||||||
pub struct Cli {
|
|
||||||
#[clap(subcommand)]
|
|
||||||
command: Option<Commands>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), pulse::PulseError> {
|
|
||||||
let cli = Cli::parse();
|
|
||||||
let exe = std::env::current_exe()?.to_str().unwrap().to_string();
|
|
||||||
|
|
||||||
let command = cli.command.unwrap_or(Commands::ListSinks);
|
|
||||||
match command {
|
|
||||||
Commands::ListSinks => {
|
|
||||||
let sinks = pulse::get_sinks()?;
|
|
||||||
for sink in sinks {
|
|
||||||
let star = sink.default.then(|| "* ").unwrap_or("");
|
|
||||||
let desc = format!("{star}{}", sink.description);
|
|
||||||
let exec = format!("{exe} set-default-sink {}", sink.index);
|
|
||||||
let entry = Entry::new(&desc, &exec, None);
|
|
||||||
println!("{}", serde_json::to_string(&entry).unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Commands::SetDefaultSink { sink } => {
|
|
||||||
let sinkstr = format!("{sink}");
|
|
||||||
pulse::set_default_sink(&sinkstr)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
use std::io::{BufRead, BufReader};
|
|
||||||
use std::process::{Command, ExitStatus, Stdio};
|
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
static PACTL: &'static str = "pactl";
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum PulseError {
|
|
||||||
#[error("Invalid Status")]
|
|
||||||
InvalidStatus(ExitStatus),
|
|
||||||
#[error("Cannot Read Command Output")]
|
|
||||||
InvalidStdout,
|
|
||||||
#[error("Invalid Output Encoding")]
|
|
||||||
InvalidEncoding(#[from] std::string::FromUtf8Error),
|
|
||||||
#[error("Unexepcted Command Output")]
|
|
||||||
UnexepctedOutput(String),
|
|
||||||
#[error("Command Error")]
|
|
||||||
CommandError(#[from] std::io::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Sink {
|
|
||||||
pub index: u32,
|
|
||||||
pub name: String,
|
|
||||||
pub description: String,
|
|
||||||
pub default: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieve Default Sink
|
|
||||||
pub fn get_default_sink() -> Result<String, PulseError> {
|
|
||||||
let output = Command::new(PACTL).arg("get-default-sink").output()?;
|
|
||||||
if !output.status.success() {
|
|
||||||
return Err(PulseError::InvalidStatus(output.status));
|
|
||||||
}
|
|
||||||
Ok(String::from_utf8(output.stdout)?.trim().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set Default PACTL Sink
|
|
||||||
pub fn set_default_sink(name: &str) -> Result<(), PulseError> {
|
|
||||||
let status = Command::new(PACTL)
|
|
||||||
.args(["set-default-sink", name])
|
|
||||||
.status()?;
|
|
||||||
if !status.success() {
|
|
||||||
return Err(PulseError::InvalidStatus(status));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieve PulseAudio Sinks From PACTL
|
|
||||||
pub fn get_sinks() -> Result<Vec<Sink>, PulseError> {
|
|
||||||
// retrieve default-sink
|
|
||||||
let default = get_default_sink()?;
|
|
||||||
// spawn command and begin reading
|
|
||||||
let mut command = Command::new(PACTL)
|
|
||||||
.args(["list", "sinks"])
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.spawn()?;
|
|
||||||
let stdout = command.stdout.as_mut().ok_or(PulseError::InvalidStdout)?;
|
|
||||||
// collect output into only important lines
|
|
||||||
let mut lines = vec![];
|
|
||||||
for line in BufReader::new(stdout).lines().filter_map(|l| l.ok()) {
|
|
||||||
let line = line.trim_start();
|
|
||||||
if line.starts_with("Sink ")
|
|
||||||
|| line.starts_with("Name: ")
|
|
||||||
|| line.starts_with("Description: ")
|
|
||||||
{
|
|
||||||
lines.push(line.to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ensure status after command completion
|
|
||||||
let status = command.wait()?;
|
|
||||||
if !status.success() {
|
|
||||||
return Err(PulseError::InvalidStatus(status));
|
|
||||||
}
|
|
||||||
// ensure number of lines matches expected
|
|
||||||
if lines.len() == 0 || lines.len() % 3 != 0 {
|
|
||||||
return Err(PulseError::UnexepctedOutput(lines.join("\n")));
|
|
||||||
}
|
|
||||||
// parse details into chunks and generate sinks
|
|
||||||
let mut sinks = vec![];
|
|
||||||
for chunk in lines.chunks(3) {
|
|
||||||
let (_, idx) = chunk[0]
|
|
||||||
.split_once("#")
|
|
||||||
.ok_or_else(|| PulseError::UnexepctedOutput(chunk[0].to_owned()))?;
|
|
||||||
let (_, name) = chunk[1]
|
|
||||||
.split_once(" ")
|
|
||||||
.ok_or_else(|| PulseError::UnexepctedOutput(chunk[1].to_owned()))?;
|
|
||||||
let (_, desc) = chunk[2]
|
|
||||||
.split_once(" ")
|
|
||||||
.ok_or_else(|| PulseError::UnexepctedOutput(chunk[2].to_owned()))?;
|
|
||||||
sinks.push(Sink {
|
|
||||||
index: idx.parse().unwrap(),
|
|
||||||
name: name.to_owned(),
|
|
||||||
description: desc.to_owned(),
|
|
||||||
default: name == default,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(sinks)
|
|
||||||
}
|
|
|
@ -79,6 +79,7 @@ fn parse_desktop(path: &PathBuf, locale: Option<&str>) -> Option<Entry> {
|
||||||
actions,
|
actions,
|
||||||
comment,
|
comment,
|
||||||
icon,
|
icon,
|
||||||
|
icon_alt: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
use glib::translate::FromGlib;
|
|
||||||
|
|
||||||
use async_std::task;
|
use async_std::task;
|
||||||
use futures_channel::oneshot;
|
use futures_channel::oneshot;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
@ -8,6 +6,9 @@ use std::rc::Rc;
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use glib::translate::FromGlib;
|
||||||
|
use glib::Variant;
|
||||||
|
use nm::traits::ObjectExt;
|
||||||
use nm::*;
|
use nm::*;
|
||||||
|
|
||||||
static SCAN_INTERVAL_MS: u64 = 500;
|
static SCAN_INTERVAL_MS: u64 = 500;
|
||||||
|
@ -32,6 +33,7 @@ pub struct AccessPoint {
|
||||||
pub security: String,
|
pub security: String,
|
||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
pub connection: Option<Connection>,
|
pub connection: Option<Connection>,
|
||||||
|
pub dbus_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// SETTING_WIRELESS_MODE
|
// SETTING_WIRELESS_MODE
|
||||||
|
@ -226,6 +228,7 @@ impl Manager {
|
||||||
let active = self.wifi.active_access_point();
|
let active = self.wifi.active_access_point();
|
||||||
for a in self.wifi.access_points() {
|
for a in self.wifi.access_points() {
|
||||||
// retrieve access-point information
|
// retrieve access-point information
|
||||||
|
let path = a.path();
|
||||||
let rate = a.max_bitrate() / 1000;
|
let rate = a.max_bitrate() / 1000;
|
||||||
let signal = a.strength();
|
let signal = a.strength();
|
||||||
let ssid = a
|
let ssid = a
|
||||||
|
@ -271,6 +274,7 @@ impl Manager {
|
||||||
is_active,
|
is_active,
|
||||||
security: security.join(" ").to_owned(),
|
security: security.join(" ").to_owned(),
|
||||||
connection: a.filter_connections(&conns).get(0).cloned(),
|
connection: a.filter_connections(&conns).get(0).cloned(),
|
||||||
|
dbus_path: path.map(|s| s.to_string()),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -295,10 +299,21 @@ impl Manager {
|
||||||
wait_conn(&active_conn, self.timeout).await?;
|
wait_conn(&active_conn, self.timeout).await?;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
// generate options
|
||||||
|
let mut options: BTreeMap<String, Variant> = BTreeMap::new();
|
||||||
|
options.insert("persist".to_string(), "disk".to_variant());
|
||||||
|
options.insert("bind-activation".to_string(), "none".to_variant());
|
||||||
|
// complete connection
|
||||||
|
let glib_opts = options.to_variant();
|
||||||
let conn = new_conn(ap, password)?;
|
let conn = new_conn(ap, password)?;
|
||||||
let active_conn = self
|
let (active_conn, _) = self
|
||||||
.client
|
.client
|
||||||
.add_and_activate_connection_future(Some(&conn), Some(&device), None)
|
.add_and_activate_connection2_future(
|
||||||
|
Some(&conn),
|
||||||
|
Some(&device),
|
||||||
|
ap.dbus_path.as_deref(),
|
||||||
|
&glib_opts,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.context("Failed to add and activate connection")?;
|
.context("Failed to add and activate connection")?;
|
||||||
wait_conn(&active_conn, self.timeout).await?;
|
wait_conn(&active_conn, self.timeout).await?;
|
||||||
|
|
|
@ -5,5 +5,16 @@ edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "rmenu_plugin"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "rmenu-build"
|
||||||
|
path = "src/bin/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bincode = "1.3.3"
|
||||||
|
clap = { version = "4.3.22", features = ["derive"] }
|
||||||
serde = { version = "1.0.171", features = ["derive"] }
|
serde = { version = "1.0.171", features = ["derive"] }
|
||||||
|
serde_json = "1.0.105"
|
||||||
|
|
257
rmenu-plugin/src/bin/main.rs
Normal file
257
rmenu-plugin/src/bin/main.rs
Normal file
|
@ -0,0 +1,257 @@
|
||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use rmenu_plugin::*;
|
||||||
|
|
||||||
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
|
||||||
|
/// Parse Action from JSON
|
||||||
|
fn parse_action(action: &str) -> Result<Action, serde_json::Error> {
|
||||||
|
serde_json::from_str(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: add options struct object that allows for further
|
||||||
|
// dynamic customization of the cli settings.
|
||||||
|
// last instance overwrites previous entries
|
||||||
|
// properties do not override cli-specified values
|
||||||
|
//
|
||||||
|
// priority order:
|
||||||
|
// 1. cli flags
|
||||||
|
// 2. plugin/source latest merged options
|
||||||
|
// 3. configuration settings
|
||||||
|
|
||||||
|
//TODO: add python library to build entries as well
|
||||||
|
|
||||||
|
/// Valid Action Modes
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum ActionMode {
|
||||||
|
Run,
|
||||||
|
Terminal,
|
||||||
|
Echo,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ActionMode {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Run => write!(f, "run"),
|
||||||
|
Self::Terminal => write!(f, "terminal"),
|
||||||
|
Self::Echo => write!(f, "echo"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ActionMode {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"run" => Ok(Self::Run),
|
||||||
|
"terminal" => Ok(Self::Terminal),
|
||||||
|
"echo" => Ok(Self::Echo),
|
||||||
|
_ => Err(format!("Invalid Method: {s:?}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arguents for Action CLI Command
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
struct ActionArgs {
|
||||||
|
/// Set Name of Action
|
||||||
|
#[arg(short, long, default_value_t=String::from("main"))]
|
||||||
|
name: String,
|
||||||
|
/// Set Comment of Action
|
||||||
|
#[arg(short, long)]
|
||||||
|
comment: Option<String>,
|
||||||
|
/// Arguments to run As Action Command
|
||||||
|
#[clap(required = true, value_delimiter = ' ')]
|
||||||
|
args: Vec<String>,
|
||||||
|
/// Action Mode
|
||||||
|
#[arg(short, long, default_value_t=ActionMode::Run)]
|
||||||
|
mode: ActionMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<Action> for ActionArgs {
|
||||||
|
fn into(self) -> Action {
|
||||||
|
let exec = self.args.join(" ");
|
||||||
|
Action {
|
||||||
|
name: self.name,
|
||||||
|
comment: self.comment,
|
||||||
|
exec: match self.mode {
|
||||||
|
ActionMode::Run => Method::Run(exec),
|
||||||
|
ActionMode::Terminal => Method::Terminal(exec),
|
||||||
|
ActionMode::Echo => Method::Echo(exec),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arguments for Entry CLI Command
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
struct EntryArgs {
|
||||||
|
/// Set Name of Entry
|
||||||
|
#[arg(short, long, default_value_t=String::from("main"))]
|
||||||
|
name: String,
|
||||||
|
/// Set Comment of Entry
|
||||||
|
#[arg(short, long)]
|
||||||
|
comment: Option<String>,
|
||||||
|
/// Precomposed Action JSON Objects
|
||||||
|
#[arg(short, long, value_parser=parse_action)]
|
||||||
|
#[clap(required = true)]
|
||||||
|
actions: Vec<Action>,
|
||||||
|
/// Icon Image Path
|
||||||
|
#[arg(short = 'i', long)]
|
||||||
|
icon: Option<String>,
|
||||||
|
/// Alternative Image Text/HTML
|
||||||
|
#[arg(short = 'I', long)]
|
||||||
|
icon_alt: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<Entry> for EntryArgs {
|
||||||
|
fn into(self) -> Entry {
|
||||||
|
Entry {
|
||||||
|
name: self.name,
|
||||||
|
comment: self.comment,
|
||||||
|
actions: self.actions,
|
||||||
|
icon: self.icon,
|
||||||
|
icon_alt: self.icon_alt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arguments for Options CLI Command
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
struct OptionArgs {
|
||||||
|
/// Override Applicaiton Theme
|
||||||
|
#[arg(short = 'C', long)]
|
||||||
|
pub css: Option<String>,
|
||||||
|
#[arg(short = 's', long)]
|
||||||
|
pub page_size: Option<usize>,
|
||||||
|
#[arg(short = 'l', long)]
|
||||||
|
pub page_load: Option<f64>,
|
||||||
|
#[arg(short = 'd', long)]
|
||||||
|
pub jump_dist: Option<usize>,
|
||||||
|
// search settings
|
||||||
|
/// Override Default Placeholder
|
||||||
|
#[arg(short = 'P', long)]
|
||||||
|
pub placeholder: Option<String>,
|
||||||
|
/// Override Search Restriction
|
||||||
|
#[arg(short = 'r', long)]
|
||||||
|
pub search_restrict: Option<String>,
|
||||||
|
/// Override Minimum Search Length
|
||||||
|
#[arg(short = 'm', long)]
|
||||||
|
pub search_min_length: Option<usize>,
|
||||||
|
/// Override Maximum Search Length
|
||||||
|
#[arg(short = 'M', long)]
|
||||||
|
pub search_max_length: Option<usize>,
|
||||||
|
// key settings
|
||||||
|
/// Override Execution Keybinds
|
||||||
|
#[arg(short = 'e', long)]
|
||||||
|
pub key_exec: Option<Vec<String>>,
|
||||||
|
/// Override Program-Exit Keybinds
|
||||||
|
#[arg(short = 'E', long)]
|
||||||
|
pub key_exit: Option<Vec<String>>,
|
||||||
|
/// Override Move-Next Keybinds
|
||||||
|
#[arg(short = 'n', long)]
|
||||||
|
pub key_move_next: Option<Vec<String>>,
|
||||||
|
/// Override Move-Previous Keybinds
|
||||||
|
#[arg(short = 'p', long)]
|
||||||
|
pub key_move_prev: Option<Vec<String>>,
|
||||||
|
/// Override Open-Menu Keybinds
|
||||||
|
#[arg(short = 'o', long)]
|
||||||
|
pub key_open_menu: Option<Vec<String>>,
|
||||||
|
/// Override Close-Menu Keybinds
|
||||||
|
#[arg(short = 'c', long)]
|
||||||
|
pub key_close_menu: Option<Vec<String>>,
|
||||||
|
/// Override Jump-Next Keybinds
|
||||||
|
#[arg(short = 'j', long)]
|
||||||
|
pub key_jump_next: Option<Vec<String>>,
|
||||||
|
/// Override Jump-Previous Keybinds
|
||||||
|
#[arg(short = 'J', long)]
|
||||||
|
pub key_jump_prev: Option<Vec<String>>,
|
||||||
|
// window settings
|
||||||
|
/// Override Window Title
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub title: Option<String>,
|
||||||
|
/// Override Window Deocration Settings
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub deocorate: Option<bool>,
|
||||||
|
/// Override Window Fullscreen Settings
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub fullscreen: Option<bool>,
|
||||||
|
/// Override Window Tranparent Settings
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub transparent: Option<bool>,
|
||||||
|
/// Override Window Width
|
||||||
|
#[arg(short = 'w', long)]
|
||||||
|
pub window_width: Option<f64>,
|
||||||
|
/// Override Window Height
|
||||||
|
#[arg(short = 'h', long)]
|
||||||
|
pub window_height: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<Options> for OptionArgs {
|
||||||
|
fn into(self) -> Options {
|
||||||
|
Options {
|
||||||
|
css: self.css,
|
||||||
|
page_size: self.page_size,
|
||||||
|
page_load: self.page_load,
|
||||||
|
jump_dist: self.jump_dist,
|
||||||
|
placeholder: self.placeholder,
|
||||||
|
search_restrict: self.search_restrict,
|
||||||
|
search_min_length: self.search_min_length,
|
||||||
|
search_max_length: self.search_max_length,
|
||||||
|
key_exec: self.key_exec,
|
||||||
|
key_exit: self.key_exit,
|
||||||
|
key_move_next: self.key_move_next,
|
||||||
|
key_move_prev: self.key_move_prev,
|
||||||
|
key_open_menu: self.key_open_menu,
|
||||||
|
key_close_menu: self.key_close_menu,
|
||||||
|
key_jump_next: self.key_jump_next,
|
||||||
|
key_jump_prev: self.key_jump_prev,
|
||||||
|
title: self.title,
|
||||||
|
decorate: self.deocorate,
|
||||||
|
fullscreen: self.fullscreen,
|
||||||
|
transparent: self.transparent,
|
||||||
|
window_width: self.window_width,
|
||||||
|
window_height: self.window_height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Valid CLI Commands and their Arguments
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
enum Command {
|
||||||
|
/// Generate Complete RMenu Entry
|
||||||
|
Entry(EntryArgs),
|
||||||
|
/// Generate RMenu Entry Action Object
|
||||||
|
Action(ActionArgs),
|
||||||
|
/// Generate RMenu Options Settings
|
||||||
|
Options(OptionArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
#[command(propagate_version = true)]
|
||||||
|
struct Cli {
|
||||||
|
/// Generate an Entry/Action Object
|
||||||
|
#[clap(subcommand)]
|
||||||
|
command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
let result = match cli.command {
|
||||||
|
Command::Entry(args) => {
|
||||||
|
let entry: Entry = args.into();
|
||||||
|
serde_json::to_string(&entry)
|
||||||
|
}
|
||||||
|
Command::Action(args) => {
|
||||||
|
let action: Action = args.into();
|
||||||
|
serde_json::to_string(&action)
|
||||||
|
}
|
||||||
|
Command::Options(args) => {
|
||||||
|
let options: Options = args.into();
|
||||||
|
serde_json::to_string(&options)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
println!("{}", result.expect("Serialization Failed"));
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
|
//! RMenu-Plugin Object Implementations
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
/// Methods allowed to Execute Actions on Selection
|
||||||
|
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum Method {
|
pub enum Method {
|
||||||
Terminal(String),
|
Terminal(String),
|
||||||
|
@ -9,6 +11,7 @@ pub enum Method {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Method {
|
impl Method {
|
||||||
|
/// Generate the Required Method from a Function
|
||||||
pub fn new(exec: String, terminal: bool) -> Self {
|
pub fn new(exec: String, terminal: bool) -> Self {
|
||||||
match terminal {
|
match terminal {
|
||||||
true => Self::Terminal(exec),
|
true => Self::Terminal(exec),
|
||||||
|
@ -17,7 +20,8 @@ impl Method {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
/// RMenu Entry Action Definition
|
||||||
|
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||||
pub struct Action {
|
pub struct Action {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub exec: Method,
|
pub exec: Method,
|
||||||
|
@ -25,13 +29,15 @@ pub struct Action {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Action {
|
impl Action {
|
||||||
pub fn new(exec: &str) -> Self {
|
/// Generate a simple Execution Action
|
||||||
|
pub fn exec(exec: &str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: "main".to_string(),
|
name: "main".to_string(),
|
||||||
exec: Method::Run(exec.to_string()),
|
exec: Method::Run(exec.to_string()),
|
||||||
comment: None,
|
comment: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// Generate a simple Echo Action
|
||||||
pub fn echo(echo: &str) -> Self {
|
pub fn echo(echo: &str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: "main".to_string(),
|
name: "main".to_string(),
|
||||||
|
@ -41,30 +47,108 @@ impl Action {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
/// RMenu Menu-Entry Implementation
|
||||||
|
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type", rename = "entry")]
|
||||||
pub struct Entry {
|
pub struct Entry {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub actions: Vec<Action>,
|
pub actions: Vec<Action>,
|
||||||
pub comment: Option<String>,
|
pub comment: Option<String>,
|
||||||
pub icon: Option<String>,
|
pub icon: Option<String>,
|
||||||
|
pub icon_alt: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entry {
|
impl Entry {
|
||||||
|
/// Generate a simplified Exec Action Entry
|
||||||
pub fn new(name: &str, action: &str, comment: Option<&str>) -> Self {
|
pub fn new(name: &str, action: &str, comment: Option<&str>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: name.to_owned(),
|
name: name.to_owned(),
|
||||||
actions: vec![Action::new(action)],
|
actions: vec![Action::exec(action)],
|
||||||
comment: comment.map(|c| c.to_owned()),
|
comment: comment.map(|c| c.to_owned()),
|
||||||
icon: Default::default(),
|
icon: Default::default(),
|
||||||
|
icon_alt: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// Generate a simplified Echo Action Entry
|
||||||
pub fn echo(echo: &str, comment: Option<&str>) -> Self {
|
pub fn echo(echo: &str, comment: Option<&str>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: echo.to_owned(),
|
name: echo.to_owned(),
|
||||||
actions: vec![Action::echo(echo)],
|
actions: vec![Action::echo(echo)],
|
||||||
comment: comment.map(|c| c.to_owned()),
|
comment: comment.map(|c| c.to_owned()),
|
||||||
icon: Default::default(),
|
icon: Default::default(),
|
||||||
|
icon_alt: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Additional Plugin Option Overrides
|
||||||
|
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(default, tag = "type", rename = "options")]
|
||||||
|
pub struct Options {
|
||||||
|
// base settings
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub css: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub page_size: Option<usize>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub page_load: Option<f64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub jump_dist: Option<usize>,
|
||||||
|
// search settings
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub placeholder: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub search_restrict: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub search_min_length: Option<usize>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub search_max_length: Option<usize>,
|
||||||
|
// key settings
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub key_exec: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub key_exit: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub key_move_next: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub key_move_prev: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub key_open_menu: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub key_close_menu: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub key_jump_next: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub key_jump_prev: Option<Vec<String>>,
|
||||||
|
// window settings
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub title: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub decorate: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub transparent: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub fullscreen: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub window_width: Option<f64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub window_height: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Valid RMenu Plugin Messages
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "lowercase")]
|
||||||
|
pub enum Message {
|
||||||
|
Entry(Entry),
|
||||||
|
Options(Options),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve EXE of Self
|
||||||
|
#[inline]
|
||||||
|
pub fn self_exe() -> String {
|
||||||
|
std::env::current_exe()
|
||||||
|
.expect("Cannot Find EXE of Self")
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bincode = "1.3.3"
|
|
||||||
cached = "0.44.0"
|
cached = "0.44.0"
|
||||||
clap = { version = "4.3.15", features = ["derive"] }
|
clap = { version = "4.3.15", features = ["derive"] }
|
||||||
dioxus = "0.4.0"
|
dioxus = "0.4.0"
|
||||||
|
|
|
@ -25,10 +25,16 @@ plugins:
|
||||||
drun:
|
drun:
|
||||||
exec: ["~/.config/rmenu/rmenu-desktop"]
|
exec: ["~/.config/rmenu/rmenu-desktop"]
|
||||||
cache: onlogin
|
cache: onlogin
|
||||||
audio:
|
options:
|
||||||
exec: ["~/.config/rmenu/rmenu-audio"]
|
css: ~/.config/rmenu/themes/launchpad.css
|
||||||
cache: false
|
page_size: 500
|
||||||
placeholder: "Select an Audio Sink"
|
transparent: true
|
||||||
|
window_width: 1200
|
||||||
|
window_height: 800
|
||||||
|
key_move_prev: ["Arrow-Left"]
|
||||||
|
key_move_next: ["Arrow-Right"]
|
||||||
|
key_jump_prev: ["Arrow-Up"]
|
||||||
|
key_jump_next: ["Arrow-Down"]
|
||||||
network:
|
network:
|
||||||
exec: ["~/.config/rmenu/rmenu-network"]
|
exec: ["~/.config/rmenu/rmenu-network"]
|
||||||
cache: false
|
cache: false
|
||||||
|
@ -37,12 +43,21 @@ plugins:
|
||||||
exec: ["~/.config/rmenu/rmenu-window"]
|
exec: ["~/.config/rmenu/rmenu-window"]
|
||||||
cache: false
|
cache: false
|
||||||
placeholder: "Jump to the Specified Window"
|
placeholder: "Jump to the Specified Window"
|
||||||
|
audio:
|
||||||
|
exec: ["~/.config/rmenu/pactl-audio.sh"]
|
||||||
|
cache: false
|
||||||
|
placeholder: "Select an Audio Sink"
|
||||||
|
powermenu:
|
||||||
|
exec: ["~/.config/rmenu/powermenu.sh"]
|
||||||
|
cache: false
|
||||||
|
|
||||||
# custom keybindings
|
# custom keybindings
|
||||||
keybinds:
|
keybinds:
|
||||||
exec: ["Enter"]
|
exec: ["Enter"]
|
||||||
exit: ["Escape"]
|
exit: ["Escape"]
|
||||||
move_up: ["Arrow-Up", "Shift+Tab"]
|
move_next: ["Arrow-Down", "Tab"]
|
||||||
move_down: ["Arrow-Down", "Tab"]
|
move_prev: ["Arrow-Up", "Shift+Tab"]
|
||||||
|
jump_next: ["Page-Down"]
|
||||||
|
jump_prev: ["Page-Up"]
|
||||||
open_menu: ["Arrow-Right"]
|
open_menu: ["Arrow-Right"]
|
||||||
close_menu: ["Arrow-Left"]
|
close_menu: ["Arrow-Left"]
|
||||||
|
|
|
@ -8,6 +8,12 @@ body > div {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
.content {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -28,8 +34,12 @@ body > div {
|
||||||
|
|
||||||
/* Navigation */
|
/* Navigation */
|
||||||
|
|
||||||
|
#search:invalid {
|
||||||
|
border: 1px solid red;
|
||||||
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
width: -webkit-fill-available;
|
||||||
height: 5vw;
|
height: 5vw;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
@ -49,24 +59,24 @@ input {
|
||||||
margin: 2px 5px;
|
margin: 2px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result > .icon {
|
.icon {
|
||||||
width: 4%;
|
width: 4%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result > .icon > img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result > .name {
|
.name {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result > .comment {
|
.comment {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
//! RMenu Plugin Result Cache
|
//! RMenu Plugin Result Cache
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Write;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
|
@ -25,7 +24,7 @@ pub enum CacheError {
|
||||||
#[error("Cache File Error")]
|
#[error("Cache File Error")]
|
||||||
FileError(#[from] std::io::Error),
|
FileError(#[from] std::io::Error),
|
||||||
#[error("Encoding Error")]
|
#[error("Encoding Error")]
|
||||||
EncodingError(#[from] bincode::Error),
|
EncodingError(#[from] serde_json::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -69,7 +68,7 @@ pub fn read_cache(name: &str, cfg: &PluginConfig) -> Result<Vec<Entry>, CacheErr
|
||||||
}
|
}
|
||||||
// attempt to read content
|
// attempt to read content
|
||||||
let data = fs::read(path)?;
|
let data = fs::read(path)?;
|
||||||
let results: Vec<Entry> = bincode::deserialize(&data)?;
|
let results: Vec<Entry> = serde_json::from_slice(&data)?;
|
||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,10 +78,10 @@ pub fn write_cache(name: &str, cfg: &PluginConfig, entries: &Vec<Entry>) -> Resu
|
||||||
match cfg.cache {
|
match cfg.cache {
|
||||||
CacheSetting::NoCache => {}
|
CacheSetting::NoCache => {}
|
||||||
_ => {
|
_ => {
|
||||||
|
log::debug!("{name:?} writing {} entries", entries.len());
|
||||||
let path = cache_file(name);
|
let path = cache_file(name);
|
||||||
let data = bincode::serialize(entries)?;
|
let f = fs::File::create(path)?;
|
||||||
let mut f = fs::File::create(path)?;
|
serde_json::to_writer(f, entries)?;
|
||||||
f.write_all(&data)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
379
rmenu/src/cli.rs
Normal file
379
rmenu/src/cli.rs
Normal file
|
@ -0,0 +1,379 @@
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufRead, BufReader, Read};
|
||||||
|
use std::process::{Command, ExitStatus, Stdio};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::{fmt::Display, fs::read_to_string};
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use rmenu_plugin::{Entry, Message};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::config::{cfg_replace, Config, Keybind};
|
||||||
|
use crate::{DEFAULT_CONFIG, DEFAULT_CSS};
|
||||||
|
|
||||||
|
/// Allowed Formats for Entry Ingestion
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Format {
|
||||||
|
Json,
|
||||||
|
DMenu,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Format {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&format!("{self:?}").to_lowercase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Format {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||||
|
match s.to_ascii_lowercase().as_str() {
|
||||||
|
"json" => Ok(Format::Json),
|
||||||
|
"dmenu" => Ok(Format::DMenu),
|
||||||
|
_ => Err("No Such Format".to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dynamic Applicaiton-Menu Tool (Built with Rust)
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
#[command(propagate_version = true)]
|
||||||
|
pub struct Args {
|
||||||
|
// simple configuration arguments
|
||||||
|
/// Filepath for entry input
|
||||||
|
#[arg(short, long)]
|
||||||
|
input: Option<String>,
|
||||||
|
/// Format to accept entries
|
||||||
|
#[arg(short, long, default_value_t=Format::Json)]
|
||||||
|
format: Format,
|
||||||
|
/// Plugins to run
|
||||||
|
#[arg(short, long)]
|
||||||
|
run: Vec<String>,
|
||||||
|
/// Override default configuration path
|
||||||
|
#[arg(short, long)]
|
||||||
|
config: Option<String>,
|
||||||
|
/// Override base css theme styling
|
||||||
|
#[arg(long, default_value_t=String::from(DEFAULT_CSS))]
|
||||||
|
theme: String,
|
||||||
|
/// Include additional css settings
|
||||||
|
#[arg(long)]
|
||||||
|
css: Option<String>,
|
||||||
|
|
||||||
|
// root config settings
|
||||||
|
/// Override terminal command
|
||||||
|
#[arg(long)]
|
||||||
|
terminal: Option<String>,
|
||||||
|
/// Number of results to include for each page
|
||||||
|
#[arg(long)]
|
||||||
|
page_size: Option<usize>,
|
||||||
|
/// Control ratio on when to load next page
|
||||||
|
#[arg(long)]
|
||||||
|
page_load: Option<f64>,
|
||||||
|
/// Force enable/disable comments
|
||||||
|
#[arg(long)]
|
||||||
|
use_icons: Option<bool>,
|
||||||
|
/// Force enable/disable comments
|
||||||
|
#[arg(long)]
|
||||||
|
use_comments: Option<bool>,
|
||||||
|
|
||||||
|
// search settings
|
||||||
|
/// Enforce Regex Pattern on Search
|
||||||
|
#[arg(long)]
|
||||||
|
search_restrict: Option<String>,
|
||||||
|
/// Enforce Minimum Length on Search
|
||||||
|
#[arg(long)]
|
||||||
|
search_min_length: Option<usize>,
|
||||||
|
/// Enforce Maximum Length on Search
|
||||||
|
#[arg(long)]
|
||||||
|
search_max_length: Option<usize>,
|
||||||
|
/// Force enable/disable regex in search
|
||||||
|
#[arg(long)]
|
||||||
|
search_regex: Option<bool>,
|
||||||
|
/// Force enable/disable ignore-case in search
|
||||||
|
#[arg(long)]
|
||||||
|
ignore_case: Option<bool>,
|
||||||
|
/// Override placeholder in searchbar
|
||||||
|
#[arg(short, long)]
|
||||||
|
placeholder: Option<String>,
|
||||||
|
|
||||||
|
// keybinding settings
|
||||||
|
/// Override exec keybind
|
||||||
|
#[arg(long)]
|
||||||
|
key_exec: Option<Vec<Keybind>>,
|
||||||
|
/// Override exit keybind
|
||||||
|
#[arg(long)]
|
||||||
|
key_exit: Option<Vec<Keybind>>,
|
||||||
|
/// Override move-next keybind
|
||||||
|
#[arg(long)]
|
||||||
|
key_move_next: Option<Vec<Keybind>>,
|
||||||
|
/// Override move-previous keybind
|
||||||
|
#[arg(long)]
|
||||||
|
key_move_prev: Option<Vec<Keybind>>,
|
||||||
|
/// Override open-menu keybind
|
||||||
|
#[arg(long)]
|
||||||
|
key_open_menu: Option<Vec<Keybind>>,
|
||||||
|
/// Override close-menu keybind
|
||||||
|
#[arg(long)]
|
||||||
|
key_close_menu: Option<Vec<Keybind>>,
|
||||||
|
/// Override jump-next keybind
|
||||||
|
#[arg(long)]
|
||||||
|
key_jump_next: Option<Vec<Keybind>>,
|
||||||
|
/// Override jump-previous keybind
|
||||||
|
#[arg(long)]
|
||||||
|
key_jump_prev: Option<Vec<Keybind>>,
|
||||||
|
|
||||||
|
//window settings
|
||||||
|
/// Override Window Title
|
||||||
|
#[arg(long)]
|
||||||
|
title: Option<String>,
|
||||||
|
/// Override Window Width
|
||||||
|
#[arg(long)]
|
||||||
|
width: Option<f64>,
|
||||||
|
/// Override Window Height
|
||||||
|
#[arg(long)]
|
||||||
|
height: Option<f64>,
|
||||||
|
/// Override Window X Position
|
||||||
|
#[arg(long)]
|
||||||
|
xpos: Option<f64>,
|
||||||
|
/// Override Window Y Position
|
||||||
|
#[arg(long)]
|
||||||
|
ypos: Option<f64>,
|
||||||
|
/// Override Window Focus on Startup
|
||||||
|
#[arg(long)]
|
||||||
|
focus: Option<bool>,
|
||||||
|
/// Override Window Decoration
|
||||||
|
#[arg(long)]
|
||||||
|
decorate: Option<bool>,
|
||||||
|
/// Override Window Transparent
|
||||||
|
#[arg(long)]
|
||||||
|
transparent: Option<bool>,
|
||||||
|
/// Override Window Always-On-Top
|
||||||
|
#[arg(long)]
|
||||||
|
always_top: Option<bool>,
|
||||||
|
/// Override Fullscreen Settings
|
||||||
|
#[arg(long)]
|
||||||
|
fullscreen: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum RMenuError {
|
||||||
|
#[error("Invalid Config")]
|
||||||
|
InvalidConfig(#[from] serde_yaml::Error),
|
||||||
|
#[error("File Error")]
|
||||||
|
FileError(#[from] std::io::Error),
|
||||||
|
#[error("No Such Plugin")]
|
||||||
|
NoSuchPlugin(String),
|
||||||
|
#[error("Invalid Plugin Specified")]
|
||||||
|
InvalidPlugin(String),
|
||||||
|
#[error("Invalid Keybind Definition")]
|
||||||
|
InvalidKeybind(String),
|
||||||
|
#[error("Command Runtime Exception")]
|
||||||
|
CommandError(Option<ExitStatus>),
|
||||||
|
#[error("Invalid JSON Entry Object")]
|
||||||
|
InvalidJson(#[from] serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, RMenuError>;
|
||||||
|
|
||||||
|
impl Args {
|
||||||
|
/// Load Configuration File
|
||||||
|
pub fn get_config(&self) -> Result<Config> {
|
||||||
|
// read configuration
|
||||||
|
let path = self
|
||||||
|
.config
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| v.as_str())
|
||||||
|
.unwrap_or(DEFAULT_CONFIG);
|
||||||
|
let path = shellexpand::tilde(path).to_string();
|
||||||
|
let config: Config = match read_to_string(path) {
|
||||||
|
Ok(content) => serde_yaml::from_str(&content),
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Failed to Load Config: {err:?}");
|
||||||
|
Ok(Config::default())
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update Configuration w/ CLI Specified Settings
|
||||||
|
pub fn update_config(&self, mut config: Config) -> Config {
|
||||||
|
// override basic settings
|
||||||
|
config.terminal = self.terminal.clone().or_else(|| config.terminal);
|
||||||
|
config.page_size = self.page_size.unwrap_or(config.page_size);
|
||||||
|
config.page_load = self.page_load.unwrap_or(config.page_load);
|
||||||
|
config.use_icons = self.use_icons.unwrap_or(config.use_icons);
|
||||||
|
config.use_comments = self.use_icons.unwrap_or(config.use_comments);
|
||||||
|
// override search settings
|
||||||
|
cfg_replace!(config.search.restrict, self.search_restrict);
|
||||||
|
cfg_replace!(config.search.min_length, self.search_min_length);
|
||||||
|
cfg_replace!(config.search.max_length, self.search_max_length);
|
||||||
|
cfg_replace!(config.search.use_regex, self.search_regex, true);
|
||||||
|
cfg_replace!(config.search.ignore_case, self.ignore_case, true);
|
||||||
|
cfg_replace!(config.search.placeholder, self.placeholder);
|
||||||
|
// override keybind settings
|
||||||
|
cfg_replace!(config.keybinds.exec, self.key_exec, true);
|
||||||
|
cfg_replace!(config.keybinds.exit, self.key_exit, true);
|
||||||
|
cfg_replace!(config.keybinds.move_next, self.key_move_next, true);
|
||||||
|
cfg_replace!(config.keybinds.move_prev, self.key_move_prev, true);
|
||||||
|
cfg_replace!(config.keybinds.open_menu, self.key_open_menu, true);
|
||||||
|
cfg_replace!(config.keybinds.close_menu, self.key_close_menu, true);
|
||||||
|
cfg_replace!(config.keybinds.jump_next, self.key_jump_next, true);
|
||||||
|
cfg_replace!(config.keybinds.jump_prev, self.key_jump_prev, true);
|
||||||
|
// override window settings
|
||||||
|
cfg_replace!(config.window.title, self.title, true);
|
||||||
|
cfg_replace!(config.window.size.width, self.width, true);
|
||||||
|
cfg_replace!(config.window.size.height, self.height, true);
|
||||||
|
cfg_replace!(config.window.position.x, self.xpos, true);
|
||||||
|
cfg_replace!(config.window.position.y, self.ypos, true);
|
||||||
|
cfg_replace!(config.window.focus, self.focus, true);
|
||||||
|
cfg_replace!(config.window.decorate, self.decorate, true);
|
||||||
|
cfg_replace!(config.window.transparent, self.transparent, true);
|
||||||
|
cfg_replace!(config.window.always_top, self.always_top, true);
|
||||||
|
cfg_replace!(config.window.fullscreen, self.fullscreen);
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load CSS Theme or Default
|
||||||
|
pub fn get_theme(&self) -> String {
|
||||||
|
let path = shellexpand::tilde(&self.theme).to_string();
|
||||||
|
match read_to_string(&path) {
|
||||||
|
Ok(css) => css,
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Failed to load CSS: {err:?}");
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load Additional CSS or Default
|
||||||
|
pub fn get_css(&self, c: &Config) -> String {
|
||||||
|
if let Some(css) = self.css.as_ref().or(c.css.as_ref()) {
|
||||||
|
let path = shellexpand::tilde(&css).to_string();
|
||||||
|
match read_to_string(&path) {
|
||||||
|
Ok(theme) => return theme,
|
||||||
|
Err(err) => log::error!("Failed to load Theme: {err:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_entries<T: Read>(
|
||||||
|
&mut self,
|
||||||
|
r: BufReader<T>,
|
||||||
|
v: &mut Vec<Entry>,
|
||||||
|
c: &mut Config,
|
||||||
|
) -> Result<()> {
|
||||||
|
for line in r.lines().filter_map(|l| l.ok()) {
|
||||||
|
match &self.format {
|
||||||
|
Format::DMenu => v.push(Entry::echo(line.trim(), None)),
|
||||||
|
Format::Json => {
|
||||||
|
let msg: Message = serde_json::from_str(&line)?;
|
||||||
|
match msg {
|
||||||
|
Message::Entry(entry) => v.push(entry),
|
||||||
|
Message::Options(options) => c
|
||||||
|
.update(&options)
|
||||||
|
.map_err(|s| RMenuError::InvalidKeybind(s))?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read Entries from a Configured Input
|
||||||
|
fn load_input(&mut self, input: &str, config: &mut Config) -> Result<Vec<Entry>> {
|
||||||
|
// retrieve input file
|
||||||
|
let input = if input == "-" { "/dev/stdin" } else { input };
|
||||||
|
let fpath = shellexpand::tilde(input).to_string();
|
||||||
|
// read entries into iterator and collect
|
||||||
|
log::info!("reading from: {fpath:?}");
|
||||||
|
let file = File::open(fpath)?;
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
let mut entries = vec![];
|
||||||
|
self.read_entries(reader, &mut entries, config)?;
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read Entries from a Plugin Source
|
||||||
|
fn load_plugins(&mut self, config: &mut Config) -> Result<Vec<Entry>> {
|
||||||
|
let mut entries = vec![];
|
||||||
|
for name in self.run.clone().into_iter() {
|
||||||
|
// retrieve plugin configuration
|
||||||
|
log::info!("running plugin: {name:?}");
|
||||||
|
let plugin = config
|
||||||
|
.plugins
|
||||||
|
.get(&name)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| RMenuError::NoSuchPlugin(name.to_owned()))?;
|
||||||
|
// update config w/ plugin options when available
|
||||||
|
if let Some(options) = plugin.options.as_ref() {
|
||||||
|
config
|
||||||
|
.update(options)
|
||||||
|
.map_err(|e| RMenuError::InvalidKeybind(e))?;
|
||||||
|
}
|
||||||
|
// read cache when available
|
||||||
|
match crate::cache::read_cache(&name, &plugin) {
|
||||||
|
Err(err) => log::error!("cache read failed: {err:?}"),
|
||||||
|
Ok(cached) => {
|
||||||
|
entries.extend(cached);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// build command arguments
|
||||||
|
let args: Vec<String> = plugin
|
||||||
|
.exec
|
||||||
|
.iter()
|
||||||
|
.map(|s| shellexpand::tilde(s).to_string())
|
||||||
|
.collect();
|
||||||
|
let main = args
|
||||||
|
.get(0)
|
||||||
|
.ok_or_else(|| RMenuError::InvalidPlugin(name.to_owned()))?;
|
||||||
|
// spawn command
|
||||||
|
let mut command = Command::new(main)
|
||||||
|
.args(&args[1..])
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
let stdout = command
|
||||||
|
.stdout
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| RMenuError::CommandError(None))?;
|
||||||
|
// parse and read entries into vector of results
|
||||||
|
let reader = BufReader::new(stdout);
|
||||||
|
let mut entry = vec![];
|
||||||
|
self.read_entries(reader, &mut entry, config)?;
|
||||||
|
let status = command.wait()?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err(RMenuError::CommandError(Some(status)));
|
||||||
|
}
|
||||||
|
// finalize settings and save to cache
|
||||||
|
if config.search.placeholder.is_none() {
|
||||||
|
config.search.placeholder = plugin.placeholder.clone();
|
||||||
|
}
|
||||||
|
match crate::cache::write_cache(&name, &plugin, &entry) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => log::error!("cache write error: {err:?}"),
|
||||||
|
}
|
||||||
|
// write collected entries to main output
|
||||||
|
entries.append(&mut entry);
|
||||||
|
}
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load Entries from Enabled/Configured Entry-Sources
|
||||||
|
pub fn get_entries(&mut self, config: &mut Config) -> Result<Vec<Entry>> {
|
||||||
|
// configure default source if none are given
|
||||||
|
let mut input = self.input.clone();
|
||||||
|
let mut entries = vec![];
|
||||||
|
if input.is_none() && self.run.is_empty() {
|
||||||
|
input = Some("-".to_owned());
|
||||||
|
}
|
||||||
|
// load entries
|
||||||
|
if let Some(input) = input {
|
||||||
|
entries.extend(self.load_input(&input, config)?);
|
||||||
|
}
|
||||||
|
entries.extend(self.load_plugins(config)?);
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,15 @@
|
||||||
//! RMENU Configuration Implementations
|
//! RMENU Configuration Implementations
|
||||||
use heck::AsPascalCase;
|
use heck::AsPascalCase;
|
||||||
use keyboard_types::{Code, Modifiers};
|
use keyboard_types::{Code, Modifiers};
|
||||||
|
use rmenu_plugin::Options;
|
||||||
use serde::{de::Error, Deserialize};
|
use serde::{de::Error, Deserialize};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use dioxus_desktop::tao::dpi::{LogicalPosition, LogicalSize};
|
use dioxus_desktop::tao::{
|
||||||
|
dpi::{LogicalPosition, LogicalSize},
|
||||||
|
window::Fullscreen,
|
||||||
|
};
|
||||||
|
|
||||||
// parse supported modifiers from string
|
// parse supported modifiers from string
|
||||||
fn mod_from_str(s: &str) -> Option<Modifiers> {
|
fn mod_from_str(s: &str) -> Option<Modifiers> {
|
||||||
|
@ -19,7 +23,7 @@ fn mod_from_str(s: &str) -> Option<Modifiers> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Single GUI Keybind for Configuration
|
/// Single GUI Keybind for Configuration
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Keybind {
|
pub struct Keybind {
|
||||||
pub mods: Modifiers,
|
pub mods: Modifiers,
|
||||||
pub key: Code,
|
pub key: Code,
|
||||||
|
@ -80,10 +84,12 @@ impl<'de> Deserialize<'de> for Keybind {
|
||||||
pub struct KeyConfig {
|
pub struct KeyConfig {
|
||||||
pub exec: Vec<Keybind>,
|
pub exec: Vec<Keybind>,
|
||||||
pub exit: Vec<Keybind>,
|
pub exit: Vec<Keybind>,
|
||||||
pub move_up: Vec<Keybind>,
|
pub move_next: Vec<Keybind>,
|
||||||
pub move_down: Vec<Keybind>,
|
pub move_prev: Vec<Keybind>,
|
||||||
pub open_menu: Vec<Keybind>,
|
pub open_menu: Vec<Keybind>,
|
||||||
pub close_menu: Vec<Keybind>,
|
pub close_menu: Vec<Keybind>,
|
||||||
|
pub jump_next: Vec<Keybind>,
|
||||||
|
pub jump_prev: Vec<Keybind>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for KeyConfig {
|
impl Default for KeyConfig {
|
||||||
|
@ -91,10 +97,12 @@ impl Default for KeyConfig {
|
||||||
return Self {
|
return Self {
|
||||||
exec: vec![Keybind::new(Code::Enter)],
|
exec: vec![Keybind::new(Code::Enter)],
|
||||||
exit: vec![Keybind::new(Code::Escape)],
|
exit: vec![Keybind::new(Code::Escape)],
|
||||||
move_up: vec![Keybind::new(Code::ArrowUp)],
|
move_next: vec![Keybind::new(Code::ArrowUp)],
|
||||||
move_down: vec![Keybind::new(Code::ArrowDown)],
|
move_prev: vec![Keybind::new(Code::ArrowDown)],
|
||||||
open_menu: vec![],
|
open_menu: vec![],
|
||||||
close_menu: vec![],
|
close_menu: vec![],
|
||||||
|
jump_next: vec![Keybind::new(Code::PageDown)],
|
||||||
|
jump_prev: vec![Keybind::new(Code::PageUp)],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,13 +113,26 @@ pub struct WindowConfig {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub size: LogicalSize<f64>,
|
pub size: LogicalSize<f64>,
|
||||||
pub position: LogicalPosition<f64>,
|
pub position: LogicalPosition<f64>,
|
||||||
|
#[serde(default = "_true")]
|
||||||
pub focus: bool,
|
pub focus: bool,
|
||||||
pub decorate: bool,
|
pub decorate: bool,
|
||||||
pub transparent: bool,
|
pub transparent: bool,
|
||||||
|
#[serde(default = "_true")]
|
||||||
pub always_top: bool,
|
pub always_top: bool,
|
||||||
|
pub fullscreen: Option<bool>,
|
||||||
pub dark_mode: Option<bool>,
|
pub dark_mode: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WindowConfig {
|
||||||
|
/// Retrieve Desktop Compatabible Fullscreen Settings
|
||||||
|
pub fn get_fullscreen(&self) -> Option<Fullscreen> {
|
||||||
|
self.fullscreen.and_then(|fs| match fs {
|
||||||
|
true => Some(Fullscreen::Borderless(None)),
|
||||||
|
false => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for WindowConfig {
|
impl Default for WindowConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -125,13 +146,14 @@ impl Default for WindowConfig {
|
||||||
decorate: false,
|
decorate: false,
|
||||||
transparent: false,
|
transparent: false,
|
||||||
always_top: true,
|
always_top: true,
|
||||||
|
fullscreen: None,
|
||||||
dark_mode: None,
|
dark_mode: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cache Settings for Configured RMenu Plugins
|
/// Cache Settings for Configured RMenu Plugins
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum CacheSetting {
|
pub enum CacheSetting {
|
||||||
NoCache,
|
NoCache,
|
||||||
Never,
|
Never,
|
||||||
|
@ -173,13 +195,15 @@ impl Default for CacheSetting {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RMenu Data-Source Plugin Configuration
|
/// RMenu Data-Source Plugin Configuration
|
||||||
#[derive(Debug, PartialEq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
pub struct PluginConfig {
|
pub struct PluginConfig {
|
||||||
pub exec: Vec<String>,
|
pub exec: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub cache: CacheSetting,
|
pub cache: CacheSetting,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub placeholder: Option<String>,
|
pub placeholder: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub options: Option<Options>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -187,24 +211,48 @@ fn _true() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct SearchConfig {
|
||||||
|
pub restrict: Option<String>,
|
||||||
|
pub min_length: Option<usize>,
|
||||||
|
pub max_length: Option<usize>,
|
||||||
|
pub placeholder: Option<String>,
|
||||||
|
#[serde(default = "_true")]
|
||||||
|
pub use_regex: bool,
|
||||||
|
#[serde(default = "_true")]
|
||||||
|
pub ignore_case: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SearchConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
restrict: Default::default(),
|
||||||
|
min_length: Default::default(),
|
||||||
|
max_length: Default::default(),
|
||||||
|
placeholder: Default::default(),
|
||||||
|
use_regex: true,
|
||||||
|
ignore_case: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Global RMenu Complete Configuration
|
/// Global RMenu Complete Configuration
|
||||||
#[derive(Debug, PartialEq, Deserialize)]
|
#[derive(Debug, PartialEq, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub page_size: usize,
|
pub page_size: usize,
|
||||||
pub page_load: f64,
|
pub page_load: f64,
|
||||||
|
pub jump_dist: usize,
|
||||||
#[serde(default = "_true")]
|
#[serde(default = "_true")]
|
||||||
pub use_icons: bool,
|
pub use_icons: bool,
|
||||||
#[serde(default = "_true")]
|
#[serde(default = "_true")]
|
||||||
pub use_comments: bool,
|
pub use_comments: bool,
|
||||||
#[serde(default = "_true")]
|
pub search: SearchConfig,
|
||||||
pub search_regex: bool,
|
|
||||||
#[serde(default = "_true")]
|
|
||||||
pub ignore_case: bool,
|
|
||||||
pub placeholder: Option<String>,
|
|
||||||
pub plugins: BTreeMap<String, PluginConfig>,
|
pub plugins: BTreeMap<String, PluginConfig>,
|
||||||
pub keybinds: KeyConfig,
|
pub keybinds: KeyConfig,
|
||||||
pub window: WindowConfig,
|
pub window: WindowConfig,
|
||||||
|
pub css: Option<String>,
|
||||||
pub terminal: Option<String>,
|
pub terminal: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,15 +261,77 @@ impl Default for Config {
|
||||||
Self {
|
Self {
|
||||||
page_size: 50,
|
page_size: 50,
|
||||||
page_load: 0.8,
|
page_load: 0.8,
|
||||||
|
jump_dist: 5,
|
||||||
use_icons: true,
|
use_icons: true,
|
||||||
use_comments: true,
|
use_comments: true,
|
||||||
search_regex: false,
|
search: Default::default(),
|
||||||
ignore_case: true,
|
|
||||||
placeholder: Default::default(),
|
|
||||||
plugins: Default::default(),
|
plugins: Default::default(),
|
||||||
keybinds: Default::default(),
|
keybinds: Default::default(),
|
||||||
window: Default::default(),
|
window: Default::default(),
|
||||||
terminal: Default::default(),
|
css: None,
|
||||||
|
terminal: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! cfg_replace {
|
||||||
|
($key:expr, $repl:expr) => {
|
||||||
|
if $repl.is_some() {
|
||||||
|
$key = $repl.clone();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($key:expr, $repl:expr, true) => {
|
||||||
|
if let Some(value) = $repl.as_ref() {
|
||||||
|
$key = value.to_owned();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! cfg_keybind {
|
||||||
|
($key:expr, $repl:expr) => {
|
||||||
|
if let Some(bind_strings) = $repl.as_ref() {
|
||||||
|
let mut keybinds = vec![];
|
||||||
|
for bind_str in bind_strings.iter() {
|
||||||
|
let bind = Keybind::from_str(bind_str)?;
|
||||||
|
keybinds.push(bind);
|
||||||
|
}
|
||||||
|
$key = keybinds;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) use cfg_keybind;
|
||||||
|
pub(crate) use cfg_replace;
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
/// Update Configuration from Options Object
|
||||||
|
pub fn update(&mut self, options: &Options) -> Result<(), String> {
|
||||||
|
cfg_replace!(self.css, options.css);
|
||||||
|
cfg_replace!(self.page_size, options.page_size, true);
|
||||||
|
cfg_replace!(self.page_load, options.page_load, true);
|
||||||
|
cfg_replace!(self.jump_dist, options.jump_dist, true);
|
||||||
|
// search settings
|
||||||
|
cfg_replace!(self.search.placeholder, options.placeholder);
|
||||||
|
cfg_replace!(self.search.restrict, options.search_restrict);
|
||||||
|
cfg_replace!(self.search.min_length, options.search_min_length);
|
||||||
|
cfg_replace!(self.search.max_length, options.search_max_length);
|
||||||
|
// keybind settings
|
||||||
|
cfg_keybind!(self.keybinds.exec, options.key_exec);
|
||||||
|
cfg_keybind!(self.keybinds.exec, options.key_exec);
|
||||||
|
cfg_keybind!(self.keybinds.exit, options.key_exit);
|
||||||
|
cfg_keybind!(self.keybinds.move_next, options.key_move_next);
|
||||||
|
cfg_keybind!(self.keybinds.move_prev, options.key_move_prev);
|
||||||
|
cfg_keybind!(self.keybinds.open_menu, options.key_open_menu);
|
||||||
|
cfg_keybind!(self.keybinds.close_menu, options.key_close_menu);
|
||||||
|
cfg_keybind!(self.keybinds.jump_next, options.key_jump_next);
|
||||||
|
cfg_keybind!(self.keybinds.jump_prev, options.key_jump_prev);
|
||||||
|
// window settings
|
||||||
|
cfg_replace!(self.window.title, options.title, true);
|
||||||
|
cfg_replace!(self.window.decorate, options.decorate, true);
|
||||||
|
cfg_replace!(self.window.fullscreen, options.fullscreen);
|
||||||
|
cfg_replace!(self.window.transparent, options.transparent, true);
|
||||||
|
cfg_replace!(self.window.size.width, options.window_width, true);
|
||||||
|
cfg_replace!(self.window.size.height, options.window_height, true);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
101
rmenu/src/gui.rs
101
rmenu/src/gui.rs
|
@ -1,5 +1,7 @@
|
||||||
//! RMENU GUI Implementation using Dioxus
|
//! RMENU GUI Implementation using Dioxus
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use keyboard_types::{Code, Modifiers};
|
use keyboard_types::{Code, Modifiers};
|
||||||
use rmenu_plugin::Entry;
|
use rmenu_plugin::Entry;
|
||||||
|
@ -10,7 +12,6 @@ use crate::{App, DEFAULT_CSS_CONTENT};
|
||||||
|
|
||||||
/// spawn and run the app on the configured platform
|
/// spawn and run the app on the configured platform
|
||||||
pub fn run(app: App) {
|
pub fn run(app: App) {
|
||||||
// customize window
|
|
||||||
let theme = match app.config.window.dark_mode {
|
let theme = match app.config.window.dark_mode {
|
||||||
Some(dark) => match dark {
|
Some(dark) => match dark {
|
||||||
true => Some(dioxus_desktop::tao::window::Theme::Dark),
|
true => Some(dioxus_desktop::tao::window::Theme::Dark),
|
||||||
|
@ -26,6 +27,7 @@ pub fn run(app: App) {
|
||||||
.with_decorations(app.config.window.decorate)
|
.with_decorations(app.config.window.decorate)
|
||||||
.with_transparent(app.config.window.transparent)
|
.with_transparent(app.config.window.transparent)
|
||||||
.with_always_on_top(app.config.window.always_top)
|
.with_always_on_top(app.config.window.always_top)
|
||||||
|
.with_fullscreen(app.config.window.get_fullscreen())
|
||||||
.with_theme(theme);
|
.with_theme(theme);
|
||||||
let config = dioxus_desktop::Config::new().with_window(builder);
|
let config = dioxus_desktop::Config::new().with_window(builder);
|
||||||
dioxus_desktop::launch_with_props(App, app, config);
|
dioxus_desktop::launch_with_props(App, app, config);
|
||||||
|
@ -41,21 +43,28 @@ struct GEntry<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn render_comment(comment: Option<&String>) -> String {
|
fn render_comment(comment: Option<&String>) -> &str {
|
||||||
return comment.map(|s| s.as_str()).unwrap_or("").to_string();
|
comment.map(|s| s.as_str()).unwrap_or("")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn render_image<'a, T>(cx: Scope<'a, T>, image: Option<&String>) -> Element<'a> {
|
fn render_image<'a, T>(
|
||||||
|
cx: Scope<'a, T>,
|
||||||
|
image: Option<&String>,
|
||||||
|
alt: Option<&String>,
|
||||||
|
) -> Element<'a> {
|
||||||
if let Some(img) = image {
|
if let Some(img) = image {
|
||||||
if img.ends_with(".svg") {
|
if img.ends_with(".svg") {
|
||||||
if let Some(content) = crate::image::convert_svg(img.to_owned()) {
|
if let Some(content) = crate::image::convert_svg(img.to_owned()) {
|
||||||
return cx.render(rsx! { img { class: "image", src: "{content}" } });
|
return cx.render(rsx! { img { class: "image", src: "{content}" } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cx.render(rsx! { img { class: "image", src: "{img}" } });
|
if crate::image::image_exists(img.to_owned()) {
|
||||||
|
return cx.render(rsx! { img { class: "image", src: "{img}" } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None
|
let alt = alt.map(|s| s.as_str()).unwrap_or_else(|| "?");
|
||||||
|
return cx.render(rsx! { div { class: "icon_alt", dangerous_inner_html: "{alt}" } });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// render a single result entry w/ the given information
|
/// render a single result entry w/ the given information
|
||||||
|
@ -95,7 +104,7 @@ fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> {
|
||||||
ondblclick: |_| cx.props.state.set_event(KeyEvent::Exec),
|
ondblclick: |_| cx.props.state.set_event(KeyEvent::Exec),
|
||||||
div {
|
div {
|
||||||
class: "action-name",
|
class: "action-name",
|
||||||
"{action.name}"
|
dangerous_inner_html: "{action.name}"
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
class: "action-comment",
|
class: "action-comment",
|
||||||
|
@ -117,7 +126,7 @@ fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> {
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
class: "icon",
|
class: "icon",
|
||||||
render_image(cx, cx.props.entry.icon.as_ref())
|
render_image(cx, cx.props.entry.icon.as_ref(), cx.props.entry.icon_alt.as_ref())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -125,17 +134,17 @@ fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> {
|
||||||
true => cx.render(rsx! {
|
true => cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
class: "name",
|
class: "name",
|
||||||
"{cx.props.entry.name}"
|
dangerous_inner_html: "{cx.props.entry.name}"
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
class: "comment",
|
class: "comment",
|
||||||
render_comment(cx.props.entry.comment.as_ref())
|
dangerous_inner_html: render_comment(cx.props.entry.comment.as_ref())
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
false => cx.render(rsx! {
|
false => cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
class: "entry",
|
class: "entry",
|
||||||
"{cx.props.entry.name}"
|
dangerous_inner_html: "{cx.props.entry.name}"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -162,6 +171,12 @@ fn matches(bind: &Vec<Keybind>, mods: &Modifiers, key: &Code) -> bool {
|
||||||
bind.iter().any(|b| mods.contains(b.mods) && &b.key == key)
|
bind.iter().any(|b| mods.contains(b.mods) && &b.key == key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// retrieve string value for display-capable enum
|
||||||
|
#[inline]
|
||||||
|
fn get_str<T: Display>(item: Option<T>) -> String {
|
||||||
|
item.map(|i| i.to_string()).unwrap_or_else(String::new)
|
||||||
|
}
|
||||||
|
|
||||||
/// main application function/loop
|
/// main application function/loop
|
||||||
fn App<'a>(cx: Scope<App>) -> Element {
|
fn App<'a>(cx: Scope<App>) -> Element {
|
||||||
let mut state = AppState::new(cx, cx.props);
|
let mut state = AppState::new(cx, cx.props);
|
||||||
|
@ -176,11 +191,8 @@ fn App<'a>(cx: Scope<App>) -> Element {
|
||||||
|
|
||||||
// generate state tracker instances
|
// generate state tracker instances
|
||||||
let results = state.results(&cx.props.entries);
|
let results = state.results(&cx.props.entries);
|
||||||
let s_updater = state.partial_copy();
|
|
||||||
let k_updater = state.partial_copy();
|
let k_updater = state.partial_copy();
|
||||||
|
let s_updater = state.partial_copy();
|
||||||
//TODO: consider implementing some sort of
|
|
||||||
// action channel reference to pass to keboard events
|
|
||||||
|
|
||||||
// build keyboard actions event handler
|
// build keyboard actions event handler
|
||||||
let keybinds = &cx.props.config.keybinds;
|
let keybinds = &cx.props.config.keybinds;
|
||||||
|
@ -191,14 +203,18 @@ fn App<'a>(cx: Scope<App>) -> Element {
|
||||||
k_updater.set_event(KeyEvent::Exec);
|
k_updater.set_event(KeyEvent::Exec);
|
||||||
} else if matches(&keybinds.exit, &mods, &code) {
|
} else if matches(&keybinds.exit, &mods, &code) {
|
||||||
k_updater.set_event(KeyEvent::Exit);
|
k_updater.set_event(KeyEvent::Exit);
|
||||||
} else if matches(&keybinds.move_up, &mods, &code) {
|
} else if matches(&keybinds.move_next, &mods, &code) {
|
||||||
k_updater.set_event(KeyEvent::ShiftUp);
|
k_updater.set_event(KeyEvent::MoveNext);
|
||||||
} else if matches(&keybinds.move_down, &mods, &code) {
|
} else if matches(&keybinds.move_prev, &mods, &code) {
|
||||||
k_updater.set_event(KeyEvent::ShiftDown);
|
k_updater.set_event(KeyEvent::MovePrev);
|
||||||
} else if matches(&keybinds.open_menu, &mods, &code) {
|
} else if matches(&keybinds.open_menu, &mods, &code) {
|
||||||
k_updater.set_event(KeyEvent::OpenMenu);
|
k_updater.set_event(KeyEvent::OpenMenu);
|
||||||
} else if matches(&keybinds.close_menu, &mods, &code) {
|
} else if matches(&keybinds.close_menu, &mods, &code) {
|
||||||
k_updater.set_event(KeyEvent::CloseMenu);
|
k_updater.set_event(KeyEvent::CloseMenu);
|
||||||
|
} else if matches(&keybinds.jump_next, &mods, &code) {
|
||||||
|
k_updater.set_event(KeyEvent::JumpNext)
|
||||||
|
} else if matches(&keybinds.jump_prev, &mods, &code) {
|
||||||
|
k_updater.set_event(KeyEvent::JumpPrev)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -219,29 +235,46 @@ fn App<'a>(cx: Scope<App>) -> Element {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
// retreive placeholder
|
// get input settings
|
||||||
let placeholder = cx
|
let minlen = get_str(cx.props.config.search.min_length.as_ref());
|
||||||
.props
|
let maxlen = get_str(cx.props.config.search.max_length.as_ref());
|
||||||
.config
|
let placeholder = get_str(cx.props.config.search.placeholder.as_ref());
|
||||||
.placeholder
|
|
||||||
.as_ref()
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.unwrap_or_else(|| "".to_owned());
|
|
||||||
|
|
||||||
// complete final rendering
|
// complete final rendering
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
style { DEFAULT_CSS_CONTENT }
|
style { DEFAULT_CSS_CONTENT }
|
||||||
|
style { "{cx.props.theme}" }
|
||||||
style { "{cx.props.css}" }
|
style { "{cx.props.css}" }
|
||||||
div {
|
div {
|
||||||
// onclick: |_| focus(cx),
|
id: "content",
|
||||||
onkeydown: keyboard_controls,
|
class: "content",
|
||||||
div {
|
div {
|
||||||
|
id: "navbar",
|
||||||
class: "navbar",
|
class: "navbar",
|
||||||
input {
|
match cx.props.config.search.restrict.as_ref() {
|
||||||
id: "search",
|
Some(pattern) => cx.render(rsx! {
|
||||||
value: "{search}",
|
input {
|
||||||
placeholder: "{placeholder}",
|
id: "search",
|
||||||
oninput: move |evt| s_updater.set_search(cx, evt.value.clone()),
|
value: "{search}",
|
||||||
|
pattern: "{pattern}",
|
||||||
|
minlength: "{minlen}",
|
||||||
|
maxlength: "{maxlen}",
|
||||||
|
placeholder: "{placeholder}",
|
||||||
|
oninput: move |e| s_updater.set_search(cx, e.value.clone()),
|
||||||
|
onkeydown: keyboard_controls,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
None => cx.render(rsx! {
|
||||||
|
input {
|
||||||
|
id: "search",
|
||||||
|
value: "{search}",
|
||||||
|
minlength: "{minlen}",
|
||||||
|
maxlength: "{maxlen}",
|
||||||
|
placeholder: "{placeholder}",
|
||||||
|
oninput: move |e| s_updater.set_search(cx, e.value.clone()),
|
||||||
|
onkeydown: keyboard_controls,
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//! GUI Image Processing
|
//! GUI Image Processing
|
||||||
use std::fs::{create_dir_all, read_to_string, write};
|
use std::fs::{create_dir_all, write};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
@ -37,9 +37,9 @@ fn make_temp() -> Result<(), io::Error> {
|
||||||
/// Convert SVG to PNG Image
|
/// Convert SVG to PNG Image
|
||||||
fn svg_to_png(path: &str, dest: &PathBuf, pixels: u32) -> Result<(), SvgError> {
|
fn svg_to_png(path: &str, dest: &PathBuf, pixels: u32) -> Result<(), SvgError> {
|
||||||
// read and convert to resvg document tree
|
// read and convert to resvg document tree
|
||||||
let xml = read_to_string(path)?;
|
let xml = std::fs::read(path)?;
|
||||||
let opt = resvg::usvg::Options::default();
|
let opt = resvg::usvg::Options::default();
|
||||||
let tree = resvg::usvg::Tree::from_str(&xml, &opt)?;
|
let tree = resvg::usvg::Tree::from_data(&xml, &opt)?;
|
||||||
let rtree = resvg::Tree::from_usvg(&tree);
|
let rtree = resvg::Tree::from_usvg(&tree);
|
||||||
// generate pixel-buffer and scale according to size preference
|
// generate pixel-buffer and scale according to size preference
|
||||||
let size = rtree.size.to_int_size();
|
let size = rtree.size.to_int_size();
|
||||||
|
@ -75,3 +75,8 @@ pub fn convert_svg(path: String) -> Option<String> {
|
||||||
}
|
}
|
||||||
Some(new_path.to_str()?.to_string())
|
Some(new_path.to_str()?.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cached]
|
||||||
|
pub fn image_exists(path: String) -> bool {
|
||||||
|
PathBuf::from(path).exists()
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
use std::collections::VecDeque;
|
|
||||||
use std::fmt::Display;
|
|
||||||
use std::fs::{read_to_string, File};
|
|
||||||
use std::io::{self, prelude::*, BufReader};
|
|
||||||
use std::process::{Command, ExitStatus, Stdio};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
mod cache;
|
mod cache;
|
||||||
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
mod exec;
|
mod exec;
|
||||||
mod gui;
|
mod gui;
|
||||||
|
@ -14,252 +8,68 @@ mod search;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use rmenu_plugin::Entry;
|
use rmenu_plugin::{self_exe, Entry};
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
static CONFIG_DIR: &'static str = "~/.config/rmenu/";
|
static CONFIG_DIR: &'static str = "~/.config/rmenu/";
|
||||||
static DEFAULT_CSS: &'static str = "~/.config/rmenu/style.css";
|
static DEFAULT_CSS: &'static str = "~/.config/rmenu/style.css";
|
||||||
static DEFAULT_CONFIG: &'static str = "~/.config/rmenu/config.yaml";
|
static DEFAULT_CONFIG: &'static str = "~/.config/rmenu/config.yaml";
|
||||||
static DEFAULT_CSS_CONTENT: &'static str = include_str!("../public/default.css");
|
static DEFAULT_CSS_CONTENT: &'static str = include_str!("../public/default.css");
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Format {
|
|
||||||
Json,
|
|
||||||
DMenu,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Format {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(&format!("{self:?}").to_lowercase())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Format {
|
|
||||||
type Err = String;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s.to_ascii_lowercase().as_str() {
|
|
||||||
"json" => Ok(Format::Json),
|
|
||||||
"dmenu" => Ok(Format::DMenu),
|
|
||||||
_ => Err("No Such Format".to_owned()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum RMenuError {
|
|
||||||
#[error("$HOME not found")]
|
|
||||||
HomeNotFound,
|
|
||||||
#[error("Invalid Config")]
|
|
||||||
InvalidConfig(#[from] serde_yaml::Error),
|
|
||||||
#[error("File Error")]
|
|
||||||
FileError(#[from] io::Error),
|
|
||||||
#[error("No Such Plugin")]
|
|
||||||
NoSuchPlugin(String),
|
|
||||||
#[error("Invalid Plugin Specified")]
|
|
||||||
InvalidPlugin(String),
|
|
||||||
#[error("Command Runtime Exception")]
|
|
||||||
CommandError(Vec<String>, Option<ExitStatus>),
|
|
||||||
#[error("Invalid JSON Entry Object")]
|
|
||||||
InvalidJson(#[from] serde_json::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Application State for GUI
|
/// Application State for GUI
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
css: String,
|
css: String,
|
||||||
name: String,
|
name: String,
|
||||||
|
theme: String,
|
||||||
entries: Vec<Entry>,
|
entries: Vec<Entry>,
|
||||||
config: config::Config,
|
config: config::Config,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rofi Clone (Built with Rust)
|
//TODO: how should scripting work?
|
||||||
#[derive(Parser, Debug)]
|
// - need a better mechanism for rmenu and another executable to go back and forth
|
||||||
#[command(author, version, about, long_about = None)]
|
// - need some way to preserve settings between executions of rmenu
|
||||||
#[command(propagate_version = true)]
|
// - need some way for plugins to customize configuration according to preference
|
||||||
pub struct Args {
|
|
||||||
#[arg(short, long, default_value_t=String::from("-"))]
|
|
||||||
input: String,
|
|
||||||
#[arg(short, long, default_value_t=Format::Json)]
|
|
||||||
format: Format,
|
|
||||||
#[arg(short, long)]
|
|
||||||
run: Vec<String>,
|
|
||||||
#[arg(long)]
|
|
||||||
regex: Option<bool>,
|
|
||||||
#[arg(short, long)]
|
|
||||||
config: Option<String>,
|
|
||||||
#[arg(long)]
|
|
||||||
css: Option<String>,
|
|
||||||
#[arg(short, long)]
|
|
||||||
placehold: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Args {
|
fn main() -> cli::Result<()> {
|
||||||
/// Load Config based on CLI Settings
|
// export self to environment for other scripts
|
||||||
fn config(&self) -> Result<config::Config, RMenuError> {
|
let exe = self_exe();
|
||||||
let path = match &self.config {
|
std::env::set_var("RMENU", exe);
|
||||||
Some(path) => path.to_owned(),
|
|
||||||
None => shellexpand::tilde(DEFAULT_CONFIG).to_string(),
|
|
||||||
};
|
|
||||||
log::debug!("loading config from {path:?}");
|
|
||||||
let cfg = match read_to_string(path) {
|
|
||||||
Ok(cfg) => cfg,
|
|
||||||
Err(err) => {
|
|
||||||
log::error!("failed to load config: {err:?}");
|
|
||||||
return Ok(config::Config::default());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
serde_yaml::from_str(&cfg).map_err(|e| RMenuError::InvalidConfig(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read single entry from incoming line object
|
|
||||||
fn readentry(&self, cfg: &config::Config, line: &str) -> Result<Entry, RMenuError> {
|
|
||||||
let mut entry = match self.format {
|
|
||||||
Format::Json => serde_json::from_str::<Entry>(line)?,
|
|
||||||
Format::DMenu => Entry::echo(line.trim(), None),
|
|
||||||
};
|
|
||||||
if !cfg.use_icons {
|
|
||||||
entry.icon = None;
|
|
||||||
}
|
|
||||||
Ok(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load Entries From Input (Stdin by Default)
|
|
||||||
fn load_default(&self, cfg: &config::Config) -> Result<Vec<Entry>, RMenuError> {
|
|
||||||
let fpath = match self.input.as_str() {
|
|
||||||
"-" => "/dev/stdin",
|
|
||||||
_ => &self.input,
|
|
||||||
};
|
|
||||||
log::info!("reading from {fpath:?}");
|
|
||||||
let file = File::open(fpath).map_err(|e| RMenuError::FileError(e))?;
|
|
||||||
let reader = BufReader::new(file);
|
|
||||||
let mut entries = vec![];
|
|
||||||
for line in reader.lines() {
|
|
||||||
let entry = self.readentry(cfg, &line?)?;
|
|
||||||
entries.push(entry);
|
|
||||||
}
|
|
||||||
Ok(entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load Entries From Specified Sources
|
|
||||||
fn load_sources(&self, cfg: &mut config::Config) -> Result<Vec<Entry>, RMenuError> {
|
|
||||||
log::debug!("config: {cfg:?}");
|
|
||||||
// execute commands to get a list of entries
|
|
||||||
let mut entries = vec![];
|
|
||||||
for name in self.run.iter() {
|
|
||||||
log::debug!("running plugin: {name}");
|
|
||||||
// retrieve plugin command arguments
|
|
||||||
let plugin = cfg
|
|
||||||
.plugins
|
|
||||||
.get(name)
|
|
||||||
.ok_or_else(|| RMenuError::NoSuchPlugin(name.to_owned()))?;
|
|
||||||
// attempt to read cache rather than run command
|
|
||||||
match cache::read_cache(name, plugin) {
|
|
||||||
Ok(cached) => {
|
|
||||||
entries.extend(cached);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(err) => log::error!("cache read error: {err:?}"),
|
|
||||||
}
|
|
||||||
// build command
|
|
||||||
let mut cmdargs: VecDeque<String> = plugin
|
|
||||||
.exec
|
|
||||||
.iter()
|
|
||||||
.map(|arg| shellexpand::tilde(arg).to_string())
|
|
||||||
.collect();
|
|
||||||
let main = cmdargs
|
|
||||||
.pop_front()
|
|
||||||
.ok_or_else(|| RMenuError::InvalidPlugin(name.to_owned()))?;
|
|
||||||
let mut cmd = Command::new(main);
|
|
||||||
for arg in cmdargs.iter() {
|
|
||||||
cmd.arg(arg);
|
|
||||||
}
|
|
||||||
// spawn command
|
|
||||||
let mut proc = cmd.stdout(Stdio::piped()).spawn()?;
|
|
||||||
let stdout = proc
|
|
||||||
.stdout
|
|
||||||
.as_mut()
|
|
||||||
.ok_or_else(|| RMenuError::CommandError(plugin.exec.clone().into(), None))?;
|
|
||||||
let reader = BufReader::new(stdout);
|
|
||||||
// read output line by line and parse content
|
|
||||||
for line in reader.lines() {
|
|
||||||
let entry = self.readentry(cfg, &line?)?;
|
|
||||||
entries.push(entry);
|
|
||||||
}
|
|
||||||
// check status of command on exit
|
|
||||||
let status = proc.wait()?;
|
|
||||||
if !status.success() {
|
|
||||||
return Err(RMenuError::CommandError(
|
|
||||||
plugin.exec.clone().into(),
|
|
||||||
Some(status.clone()),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
// update placeholder if empty
|
|
||||||
if cfg.placeholder.is_none() {
|
|
||||||
cfg.placeholder = plugin.placeholder.clone();
|
|
||||||
}
|
|
||||||
// write cache for entries collected
|
|
||||||
match cache::write_cache(name, plugin, &entries) {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(err) => log::error!("cache write error: {err:?}"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Ok(entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load Application
|
|
||||||
pub fn parse_app() -> Result<App, RMenuError> {
|
|
||||||
let args = Self::parse();
|
|
||||||
let mut config = args.config()?;
|
|
||||||
// load css files from settings
|
|
||||||
let csspath = args.css.clone().unwrap_or_else(|| DEFAULT_CSS.to_owned());
|
|
||||||
let csspath = shellexpand::tilde(&csspath).to_string();
|
|
||||||
let css = match read_to_string(csspath) {
|
|
||||||
Ok(css) => css,
|
|
||||||
Err(err) => {
|
|
||||||
log::error!("failed to load css: {err:?}");
|
|
||||||
"".to_owned()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// load entries from configured sources
|
|
||||||
let entries = match args.run.len() > 0 {
|
|
||||||
true => args.load_sources(&mut config)?,
|
|
||||||
false => args.load_default(&config)?,
|
|
||||||
};
|
|
||||||
// update configuration based on cli
|
|
||||||
config.use_icons = config.use_icons && entries.iter().any(|e| e.icon.is_some());
|
|
||||||
config.use_comments = config.use_icons && entries.iter().any(|e| e.comment.is_some());
|
|
||||||
config.search_regex = args.regex.unwrap_or(config.search_regex);
|
|
||||||
if args.placehold.is_some() {
|
|
||||||
config.placeholder = args.placehold.clone();
|
|
||||||
};
|
|
||||||
// generate app object
|
|
||||||
return Ok(App {
|
|
||||||
css,
|
|
||||||
name: "rmenu".to_owned(),
|
|
||||||
entries,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: improve search w/ modes?
|
|
||||||
//TODO: improve looks and css
|
|
||||||
|
|
||||||
fn main() -> Result<(), RMenuError> {
|
|
||||||
// enable log and set default level
|
// enable log and set default level
|
||||||
if std::env::var("RUST_LOG").is_err() {
|
if std::env::var("RUST_LOG").is_err() {
|
||||||
std::env::set_var("RUST_LOG", "info");
|
std::env::set_var("RUST_LOG", "info");
|
||||||
}
|
}
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
// parse cli / config / application-settings
|
|
||||||
let app = Args::parse_app()?;
|
// parse cli and retrieve values for app
|
||||||
// change directory to configuration dir
|
let mut cli = cli::Args::parse();
|
||||||
|
let mut config = cli.get_config()?;
|
||||||
|
let entries = cli.get_entries(&mut config)?;
|
||||||
|
let css = cli.get_css(&config);
|
||||||
|
let theme = cli.get_theme();
|
||||||
|
|
||||||
|
// update config based on cli-settings and entries
|
||||||
|
config = cli.update_config(config);
|
||||||
|
config.use_icons = config.use_icons
|
||||||
|
&& entries
|
||||||
|
.iter()
|
||||||
|
.any(|e| e.icon.is_some() || e.icon_alt.is_some());
|
||||||
|
config.use_comments = config.use_comments && entries.iter().any(|e| e.comment.is_some());
|
||||||
|
|
||||||
|
// change directory to config folder
|
||||||
let cfgdir = shellexpand::tilde(CONFIG_DIR).to_string();
|
let cfgdir = shellexpand::tilde(CONFIG_DIR).to_string();
|
||||||
if let Err(err) = std::env::set_current_dir(&cfgdir) {
|
if let Err(err) = std::env::set_current_dir(&cfgdir) {
|
||||||
log::error!("failed to change directory: {err:?}");
|
log::error!("failed to change directory: {err:?}");
|
||||||
}
|
}
|
||||||
// run gui
|
|
||||||
gui::run(app);
|
// genrate app context and run gui
|
||||||
|
gui::run(App {
|
||||||
|
name: "rmenu".to_owned(),
|
||||||
|
css,
|
||||||
|
theme,
|
||||||
|
entries,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,9 @@ use crate::config::Config;
|
||||||
/// Configurtaion Settings and Search-String
|
/// Configurtaion Settings and Search-String
|
||||||
pub fn new_searchfn(cfg: &Config, search: &str) -> Box<dyn Fn(&Entry) -> bool> {
|
pub fn new_searchfn(cfg: &Config, search: &str) -> Box<dyn Fn(&Entry) -> bool> {
|
||||||
// build regex search expression
|
// build regex search expression
|
||||||
if cfg.search_regex {
|
if cfg.search.use_regex {
|
||||||
let rgx = RegexBuilder::new(search)
|
let rgx = RegexBuilder::new(search)
|
||||||
.case_insensitive(cfg.ignore_case)
|
.case_insensitive(cfg.search.ignore_case)
|
||||||
.build();
|
.build();
|
||||||
let Ok(regex) = rgx else {
|
let Ok(regex) = rgx else {
|
||||||
return Box::new(|_| false);
|
return Box::new(|_| false);
|
||||||
|
@ -26,7 +26,7 @@ pub fn new_searchfn(cfg: &Config, search: &str) -> Box<dyn Fn(&Entry) -> bool> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// build case-insensitive search expression
|
// build case-insensitive search expression
|
||||||
if cfg.ignore_case {
|
if cfg.search.ignore_case {
|
||||||
let matchstr = search.to_lowercase();
|
let matchstr = search.to_lowercase();
|
||||||
return Box::new(move |entry: &Entry| {
|
return Box::new(move |entry: &Entry| {
|
||||||
if entry.name.to_lowercase().contains(&matchstr) {
|
if entry.name.to_lowercase().contains(&matchstr) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use dioxus::prelude::{use_eval, use_ref, Scope, UseRef};
|
use dioxus::prelude::{use_eval, use_ref, Scope, UseRef};
|
||||||
|
use regex::Regex;
|
||||||
use rmenu_plugin::Entry;
|
use rmenu_plugin::Entry;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
@ -17,10 +18,12 @@ fn scroll<T>(cx: Scope<T>, pos: usize) {
|
||||||
pub enum KeyEvent {
|
pub enum KeyEvent {
|
||||||
Exec,
|
Exec,
|
||||||
Exit,
|
Exit,
|
||||||
ShiftUp,
|
MovePrev,
|
||||||
ShiftDown,
|
MoveNext,
|
||||||
OpenMenu,
|
OpenMenu,
|
||||||
CloseMenu,
|
CloseMenu,
|
||||||
|
JumpNext,
|
||||||
|
JumpPrev,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct InnerState {
|
pub struct InnerState {
|
||||||
|
@ -29,6 +32,7 @@ pub struct InnerState {
|
||||||
page: usize,
|
page: usize,
|
||||||
search: String,
|
search: String,
|
||||||
event: Option<KeyEvent>,
|
event: Option<KeyEvent>,
|
||||||
|
search_regex: Option<Regex>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InnerState {
|
impl InnerState {
|
||||||
|
@ -44,8 +48,20 @@ impl InnerState {
|
||||||
self.pos = std::cmp::min(self.pos + x, max - 1)
|
self.pos = std::cmp::min(self.pos + x, max - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Jump a spefified number of results upwards
|
||||||
|
#[inline]
|
||||||
|
pub fn jump_up(&mut self, jump: usize) {
|
||||||
|
self.move_up(jump)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Jump a specified number of results downwards
|
||||||
|
pub fn jump_down(&mut self, jump: usize, results: &Vec<&Entry>) {
|
||||||
|
let max = std::cmp::max(results.len(), 1);
|
||||||
|
self.move_down(jump, max);
|
||||||
|
}
|
||||||
|
|
||||||
/// Move Up Once With Context of SubMenu
|
/// Move Up Once With Context of SubMenu
|
||||||
pub fn shift_up(&mut self) {
|
pub fn move_prev(&mut self) {
|
||||||
if self.subpos > 0 {
|
if self.subpos > 0 {
|
||||||
self.subpos -= 1;
|
self.subpos -= 1;
|
||||||
return;
|
return;
|
||||||
|
@ -54,15 +70,14 @@ impl InnerState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move Down Once With Context of SubMenu
|
/// Move Down Once With Context of SubMenu
|
||||||
pub fn shift_down(&mut self, results: &Vec<&Entry>) {
|
pub fn move_next(&mut self, results: &Vec<&Entry>) {
|
||||||
if let Some(result) = results.get(self.pos) {
|
if let Some(result) = results.get(self.pos) {
|
||||||
if self.subpos > 0 && self.subpos < result.actions.len() - 1 {
|
if self.subpos > 0 && self.subpos < result.actions.len() - 1 {
|
||||||
self.subpos += 1;
|
self.subpos += 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let max = std::cmp::max(results.len(), 1);
|
self.jump_down(1, results)
|
||||||
self.move_down(1, max);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +98,21 @@ impl<'a> AppState<'a> {
|
||||||
page: 0,
|
page: 0,
|
||||||
search: "".to_string(),
|
search: "".to_string(),
|
||||||
event: None,
|
event: None,
|
||||||
|
search_regex: app.config.search.restrict.clone().and_then(|mut r| {
|
||||||
|
if !r.starts_with('^') {
|
||||||
|
r = format!("^{r}")
|
||||||
|
};
|
||||||
|
if !r.ends_with('$') {
|
||||||
|
r = format!("{r}$")
|
||||||
|
};
|
||||||
|
match Regex::new(&r) {
|
||||||
|
Ok(regex) => Some(regex),
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Invalid Regex Expression: {:?}", err);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
app,
|
app,
|
||||||
results: vec![],
|
results: vec![],
|
||||||
|
@ -147,13 +177,22 @@ impl<'a> AppState<'a> {
|
||||||
KeyEvent::Exec => self.execute(),
|
KeyEvent::Exec => self.execute(),
|
||||||
KeyEvent::OpenMenu => self.open_menu(),
|
KeyEvent::OpenMenu => self.open_menu(),
|
||||||
KeyEvent::CloseMenu => self.close_menu(),
|
KeyEvent::CloseMenu => self.close_menu(),
|
||||||
KeyEvent::ShiftUp => {
|
KeyEvent::MovePrev => {
|
||||||
self.shift_up();
|
self.move_prev();
|
||||||
let pos = self.position().0;
|
let pos = self.position().0;
|
||||||
scroll(cx, if pos <= 3 { pos } else { pos + 3 })
|
scroll(cx, if pos <= 3 { pos } else { pos + 3 })
|
||||||
}
|
}
|
||||||
KeyEvent::ShiftDown => {
|
KeyEvent::MoveNext => {
|
||||||
self.shift_down();
|
self.move_next();
|
||||||
|
scroll(cx, self.position().0 + 3)
|
||||||
|
}
|
||||||
|
KeyEvent::JumpPrev => {
|
||||||
|
self.jump_prev();
|
||||||
|
let pos = self.position().0;
|
||||||
|
scroll(cx, if pos <= 3 { pos } else { pos + 3 })
|
||||||
|
}
|
||||||
|
KeyEvent::JumpNext => {
|
||||||
|
self.jump_next();
|
||||||
scroll(cx, self.position().0 + 3)
|
scroll(cx, self.position().0 + 3)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -184,6 +223,27 @@ impl<'a> AppState<'a> {
|
||||||
|
|
||||||
/// Update Search and Reset Position
|
/// Update Search and Reset Position
|
||||||
pub fn set_search(&self, cx: Scope<'_, App>, search: String) {
|
pub fn set_search(&self, cx: Scope<'_, App>, search: String) {
|
||||||
|
// confirm search meets required criteria
|
||||||
|
if let Some(min) = self.app.config.search.min_length.as_ref() {
|
||||||
|
if search.len() < *min {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(min) = self.app.config.search.min_length.as_ref() {
|
||||||
|
if search.len() < *min {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let is_match = self.state.with(|s| {
|
||||||
|
s.search_regex
|
||||||
|
.as_ref()
|
||||||
|
.map(|r| r.is_match(&search))
|
||||||
|
.unwrap_or(true)
|
||||||
|
});
|
||||||
|
if !is_match {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// update search w/ new content
|
||||||
self.state.with_mut(|s| {
|
self.state.with_mut(|s| {
|
||||||
s.pos = 0;
|
s.pos = 0;
|
||||||
s.subpos = 0;
|
s.subpos = 0;
|
||||||
|
@ -227,13 +287,28 @@ impl<'a> AppState<'a> {
|
||||||
|
|
||||||
/// Move Up Once With Context of SubMenu
|
/// Move Up Once With Context of SubMenu
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn shift_up(&self) {
|
pub fn move_prev(&self) {
|
||||||
self.state.with_mut(|s| s.shift_up());
|
self.state.with_mut(|s| s.move_prev());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move Down Once With Context of SubMenu
|
/// Move Down Once With Context of SubMenu
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn shift_down(&self) {
|
pub fn move_next(&self) {
|
||||||
self.state.with_mut(|s| s.shift_down(&self.results))
|
self.state.with_mut(|s| s.move_next(&self.results))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Jump a Configured Distance Up the Results
|
||||||
|
#[inline]
|
||||||
|
pub fn jump_prev(&self) {
|
||||||
|
let distance = self.app.config.jump_dist;
|
||||||
|
self.state.with_mut(|s| s.jump_up(distance))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Jump a Configured Distance Down the Results
|
||||||
|
#[inline]
|
||||||
|
pub fn jump_next(&self) {
|
||||||
|
let distance = self.app.config.jump_dist;
|
||||||
|
self.state
|
||||||
|
.with_mut(|s| s.jump_down(distance, &self.results))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
screenshots/dark.png
Normal file
BIN
screenshots/dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
BIN
screenshots/default.png
Normal file
BIN
screenshots/default.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
BIN
screenshots/launchpad.png
Normal file
BIN
screenshots/launchpad.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 391 KiB |
BIN
screenshots/nord.png
Normal file
BIN
screenshots/nord.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9 KiB |
BIN
screenshots/solarized.png
Normal file
BIN
screenshots/solarized.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
86
themes/launchpad.css
Normal file
86
themes/launchpad.css
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
* {
|
||||||
|
font-family: monospace;
|
||||||
|
color: #f5f5f5;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg1: #363636;
|
||||||
|
--bg2: #f5f5f520;
|
||||||
|
--bg3: #f5f5f540;
|
||||||
|
--bg4: #0860f2E6;
|
||||||
|
|
||||||
|
--fg0: #f5f5f5;
|
||||||
|
--fg1: #f5f5f580;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
background-color: #24242480;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 33.333%;
|
||||||
|
height: 1.5rem;
|
||||||
|
margin-left: 33.333%;
|
||||||
|
|
||||||
|
border: 1px;
|
||||||
|
border-color: #f5f5f540;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: #363636;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: #f5f5f520;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
margin: 5vh;
|
||||||
|
height: 90vh;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-entry {
|
||||||
|
display: flex;
|
||||||
|
flex-basis: 20%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 30px;
|
||||||
|
width: 10rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
|
||||||
|
border-radius: 16px;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name,
|
||||||
|
.entry {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
background-color: #0860f2E6;
|
||||||
|
}
|
Loading…
Reference in a new issue