Switch to a plugin system

This commit is contained in:
2023-10-29 18:10:16 +01:00
parent 5d77eace77
commit 0bff173f17
5 changed files with 173 additions and 52 deletions

View File

@ -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)?;
} }
} }
}; };

View File

@ -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(())
}

View File

@ -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
View 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
View 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(())
}