From 0bff173f17640451a0ae2604f6bdaafac334d43b Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 29 Oct 2023 18:10:16 +0100 Subject: [PATCH] Switch to a plugin system --- src/app.rs | 15 +++++--- src/commands/mod.rs | 75 +++++++++++++++----------------------- src/main.rs | 1 + src/plugins/core.rs | 87 +++++++++++++++++++++++++++++++++++++++++++++ src/plugins/mod.rs | 47 ++++++++++++++++++++++++ 5 files changed, 173 insertions(+), 52 deletions(-) create mode 100644 src/plugins/core.rs create mode 100644 src/plugins/mod.rs diff --git a/src/app.rs b/src/app.rs index e6ada9d..40a7472 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,4 @@ -use color_eyre::eyre::Result; +use color_eyre::eyre::{Result, WrapErr}; use crossterm::event::KeyEvent; use ratatui::prelude::Rect; use serde::{Deserialize, Serialize}; @@ -6,6 +6,7 @@ use tokio::sync::mpsc; use crate::{ action::Action, + commands::RataCommands, components::{fps::FpsCounter, home::Home, Component}, config::Config, mode::Mode, @@ -14,6 +15,7 @@ use crate::{ pub struct App { pub config: Config, + pub commands: RataCommands, pub tick_rate: f64, pub frame_rate: f64, pub components: Vec>, @@ -29,16 +31,19 @@ impl App { let fps = FpsCounter::default(); let config = Config::new()?; let mode = Mode::Home; - Ok(Self { + let mut app = Self { tick_rate, frame_rate, components: vec![Box::new(home), Box::new(fps)], should_quit: false, should_suspend: false, config, + commands: RataCommands::new(), mode, 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<()> { @@ -73,7 +78,7 @@ impl App { if let Some(keymap) = self.config.keybindings.get(&self.mode) { if let Some(command_line) = keymap.get(&vec![key]) { log::info!("Got command: {command_line}"); - crate::commands::run_command(command_line, &action_tx)?; + crate::commands::run_command(command_line, &self, &action_tx)?; } else { // If the key was not handled as a single key action, // then consider it for multi-key combinations. @@ -82,7 +87,7 @@ impl App { // Check for multi-key combinations if let Some(command_line) = keymap.get(&self.last_tick_key_events) { log::info!("Got command: {command_line}"); - crate::commands::run_command(command_line, &action_tx)?; + crate::commands::run_command(command_line, &self, &action_tx)?; } } }; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 5e2e4cf..b22c337 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -14,21 +14,35 @@ * along with this program. If not, see . */ +use std::collections::HashMap; + use clap::{ArgMatches, Command, Parser}; use color_eyre::eyre::{anyhow, bail, Result, WrapErr}; use tokio::sync::mpsc; use crate::action::Action; -pub type CommandHandler = fn(&str, &str, &mpsc::UnboundedSender) -> Result<()>; +pub type CommandHandler = Box) -> Result<()>>; -pub struct RataCommand { - pub name: &'static str, - pub help: &'static str, - pub handler: CommandHandler, +pub trait RataCommand { + fn name(&self) -> String; + fn help(&self) -> String; + fn handler(&self) -> CommandHandler; } -inventory::collect!(RataCommand); +pub struct RataCommands(pub HashMap>); + +impl RataCommands { + pub fn new() -> RataCommands { + RataCommands(HashMap::new()) + } + + pub fn register(&mut self, command: Box) { + if let Some(previous_command) = self.0.insert(command.name(), command) { + log::info!("Overriding existing command {}", previous_command.name()); + } + } +} /* pub fn parser() -> Command { @@ -45,7 +59,11 @@ pub fn parser() -> Command { ) }*/ -pub fn run_command(command_line: &str, action_tx: &mpsc::UnboundedSender) -> Result<()> { +pub fn run_command( + command_line: &str, + app: &crate::App, + action_tx: &mpsc::UnboundedSender, +) -> Result<()> { if command_line.bytes().nth(0) != Some(b'/') { bail!("Not a command: {}", command_line); } @@ -55,45 +73,8 @@ pub fn run_command(command_line: &str, action_tx: &mpsc::UnboundedSender None => (&command_line[1..], ""), }; - for command in inventory::iter::() { - if command.name == command_name.to_ascii_lowercase() { - return (command.handler)(command_name, args, action_tx); - } - } - - bail!("Unknown command /{}", command_name) -} - -inventory::submit! { - RataCommand { - name: "quit", - help: "Exits the process", - handler: quit_handler, + match app.commands.0.get(&command_name.to_ascii_lowercase()) { + Some(command) => command.handler()(command_name, args, action_tx), + None => bail!("Unknown command /{}", command_name), } } - -fn quit_handler(_name: &str, _args: &str, action_tx: &mpsc::UnboundedSender) -> 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, -) -> Result<()> { - action_tx - .send(Action::Suspend) - .context("Could not queue suspend action")?; - Ok(()) -} diff --git a/src/main.rs b/src/main.rs index ac464c8..51be876 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ pub mod commands; pub mod components; pub mod config; pub mod mode; +pub mod plugins; pub mod tui; pub mod utils; diff --git a/src/plugins/core.rs b/src/plugins/core.rs new file mode 100644 index 0000000..5b61592 --- /dev/null +++ b/src/plugins/core.rs @@ -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 . + */ + +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> { + 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, app: &mut crate::App) -> Result> { + 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 { + 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") + }) + } +} diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs new file mode 100644 index 0000000..de9f9a9 --- /dev/null +++ b/src/plugins/mod.rs @@ -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 . + */ + +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, app: &mut App) -> Result>; +} + +pub trait Plugin: PrePlugin {} + +pub struct PluginGetter(pub fn() -> Result>); + +inventory::collect!(PluginGetter); + +pub fn load_all(app: &mut App) -> Result<()> { + for plugin_getter in inventory::iter::() { + // 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(()) +}