Compare commits

..

3 Commits

Author SHA1 Message Date
Benedikt Peetz 20c155d58a
sqoush 2023-07-23 16:36:58 +02:00
Benedikt Peetz a22819c864
Fix(app::command_interface): Provide pre-generated file, to check syntax 2023-07-23 16:35:18 +02:00
Benedikt Peetz 838f62cb5d
Feat(treewide): Add a feature based layout and repl subcommand
Compiling the whole tui stack, just to debug the lua command line seems
counterproductive to me. This allows to just compile the needed parts
for a basic lua repl.

As of yet the repl is just a mock-up, as the event handling can, as of
right now, not easily be separated from the tui.

To activate specific features add specify the on the cargo command line
like this:
```
cargo run --features "cli tui"
```
or add them to the `default` feature set in the `Cargo.toml`.
2023-07-23 16:12:22 +02:00
13 changed files with 623 additions and 1 deletions

View File

@ -7,7 +7,7 @@ license = "MIT"
# 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
[features] [features]
default = ["cli"] default = ["full"]
full = ["cli", "tui"] full = ["cli", "tui"]
cli = ["tokio/io-std"] cli = ["tokio/io-std"]

26
lua_macros/src/new/lib.rs Normal file
View File

@ -0,0 +1,26 @@
#[proc_macro_attribute]
pub fn turn_struct_to_ci_command_enum(_attrs: TokenStream, input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let mut input: DeriveInput = syn::parse(input).expect("This should always be valid rust code, as it's extracted from direct code");
let mut named_fields = match &input.data {
syn::Data::Struct(input) => match &input.fields {
syn::Fields::Named(named_fields) => named_fields,
_ => unimplemented!("The macro only works for named fields (e.g.: `Name: Type`)"),
},
_ => unimplemented!("The macro only works for structs"),
}
.to_owned();
// Build the trait implementation
let build_lua_functions: TokenStream2 = genrate::build_lua_functions(&input);
let command_enum = generate::command_enum(&named_fields);
quote! {
}
.into()
}

View File

@ -0,0 +1,66 @@
use crate::app::{command_interface::Command, events::event_types::EventStatus, App};
use anyhow::Result;
use cli_log::info;
pub async fn handle(
app: &mut App<'_>,
command: &Command,
send_output: bool,
) -> Result<(EventStatus, String)> {
macro_rules! set_status_output {
($str:expr) => {
if send_output {
app.ui.set_command_output($str);
}
};
($str:expr, $($args:ident),+) => {
if send_output {
app.ui.set_command_output(&format!($str, $($args),+));
}
};
}
info!("Handling command: {:#?}", command);
Ok(match command {
Command::Exit => (
EventStatus::Terminate,
"Terminated the application".to_owned(),
),
Command::CommandLineShow => {
app.ui.cli_enable();
set_status_output!("CLI online");
(EventStatus::Ok, "".to_owned())
}
Command::CommandLineHide => {
app.ui.cli_disable();
set_status_output!("CLI offline");
(EventStatus::Ok, "".to_owned())
}
Command::CyclePlanes => {
app.ui.cycle_main_input_position();
set_status_output!("Switched main input position");
(EventStatus::Ok, "".to_owned())
}
Command::CyclePlanesRev => {
app.ui.cycle_main_input_position_rev();
set_status_output!("Switched main input position; reversed");
(EventStatus::Ok, "".to_owned())
}
Command::RoomMessageSend(msg) => {
if let Some(room) = app.status.room_mut() {
room.send(msg.clone()).await?;
}
set_status_output!("Send message: `{}`", msg);
(EventStatus::Ok, "".to_owned())
}
Command::Greet(name) => {
info!("Greated {}", name);
set_status_output!("Hi, {}!", name);
(EventStatus::Ok, "".to_owned())
}
Command::Help(_) => todo!(),
})
}

View File

@ -0,0 +1,21 @@
use std::time::Duration;
use anyhow::{Context, Result};
use cli_log::info;
use tokio::time::timeout;
use crate::app::{events::event_types::EventStatus, App};
pub async fn handle(app: &mut App<'_>, command: String) -> Result<EventStatus> {
info!("Recieved ci command: `{command}`; executing..");
app.lua_command_tx
.send(command.clone())
.await
.with_context(|| format!("Failed to execute: `{}`", command))?;
//let output = timeout(Duration::from_secs(5), run_command(app, command)).await??;
Ok(EventStatus::Ok)
}

View File

@ -0,0 +1,188 @@
use anyhow::{Context, Result};
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers};
use crate::{
app::{
command_interface::Command,
events::event_types::{Event, EventStatus},
App,
},
ui::central,
};
pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> {
match input_event {
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Esc, ..
}) => {
app.tx
.send(Event::CommandEvent(Command::Exit, None))
.await?;
}
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Tab, ..
}) => {
app.tx
.send(Event::CommandEvent(Command::CyclePlanes, None))
.await?;
}
CrosstermEvent::Key(KeyEvent {
code: KeyCode::BackTab,
..
}) => {
app.tx
.send(Event::CommandEvent(Command::CyclePlanesRev, None))
.await?;
}
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
..
}) => {
app.tx
.send(Event::CommandEvent(Command::CommandLineShow, None))
.await?;
}
input => match app.ui.input_position() {
central::InputPosition::MessageCompose => {
match input {
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Enter,
modifiers: KeyModifiers::ALT,
..
}) => {
app.tx
.send(Event::CommandEvent(
Command::RoomMessageSend(app.ui.message_compose.lines().join("\n")),
None,
))
.await?;
app.ui.message_compose_clear();
}
_ => {
app.ui
.message_compose
.input(tui_textarea::Input::from(input.to_owned()));
}
};
}
central::InputPosition::Rooms => {
match input {
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Up, ..
}) => {
let i = match app.ui.rooms_state.selected() {
Some(cur) => {
if cur > 0 {
cur - 1
} else {
cur
}
}
None => 0,
};
app.ui.rooms_state.select(Some(i));
app.status.set_room_by_index(i)?;
}
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Down,
..
}) => {
let i = match app.ui.rooms_state.selected() {
Some(cur) => {
if cur < app.status.rooms().len() - 1 {
cur + 1
} else {
cur
}
}
None => 0,
};
app.ui.rooms_state.select(Some(i));
app.status.set_room_by_index(i)?;
}
_ => (),
};
}
central::InputPosition::Messages => {
match input {
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Up, ..
}) => {
match app.status.room_mut() {
Some(room) => {
let len = room.timeline().len();
let i = match room.view_scroll() {
Some(i) => i + 1,
None => 0,
};
if i < len {
room.set_view_scroll(Some(i))
}
if i <= len - 5 {
room.poll_old_timeline().await?;
}
}
None => (),
};
}
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Down,
..
}) => {
match app.status.room_mut() {
Some(room) => {
match room.view_scroll() {
Some(i) => {
if i == 0 {
room.set_view_scroll(None);
} else {
room.set_view_scroll(Some(i - 1));
}
}
None => (),
};
}
None => (),
};
}
_ => (),
};
}
central::InputPosition::CLI => {
if let Some(_) = app.ui.cli {
match input {
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Enter,
..
}) => {
let ci_event = app.ui
.cli
.as_mut()
.expect("This is already checked")
.lines()
.get(0)
.expect(
"There can only be one line in the buffer, as we collect it on enter being inputted"
)
.to_owned();
app.tx
.send(Event::LuaCommand(ci_event))
.await
.context("Failed to send lua command to internal event stream")?;
}
_ => {
app.ui
.cli
.as_mut()
.expect("This is already checked")
.input(tui_textarea::Input::from(input.to_owned()));
}
};
};
}
_ => (),
},
};
Ok(EventStatus::Ok)
}

View File

@ -0,0 +1,23 @@
use matrix_sdk::deserialized_responses::SyncResponse;
use anyhow::Result;
use crate::app::{events::event_types::EventStatus, App};
pub async fn handle<'a>(app: &mut App<'a>, sync: &SyncResponse) -> Result<EventStatus> {
for (m_room_id, m_room) in sync.rooms.join.iter() {
let room = match app.status.get_room_mut(m_room_id) {
Some(r) => r,
None => continue,
};
for m_event in m_room.timeline.events.clone() {
let event = m_event
.event
.deserialize()
.unwrap()
.into_full_event(m_room_id.clone());
room.timeline_add(event);
}
}
Ok(EventStatus::Ok)
}

View File

@ -0,0 +1,10 @@
// input events
pub mod main;
pub mod setup;
// matrix
pub mod matrix;
// ci
pub mod command;
pub mod lua_command;

View File

@ -0,0 +1,72 @@
use anyhow::{bail, Context, Result};
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent};
use crate::{app::{events::event_types::EventStatus, App}, ui::setup};
pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> {
let ui = match &mut app.ui.setup_ui {
Some(ui) => ui,
None => bail!("SetupUI instance not found"),
};
match input_event {
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Esc, ..
}) => return Ok(EventStatus::Terminate),
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Tab, ..
}) => {
ui.cycle_input_position();
}
CrosstermEvent::Key(KeyEvent {
code: KeyCode::BackTab,
..
}) => {
ui.cycle_input_position_rev();
}
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Enter,
..
}) => {
match ui.input_position() {
setup::InputPosition::Ok => {
let homeserver = ui.homeserver.lines()[0].clone();
let username = ui.username.lines()[0].clone();
let password = ui.password_data.lines()[0].clone();
app.login(&homeserver, &username, &password)
.await
.context("Failed to login")?;
// We bailed in the line above, thus login must have succeeded
return Ok(EventStatus::Finished);
}
_ => ui.cycle_input_position(),
};
}
input => match ui.input_position() {
setup::InputPosition::Homeserver => {
ui.homeserver.input(input.to_owned());
}
setup::InputPosition::Username => {
ui.username.input(input.to_owned());
}
setup::InputPosition::Password => {
let textarea_input = tui_textarea::Input::from(input.to_owned());
ui.password_data.input(textarea_input.clone());
match textarea_input.key {
tui_textarea::Key::Char(_) => {
ui.password.input(tui_textarea::Input {
key: tui_textarea::Key::Char('*'),
ctrl: false,
alt: false,
});
}
_ => {
ui.password.input(textarea_input);
}
}
}
_ => (),
},
};
Ok(EventStatus::Ok)
}

View File

@ -0,0 +1,60 @@
mod handlers;
use anyhow::{Context, Result};
use cli_log::{info, trace};
use crossterm::event::Event as CrosstermEvent;
use tokio::sync::mpsc::Sender;
use crate::app::{command_interface::Command, status::State, App};
use self::handlers::{command, lua_command, main, matrix, setup};
use super::EventStatus;
#[derive(Debug)]
pub enum Event {
InputEvent(CrosstermEvent),
MatrixEvent(matrix_sdk::deserialized_responses::SyncResponse),
CommandEvent(Command, Option<Sender<String>>),
LuaCommand(String),
}
impl Event {
pub async fn handle(&self, app: &mut App<'_>) -> Result<EventStatus> {
trace!("Recieved event to handle: `{:#?}`", &self);
match &self {
Event::MatrixEvent(event) => matrix::handle(app, event)
.await
.with_context(|| format!("Failed to handle matrix event: `{:#?}`", event)),
Event::CommandEvent(event, callback_tx) => {
let (result, output) = command::handle(app, event, callback_tx.is_some())
.await
.with_context(|| format!("Failed to handle command event: `{:#?}`", event))?;
if let Some(callback_tx) = callback_tx {
callback_tx
.send(output.clone())
.await
.with_context(|| format!("Failed to send command output: {}", output))?;
}
Ok(result)
}
Event::LuaCommand(lua_code) => lua_command::handle(app, lua_code.to_owned())
.await
.with_context(|| format!("Failed to handle lua code: `{:#?}`", lua_code)),
Event::InputEvent(event) => match app.status.state() {
State::None => unreachable!(
"This state should not be available, when we are in the input handling"
),
State::Main => main::handle(app, event)
.await
.with_context(|| format!("Failed to handle input event: `{:#?}`", event)),
State::Setup => setup::handle(app, event)
.await
.with_context(|| format!("Failed to handle input event: `{:#?}`", event)),
},
}
}
}

View File

@ -0,0 +1,6 @@
#[derive(Debug)]
pub enum EventStatus {
Ok,
Finished,
Terminate,
}

View File

@ -0,0 +1,5 @@
pub mod event;
pub mod event_status;
pub use self::event::*;
pub use self::event_status::*;

85
src/event_handler/mod.rs Normal file
View File

@ -0,0 +1,85 @@
use tokio::{
runtime::Builder,
sync::{mpsc, oneshot},
task::LocalSet,
};
#[cfg(feature = "tui")]
mod poll_functions;
mod events;
// This struct describes the task you want to spawn. Here we include
// some simple examples. The oneshot channel allows sending a response
// to the spawner.
#[derive(Debug)]
enum Task {
PrintNumber(u32),
AddOne(u32, oneshot::Sender<u32>),
}
#[derive(Clone)]
struct LocalSpawner {
send: mpsc::UnboundedSender<Task>,
}
impl LocalSpawner {
pub fn new() -> Self {
let (send, mut recv) = mpsc::unbounded_channel();
let rt = Builder::new_current_thread().enable_all().build().unwrap();
std::thread::spawn(move || {
let local = LocalSet::new();
local.spawn_local(async move {
while let Some(new_task) = recv.recv().await {
tokio::task::spawn_local(run_task(new_task));
}
// If the while loop returns, then all the LocalSpawner
// objects have been dropped.
});
// This will return once all senders are dropped and all
// spawned tasks have returned.
rt.block_on(local);
});
Self { send }
}
pub fn spawn(&self, task: Task) {
self.send
.send(task)
.expect("Thread with LocalSet has shut down.");
}
}
// This task may do !Send stuff. We use printing a number as an example,
// but it could be anything.
//
// The Task struct is an enum to support spawning many different kinds
// of operations.
async fn run_task(task: Task) {
match task {
Task::PrintNumber(n) => {
println!("{}", n);
}
Task::AddOne(n, response) => {
// We ignore failures to send the response.
let _ = response.send(n + 1);
}
}
}
#[tokio::main]
async fn main() {
let spawner = LocalSpawner::new();
let (send, response) = oneshot::channel();
spawner.spawn(Task::AddOne(10, send));
let eleven = response.await.unwrap();
spawner.spawn(Task::PrintNumber(eleven));
assert_eq!(eleven, 11);
}

View File

@ -0,0 +1,60 @@
use anyhow::{bail, Result};
use matrix_sdk::{config::SyncSettings, Client, LoopCtrl};
use tokio::{sync::mpsc, time::Duration};
use tokio_util::sync::CancellationToken;
use self::event_types::Event;
pub async fn poll_input_events(
channel: mpsc::Sender<Event>,
kill: CancellationToken,
) -> Result<()> {
async fn poll_input_events_stage_2(channel: mpsc::Sender<Event>) -> Result<()> {
loop {
if crossterm::event::poll(Duration::from_millis(100))? {
let event = Event::InputEvent(crossterm::event::read()?);
channel.send(event).await?;
} else {
tokio::task::yield_now().await;
}
}
}
tokio::select! {
output = poll_input_events_stage_2(channel) => output,
_ = kill.cancelled() => bail!("received kill signal")
}
}
pub async fn poll_matrix_events(
channel: mpsc::Sender<Event>,
kill: CancellationToken,
client: Client,
) -> Result<()> {
async fn poll_matrix_events_stage_2(
channel: mpsc::Sender<Event>,
client: Client,
) -> Result<()> {
let sync_settings = SyncSettings::default();
// .token(sync_token)
// .timeout(Duration::from_secs(30));
let tx = &channel;
client
.sync_with_callback(sync_settings, |response| async move {
let event = Event::MatrixEvent(response);
match tx.send(event).await {
Ok(_) => LoopCtrl::Continue,
Err(_) => LoopCtrl::Break,
}
})
.await?;
Ok(())
}
tokio::select! {
output = poll_matrix_events_stage_2(channel, client) => output,
_ = kill.cancelled() => bail!("received kill signal"),
}
}