1
0
Fork 0

refactor(src/ui): Allow for a repl

This commit is contained in:
Benedikt Peetz 2024-05-04 17:43:30 +02:00
parent 08c4724a94
commit d76f279a05
18 changed files with 252 additions and 140 deletions

View File

@ -1,14 +1,45 @@
use std::path::PathBuf; use std::path::{Path, PathBuf};
use anyhow::Result; use anyhow::{Context, Result};
use cli_log::info; use cli_log::{info, warn};
use tokio::{fs, sync::mpsc::Sender}; use tokio::{fs, sync::mpsc::Sender};
use crate::app::events::Event; use crate::app::events::Event;
const LUA_CONFIG_FILE_NAME: &str = "init.lua";
pub async fn load(tx: Sender<Event>, path: PathBuf) -> Result<()> { pub async fn load(tx: Sender<Event>, path: PathBuf) -> Result<()> {
info!("Loaded config file at '{}'", path.display());
let lua_config_code = fs::read_to_string(&path).await?; let lua_config_code = fs::read_to_string(&path).await?;
tx.send(Event::LuaCommand(lua_config_code)).await?; tx.send(Event::LuaCommand(lua_config_code)).await?;
info!("Loaded config file at `{}`", path.display());
Ok(()) Ok(())
} }
pub async fn check_for_config_file(config_dir: &Path) -> Result<Option<PathBuf>> {
if config_dir
.try_exists()
.context("Failed to search for a config file")?
{
info!("Found config dir: `{}`", config_dir.display());
let config_file = config_dir.join(LUA_CONFIG_FILE_NAME);
info!("Searching for config file at '{}'", config_file.display());
if config_file.try_exists().with_context(|| {
format!(
"Failed to check, if the lua config file at '{}' exists.",
config_file.display()
)
})? {
info!("Found the config file at: '{}'", config_file.display());
Ok(Some(config_file))
} else {
warn!("No config file found!");
Ok(None)
}
} else {
warn!("No config directory found at {}!", config_dir.display());
Ok(None)
}
}

View File

@ -1 +1,2 @@
pub mod lua; pub mod lua;
pub mod shared_objects;

View File

@ -0,0 +1,22 @@
use std::{ffi::c_int, path::Path};
use anyhow::{Context, Result};
use cli_log::info;
use libloading::{Library, Symbol};
pub fn load(plugin: &Path) -> Result<()> {
info!("Loading a plugin from '{}'", plugin.display());
unsafe {
let lib = Library::new(plugin).context("Failed to load plugin")?;
let func: Symbol<unsafe fn() -> c_int> = lib
.get(b"plugin_main")
.context("Plugin does not have a 'plugin_main' symbol")?;
info!("Starting plugin");
let out = func();
info!("Plugin finished with: {}", out);
}
Ok(())
}

View File

@ -14,7 +14,7 @@ use crate::{
App, App,
}, },
trinitrix::api::ui::Mode, trinitrix::api::ui::Mode,
ui::central::InputPosition, ui::ui_trait::TrinitrixUi,
}; };
use anyhow::Result; use anyhow::Result;
use cli_log::{info, trace, warn}; use cli_log::{info, trace, warn};
@ -25,8 +25,8 @@ use keymaps::{
}; };
use trixy::oneshot; use trixy::oneshot;
pub async fn handle( pub async fn handle<U: TrinitrixUi>(
app: &mut App<'_>, app: &mut App<U>,
command: &Commands, command: &Commands,
// FIXME(@soispha): The `String` is temporary <2024-05-03> // FIXME(@soispha): The `String` is temporary <2024-05-03>
@ -96,24 +96,24 @@ pub async fn handle(
} }
Mode::Insert => { Mode::Insert => {
app.status.set_state(State::Insert); app.status.set_state(State::Insert);
app.ui.set_input_position(InputPosition::MessageCompose);
send_status_output!("Set input mode to Insert"); send_status_output!("Set input mode to Insert");
EventStatus::Ok EventStatus::Ok
} }
Mode::Command => { Mode::Command => {
app.ui.cli_enable();
app.status.set_state(State::Command); app.status.set_state(State::Command);
send_status_output!("Set input mode to CLI"); send_status_output!("Set input mode to CLI");
EventStatus::Ok EventStatus::Ok
} }
}, },
Ui::cycle_planes => { Ui::cycle_planes => {
app.ui.cycle_main_input_position(); // TODO(@soispha): add this <2024-05-04>
// app.ui.cycle_main_input_position();
send_status_output!("Switched main input position"); send_status_output!("Switched main input position");
EventStatus::Ok EventStatus::Ok
} }
Ui::cycle_planes_rev => { Ui::cycle_planes_rev => {
app.ui.cycle_main_input_position_rev(); // TODO(@soispha): and this <2024-05-04>
// app.ui.cycle_main_input_position_rev();
send_status_output!("Switched main input position; reversed"); send_status_output!("Switched main input position; reversed");
EventStatus::Ok EventStatus::Ok
} }
@ -178,32 +178,20 @@ pub async fn handle(
EventStatus::Ok EventStatus::Ok
} }
Raw::send_input_unprocessed { input } => { Raw::send_input_unprocessed { input } => {
let output = match app.status.state() {
State::Insert => {
let key = Key::from_str(input.as_str())?; let key = Key::from_str(input.as_str())?;
let cross_input: Event = key.try_into()?; // let cross_input: Event = key.try_into()?;
app.ui
.message_compose match app.status.state() {
.input(tui_textarea::Input::from(cross_input)); State::Insert | State::Command => {
EventStatus::Ok app.ui.input(key);
}
State::Command => {
let key = Key::from_str(input.as_str())?;
let cross_input: Event = key.try_into()?;
app.ui
.cli
.as_mut()
.expect("This should exist, when the state is 'Command'")
.input(tui_textarea::Input::from(cross_input));
EventStatus::Ok
} }
State::Normal State::Normal
| State::KeyInputPending { | State::KeyInputPending {
old_state: _, old_state: _,
pending_keys: _, pending_keys: _,
} => EventStatus::Ok, } => {}
}; }
output EventStatus::Ok
} }
Raw::Private(private) => { Raw::Private(private) => {
// no-op, this was used to store functions (not so sure, if we need it // no-op, this was used to store functions (not so sure, if we need it

View File

@ -3,7 +3,8 @@ use cli_log::info;
use crossterm::event::Event as CrosstermEvent; use crossterm::event::Event as CrosstermEvent;
use keymaps::key_repr::{Key, Keys}; use keymaps::key_repr::{Key, Keys};
use crate::app::{ use crate::{
app::{
command_interface::{ command_interface::{
trinitrix::{ trinitrix::{
api::{raw::Raw, Api}, api::{raw::Raw, Api},
@ -14,10 +15,19 @@ use crate::app::{
events::{Event, EventStatus}, events::{Event, EventStatus},
status::State, status::State,
App, App,
},
ui::ui_trait::TrinitrixUi,
}; };
pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> { pub async fn handle<U: TrinitrixUi>(
async fn default(converted_key: Key, app: &mut App<'_>, old_state: &State) -> Result<()> { app: &mut App<U>,
input_event: &CrosstermEvent,
) -> Result<EventStatus> {
async fn default<U: TrinitrixUi>(
converted_key: Key,
app: &mut App<U>,
old_state: &State,
) -> Result<()> {
info!( info!(
"No keymaps exist for key ('{}'), passing it along..", "No keymaps exist for key ('{}'), passing it along..",
converted_key converted_key

View File

@ -3,7 +3,10 @@ pub mod listeners;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use crate::app::{command_interface::Commands, App}; use crate::{
app::{command_interface::Commands, App},
ui::ui_trait::TrinitrixUi,
};
use cli_log::{trace, warn}; use cli_log::{trace, warn};
use crossterm::event::Event as CrosstermEvent; use crossterm::event::Event as CrosstermEvent;
use handlers::{command, input}; use handlers::{command, input};
@ -18,7 +21,7 @@ pub enum Event {
} }
impl Event { impl Event {
pub async fn handle(self, app: &mut App<'_>) -> Result<EventStatus> { pub async fn handle<U: TrinitrixUi>(self, app: &mut App<U>) -> Result<EventStatus> {
trace!("Received event to handle: `{:#?}`", &self); trace!("Received event to handle: `{:#?}`", &self);
match self { match self {
Event::CommandEvent(event, callback_tx) => command::handle(app, &event, callback_tx) Event::CommandEvent(event, callback_tx) => command::handle(app, &event, callback_tx)

View File

@ -3,13 +3,12 @@ pub mod config;
pub mod events; pub mod events;
pub mod status; pub mod status;
use std::{collections::HashMap, ffi::c_int, path::PathBuf, sync::OnceLock}; use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use cli_log::{info, warn}; use cli_log::warn;
use directories::ProjectDirs; use directories::ProjectDirs;
use keymaps::trie::Node; use keymaps::trie::Node;
use libloading::{Library, Symbol};
use tokio::sync::mpsc::{self, Sender}; use tokio::sync::mpsc::{self, Sender};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
@ -22,11 +21,11 @@ use crate::{
events::{Event, EventStatus}, events::{Event, EventStatus},
status::{State, Status}, status::{State, Status},
}, },
ui::central, ui::ui_trait::TrinitrixUi,
}; };
pub struct App<'runtime> { pub struct App<U: TrinitrixUi> {
ui: central::UI<'runtime>, ui: U,
status: Status, status: Status,
tx: mpsc::Sender<Event>, tx: mpsc::Sender<Event>,
@ -42,8 +41,8 @@ pub struct App<'runtime> {
pub static COMMAND_TRANSMITTER: OnceLock<Sender<Event>> = OnceLock::new(); pub static COMMAND_TRANSMITTER: OnceLock<Sender<Event>> = OnceLock::new();
impl App<'_> { impl<U: TrinitrixUi> App<U> {
pub fn new() -> Result<Self> { pub fn new(ui: U) -> Result<Self> {
let (tx, rx) = mpsc::channel(256); let (tx, rx) = mpsc::channel(256);
COMMAND_TRANSMITTER COMMAND_TRANSMITTER
@ -51,7 +50,7 @@ impl App<'_> {
.expect("The cell should always be empty at this point"); .expect("The cell should always be empty at this point");
Ok(Self { Ok(Self {
ui: central::UI::new()?, ui,
status: Status::new(), status: Status::new(),
tx: tx.clone(), tx: tx.clone(),
@ -80,50 +79,23 @@ impl App<'_> {
self.input_listener_killer.clone(), self.input_listener_killer.clone(),
)); ));
let config_dir = self.project_dirs.config_dir();
let mut lua_config_file = None;
if config_dir
.try_exists()
.context("Failed to search for a config file")?
{
lua_config_file = Some(config_dir.join("init.lua"));
info!("Found config dir: `{}`", config_dir.display());
} else {
warn!("No config dir found!");
}
if let Some(config_file) = cli_lua_config_file { if let Some(config_file) = cli_lua_config_file {
config::lua::load(self.tx.clone(), config_file).await?; config::lua::load(self.tx.clone(), config_file).await?;
} else if let Some(config_file) = lua_config_file { warn!("Loading cli config file, will ignore the default locations");
info!("Searching for config file at `{}`", config_file.display());
if config_file.try_exists().with_context(|| {
format!(
"Failed to check, if the lua config file at `{}` exists",
config_file.display()
)
})? {
config::lua::load(self.tx.clone(), config_file).await?;
} else { } else {
warn!("No config file found!"); let config_file = config::lua::check_for_config_file(self.project_dirs.config_dir())
.await
.context("Failed to check for the config file")?;
if let Some(config) = config_file {
config::lua::load(self.tx.clone(), config).await?;
} }
} }
if let Some(plugin) = plugin_path { if let Some(plugin) = plugin_path {
info!("Loading plugin_main() from {}", plugin.display()); config::shared_objects::load(&plugin)
.with_context(|| format!("Failed to load a pluging at '{}'", plugin.display()))?;
unsafe {
let lib = Library::new(plugin).context("Failed to load plugin")?;
let func: Symbol<unsafe fn() -> c_int> = lib
.get(b"plugin_main")
.context("Plugin does not have a 'plugin_main' symbol")?;
info!("Starting plugin");
let out = func();
info!("Plugin finished with: {}", out);
} }
}
self.status.set_state(State::Normal);
loop { loop {
self.ui.update(&self.status).await?; self.ui.update(&self.status).await?;

View File

@ -3,8 +3,9 @@ use core::fmt;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use keymaps::key_repr::Keys; use keymaps::key_repr::Keys;
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, Default)]
pub enum State { pub enum State {
#[default]
Normal, Normal,
Insert, Insert,
Command, Command,
@ -66,7 +67,7 @@ impl fmt::Display for State {
impl Status { impl Status {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
state: State::Normal, state: State::default(),
status_messages: vec![StatusMessage { status_messages: vec![StatusMessage {
content: "Initialized!".to_owned(), content: "Initialized!".to_owned(),
is_error: false, is_error: false,

View File

@ -23,4 +23,7 @@ pub struct Args {
pub enum Command { pub enum Command {
/// Starts the main TUI client /// Starts the main TUI client
Start {}, Start {},
/// Starts a repl to the trinitry cli interpreter
Repl {},
} }

View File

@ -4,7 +4,10 @@ mod ui;
use clap::Parser; use clap::Parser;
use crate::cli::{Args, Command}; use crate::{
cli::{Args, Command},
ui::repl::Repl,
};
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
@ -14,9 +17,16 @@ async fn main() -> anyhow::Result<()> {
let command = args.subcommand.unwrap_or(Command::Start {}); let command = args.subcommand.unwrap_or(Command::Start {});
match command { match command {
Command::Start {} => { Command::Start {} => {
let mut app = app::App::new()?; todo!("The full ui is not yet finished");
let mut app = app::App::new(Repl::new())?;
// NOTE(@soispha): The 'None' here is temporary <2024-05-03> // NOTE(@soispha): The `None` here is temporary <2024-05-03>
app.run(None, args.plugin_path).await?;
}
Command::Repl {} => {
let mut app = app::App::new(Repl::new())?;
// NOTE(@soispha): The `None` here is temporary <2024-05-03>
app.run(None, args.plugin_path).await?; app.run(None, args.plugin_path).await?;
} }
}; };

View File

@ -1,44 +1,2 @@
pub mod central; pub mod ui_trait;
pub mod repl;
use std::{io, io::Stdout};
use anyhow::{Context, Result};
use cli_log::info;
use crossterm::{
event::EnableMouseCapture,
execute,
terminal::{enable_raw_mode, EnterAlternateScreen},
};
use tui::{
style::{Color, Modifier, Style},
widgets::{Block, Borders},
};
use tui_textarea::TextArea;
fn terminal_prepare() -> Result<Stdout> {
enable_raw_mode().context("Failed to enable raw mode")?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
info!("Prepared terminal");
Ok(stdout)
}
pub fn textarea_activate(textarea: &mut TextArea) {
textarea.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED));
textarea.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
let b = textarea
.block()
.cloned()
.unwrap_or_else(|| Block::default().borders(Borders::ALL));
textarea.set_block(b.style(Style::default()));
}
pub fn textarea_inactivate(textarea: &mut TextArea) {
textarea.set_cursor_line_style(Style::default());
textarea.set_cursor_style(Style::default());
let b = textarea
.block()
.cloned()
.unwrap_or_else(|| Block::default().borders(Borders::ALL));
textarea.set_block(b.style(Style::default().fg(Color::DarkGray)));
}

View File

@ -19,6 +19,8 @@ use tui_textarea::TextArea;
use crate::ui::{terminal_prepare, textarea_activate, textarea_inactivate}; use crate::ui::{terminal_prepare, textarea_activate, textarea_inactivate};
use super::ui_trait::TirinitrixUi;
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
pub enum InputPosition { pub enum InputPosition {
Status, Status,
@ -148,6 +150,9 @@ pub struct UI<'a> {
pub cli: Option<TextArea<'a>>, pub cli: Option<TextArea<'a>>,
} }
impl<'r> TirinitrixUi for UI<'r> {
}
impl Drop for UI<'_> { impl Drop for UI<'_> {
fn drop(&mut self) { fn drop(&mut self) {
info!("Destructing UI"); info!("Destructing UI");

45
src/ui/old/mod.rs Normal file
View File

@ -0,0 +1,45 @@
pub mod central;
pub mod ui_trait;
use std::{io, io::Stdout};
use anyhow::{Context, Result};
use cli_log::info;
use crossterm::{
event::EnableMouseCapture,
execute,
terminal::{enable_raw_mode, EnterAlternateScreen},
};
use tui::{
style::{Color, Modifier, Style},
widgets::{Block, Borders},
};
use tui_textarea::TextArea;
fn terminal_prepare() -> Result<Stdout> {
enable_raw_mode().context("Failed to enable raw mode")?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
info!("Prepared terminal");
Ok(stdout)
}
pub fn textarea_activate(textarea: &mut TextArea) {
textarea.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED));
textarea.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
let b = textarea
.block()
.cloned()
.unwrap_or_else(|| Block::default().borders(Borders::ALL));
textarea.set_block(b.style(Style::default()));
}
pub fn textarea_inactivate(textarea: &mut TextArea) {
textarea.set_cursor_line_style(Style::default());
textarea.set_cursor_style(Style::default());
let b = textarea
.block()
.cloned()
.unwrap_or_else(|| Block::default().borders(Borders::ALL));
textarea.set_block(b.style(Style::default().fg(Color::DarkGray)));
}

54
src/ui/repl/mod.rs Normal file
View File

@ -0,0 +1,54 @@
use anyhow::Result;
use cli_log::info;
use keymaps::key_repr::{Key, KeyValue};
use crate::app::status::Status;
use super::ui_trait::TrinitrixUi;
pub struct Repl {
current_input: String,
enter_new_line: bool,
}
impl Repl {
pub fn new() -> Self {
println!("Welcome to the Trinitrix repl! You can now enter trinitry commands.");
print!("{}", Self::prompt(&Status::new()));
Repl {
current_input: String::new(),
enter_new_line: false,
}
}
pub fn prompt(status: &Status) -> String {
format!("{}> ", status.state())
}
}
impl TrinitrixUi for Repl {
async fn update(&mut self, status: &Status) -> Result<()> {
if self.enter_new_line {
info!("Got '{}' from user in repl.", self.current_input);
self.enter_new_line = false;
self.current_input = String::new();
print!("{}", Self::prompt(status));
}
Ok(())
}
fn input(&mut self, input: Key) {
if let Some(value) = input.value() {
if let KeyValue::Char(ch) = value {
self.current_input.push(*ch);
} else if let KeyValue::Enter = value {
self.enter_new_line = true;
}
} else {
info!("User wrote: '{}'", input.to_string_repr());
}
}
}

9
src/ui/ui_trait/mod.rs Normal file
View File

@ -0,0 +1,9 @@
use anyhow::Result;
use keymaps::key_repr::Key;
use crate::app::status::Status;
pub trait TrinitrixUi {
async fn update(&mut self, status: &Status) -> Result<()>;
fn input(&mut self, input: Key);
}