Switch to a plugin system
This commit is contained in:
15
src/app.rs
15
src/app.rs
@ -1,4 +1,4 @@
|
|||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::{Result, WrapErr};
|
||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
use ratatui::prelude::Rect;
|
use ratatui::prelude::Rect;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -6,6 +6,7 @@ use tokio::sync::mpsc;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
action::Action,
|
action::Action,
|
||||||
|
commands::RataCommands,
|
||||||
components::{fps::FpsCounter, home::Home, Component},
|
components::{fps::FpsCounter, home::Home, Component},
|
||||||
config::Config,
|
config::Config,
|
||||||
mode::Mode,
|
mode::Mode,
|
||||||
@ -14,6 +15,7 @@ use crate::{
|
|||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
|
pub commands: RataCommands,
|
||||||
pub tick_rate: f64,
|
pub tick_rate: f64,
|
||||||
pub frame_rate: f64,
|
pub frame_rate: f64,
|
||||||
pub components: Vec<Box<dyn Component>>,
|
pub components: Vec<Box<dyn Component>>,
|
||||||
@ -29,16 +31,19 @@ impl App {
|
|||||||
let fps = FpsCounter::default();
|
let fps = FpsCounter::default();
|
||||||
let config = Config::new()?;
|
let config = Config::new()?;
|
||||||
let mode = Mode::Home;
|
let mode = Mode::Home;
|
||||||
Ok(Self {
|
let mut app = Self {
|
||||||
tick_rate,
|
tick_rate,
|
||||||
frame_rate,
|
frame_rate,
|
||||||
components: vec![Box::new(home), Box::new(fps)],
|
components: vec![Box::new(home), Box::new(fps)],
|
||||||
should_quit: false,
|
should_quit: false,
|
||||||
should_suspend: false,
|
should_suspend: false,
|
||||||
config,
|
config,
|
||||||
|
commands: RataCommands::new(),
|
||||||
mode,
|
mode,
|
||||||
last_tick_key_events: Vec::new(),
|
last_tick_key_events: Vec::new(),
|
||||||
})
|
};
|
||||||
|
crate::plugins::load_all(&mut app).context("Could not load plugins")?;
|
||||||
|
Ok(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(&mut self) -> Result<()> {
|
pub async fn run(&mut self) -> Result<()> {
|
||||||
@ -73,7 +78,7 @@ impl App {
|
|||||||
if let Some(keymap) = self.config.keybindings.get(&self.mode) {
|
if let Some(keymap) = self.config.keybindings.get(&self.mode) {
|
||||||
if let Some(command_line) = keymap.get(&vec![key]) {
|
if let Some(command_line) = keymap.get(&vec![key]) {
|
||||||
log::info!("Got command: {command_line}");
|
log::info!("Got command: {command_line}");
|
||||||
crate::commands::run_command(command_line, &action_tx)?;
|
crate::commands::run_command(command_line, &self, &action_tx)?;
|
||||||
} else {
|
} else {
|
||||||
// If the key was not handled as a single key action,
|
// If the key was not handled as a single key action,
|
||||||
// then consider it for multi-key combinations.
|
// then consider it for multi-key combinations.
|
||||||
@ -82,7 +87,7 @@ impl App {
|
|||||||
// Check for multi-key combinations
|
// Check for multi-key combinations
|
||||||
if let Some(command_line) = keymap.get(&self.last_tick_key_events) {
|
if let Some(command_line) = keymap.get(&self.last_tick_key_events) {
|
||||||
log::info!("Got command: {command_line}");
|
log::info!("Got command: {command_line}");
|
||||||
crate::commands::run_command(command_line, &action_tx)?;
|
crate::commands::run_command(command_line, &self, &action_tx)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -14,21 +14,35 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use clap::{ArgMatches, Command, Parser};
|
use clap::{ArgMatches, Command, Parser};
|
||||||
use color_eyre::eyre::{anyhow, bail, Result, WrapErr};
|
use color_eyre::eyre::{anyhow, bail, Result, WrapErr};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::action::Action;
|
use crate::action::Action;
|
||||||
|
|
||||||
pub type CommandHandler = fn(&str, &str, &mpsc::UnboundedSender<Action>) -> Result<()>;
|
pub type CommandHandler = Box<dyn Fn(&str, &str, &mpsc::UnboundedSender<Action>) -> Result<()>>;
|
||||||
|
|
||||||
pub struct RataCommand {
|
pub trait RataCommand {
|
||||||
pub name: &'static str,
|
fn name(&self) -> String;
|
||||||
pub help: &'static str,
|
fn help(&self) -> String;
|
||||||
pub handler: CommandHandler,
|
fn handler(&self) -> CommandHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
inventory::collect!(RataCommand);
|
pub struct RataCommands(pub HashMap<String, Box<dyn RataCommand>>);
|
||||||
|
|
||||||
|
impl RataCommands {
|
||||||
|
pub fn new() -> RataCommands {
|
||||||
|
RataCommands(HashMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(&mut self, command: Box<dyn RataCommand>) {
|
||||||
|
if let Some(previous_command) = self.0.insert(command.name(), command) {
|
||||||
|
log::info!("Overriding existing command {}", previous_command.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
pub fn parser() -> Command {
|
pub fn parser() -> Command {
|
||||||
@ -45,7 +59,11 @@ pub fn parser() -> Command {
|
|||||||
)
|
)
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
pub fn run_command(command_line: &str, action_tx: &mpsc::UnboundedSender<Action>) -> Result<()> {
|
pub fn run_command(
|
||||||
|
command_line: &str,
|
||||||
|
app: &crate::App,
|
||||||
|
action_tx: &mpsc::UnboundedSender<Action>,
|
||||||
|
) -> Result<()> {
|
||||||
if command_line.bytes().nth(0) != Some(b'/') {
|
if command_line.bytes().nth(0) != Some(b'/') {
|
||||||
bail!("Not a command: {}", command_line);
|
bail!("Not a command: {}", command_line);
|
||||||
}
|
}
|
||||||
@ -55,45 +73,8 @@ pub fn run_command(command_line: &str, action_tx: &mpsc::UnboundedSender<Action>
|
|||||||
None => (&command_line[1..], ""),
|
None => (&command_line[1..], ""),
|
||||||
};
|
};
|
||||||
|
|
||||||
for command in inventory::iter::<RataCommand>() {
|
match app.commands.0.get(&command_name.to_ascii_lowercase()) {
|
||||||
if command.name == command_name.to_ascii_lowercase() {
|
Some(command) => command.handler()(command_name, args, action_tx),
|
||||||
return (command.handler)(command_name, args, action_tx);
|
None => bail!("Unknown command /{}", command_name),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("Unknown command /{}", command_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
inventory::submit! {
|
|
||||||
RataCommand {
|
|
||||||
name: "quit",
|
|
||||||
help: "Exits the process",
|
|
||||||
handler: quit_handler,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quit_handler(_name: &str, _args: &str, action_tx: &mpsc::UnboundedSender<Action>) -> Result<()> {
|
|
||||||
action_tx
|
|
||||||
.send(Action::Quit)
|
|
||||||
.context("Could not queue quit action")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
inventory::submit! {
|
|
||||||
RataCommand {
|
|
||||||
name: "suspend",
|
|
||||||
help: "Puts the process in the background",
|
|
||||||
handler: suspend_handler,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn suspend_handler(
|
|
||||||
_name: &str,
|
|
||||||
_args: &str,
|
|
||||||
action_tx: &mpsc::UnboundedSender<Action>,
|
|
||||||
) -> Result<()> {
|
|
||||||
action_tx
|
|
||||||
.send(Action::Suspend)
|
|
||||||
.context("Could not queue suspend action")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
@ -9,6 +9,7 @@ pub mod commands;
|
|||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod mode;
|
pub mod mode;
|
||||||
|
pub mod plugins;
|
||||||
pub mod tui;
|
pub mod tui;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
|
87
src/plugins/core.rs
Normal file
87
src/plugins/core.rs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Valentin Lorentz
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License version 3,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use color_eyre::eyre::{Result, WrapErr};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
use crate::action::Action;
|
||||||
|
use crate::commands::{CommandHandler, RataCommand};
|
||||||
|
use crate::plugins::{Plugin, PluginGetter, PrePlugin};
|
||||||
|
|
||||||
|
inventory::submit! {
|
||||||
|
PluginGetter(get_core)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_core() -> Result<Box<dyn PrePlugin>> {
|
||||||
|
Ok(Box::new(Core {}))
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Core {}
|
||||||
|
|
||||||
|
impl PrePlugin for Core {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"core".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn help(&self) -> String {
|
||||||
|
"core utilities".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(self: Box<Self>, app: &mut crate::App) -> Result<Box<dyn Plugin>> {
|
||||||
|
app.commands.register(ActionCommand::new(
|
||||||
|
"quit",
|
||||||
|
"Exits the process",
|
||||||
|
Action::Quit,
|
||||||
|
));
|
||||||
|
app.commands.register(ActionCommand::new(
|
||||||
|
"suspend",
|
||||||
|
"Puts the process in the background",
|
||||||
|
Action::Suspend,
|
||||||
|
));
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for Core {}
|
||||||
|
|
||||||
|
struct ActionCommand {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub help: &'static str,
|
||||||
|
pub action: Action,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionCommand {
|
||||||
|
pub fn new(name: &'static str, help: &'static str, action: Action) -> Box<dyn RataCommand> {
|
||||||
|
Box::new(ActionCommand { name, help, action })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RataCommand for ActionCommand {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
self.name.to_owned()
|
||||||
|
}
|
||||||
|
fn help(&self) -> String {
|
||||||
|
self.help.to_owned()
|
||||||
|
}
|
||||||
|
fn handler(&self) -> CommandHandler {
|
||||||
|
let action = self.action.clone();
|
||||||
|
Box::new(move |_name, _args, action_tx| {
|
||||||
|
action_tx
|
||||||
|
.send(action.clone())
|
||||||
|
.context("Could not queue quit action")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
47
src/plugins/mod.rs
Normal file
47
src/plugins/mod.rs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Valentin Lorentz
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License version 3,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use color_eyre::eyre::{Result, WrapErr};
|
||||||
|
|
||||||
|
use crate::App;
|
||||||
|
|
||||||
|
pub mod core;
|
||||||
|
|
||||||
|
/// A [`Plugin`] that is not initialized yet
|
||||||
|
pub trait PrePlugin {
|
||||||
|
fn name(&self) -> String;
|
||||||
|
fn help(&self) -> String;
|
||||||
|
fn load(self: Box<Self>, app: &mut App) -> Result<Box<dyn Plugin>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Plugin: PrePlugin {}
|
||||||
|
|
||||||
|
pub struct PluginGetter(pub fn() -> Result<Box<dyn PrePlugin>>);
|
||||||
|
|
||||||
|
inventory::collect!(PluginGetter);
|
||||||
|
|
||||||
|
pub fn load_all(app: &mut App) -> Result<()> {
|
||||||
|
for plugin_getter in inventory::iter::<PluginGetter>() {
|
||||||
|
// TODO: skip failing plugins instead of stopping load altogether
|
||||||
|
let pre_plugin = (plugin_getter.0)().context("Could not get new plugin")?;
|
||||||
|
let name = pre_plugin.name();
|
||||||
|
pre_plugin
|
||||||
|
.load(app)
|
||||||
|
.with_context(|| format!("Could not load plugin {}", name))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Reference in New Issue
Block a user