Remove TUI
This commit is contained in:
parent
fc1d1c9ee9
commit
6cb84a664a
43
Cargo.lock
generated
43
Cargo.lock
generated
@ -574,9 +574,7 @@ dependencies = [
|
||||
"rustyline",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strip-ansi-escapes",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tree-sitter",
|
||||
"wasm-bindgen-futures",
|
||||
]
|
||||
@ -2208,15 +2206,6 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0"
|
||||
|
||||
[[package]]
|
||||
name = "strip-ansi-escapes"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa"
|
||||
dependencies = [
|
||||
"vte",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
@ -2421,21 +2410,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.43",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.32"
|
||||
@ -2535,26 +2512,6 @@ version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "vte"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
"vte_generate_state_changes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vte_generate_state_changes"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.4.0"
|
||||
|
@ -27,9 +27,7 @@ reqwest = { version = "0.11.20", features = ["blocking", "json"] }
|
||||
rustyline = { version = "12.0.0", features = ["derive", "with-file-history"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
serde_json = "1.0.107"
|
||||
strip-ansi-escapes = "0.2.0"
|
||||
toml = "0.8.1"
|
||||
tracing = "0.1.40"
|
||||
tree-sitter = "0.20.10"
|
||||
egui = "0.24.1"
|
||||
eframe = { version = "0.24.1", default-features = false, features = [
|
||||
|
@ -1,25 +0,0 @@
|
||||
use std::{fmt, string::ToString};
|
||||
|
||||
use serde::{
|
||||
de::{self, Deserializer, Visitor},
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
use strum::Display;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Display, Deserialize)]
|
||||
pub enum Action {
|
||||
Tick,
|
||||
Render,
|
||||
ReadFile,
|
||||
Resize(u16, u16),
|
||||
Suspend,
|
||||
Resume,
|
||||
Quit,
|
||||
Refresh,
|
||||
Error(String),
|
||||
Help,
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
}
|
@ -1,152 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use color_eyre::eyre::Result;
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::prelude::Rect;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::{
|
||||
action::Action,
|
||||
components::{fps::FpsCounter, home::Home, Component},
|
||||
config::Config,
|
||||
mode::Mode,
|
||||
tui,
|
||||
};
|
||||
|
||||
pub struct App {
|
||||
pub config: Config,
|
||||
pub components: Vec<Box<dyn Component>>,
|
||||
pub should_quit: bool,
|
||||
pub should_suspend: bool,
|
||||
pub mode: Mode,
|
||||
pub last_tick_key_events: Vec<KeyEvent>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(path: PathBuf) -> Result<Self> {
|
||||
let home = Home::new(path)?;
|
||||
let config = Config::new()?;
|
||||
let mode = Mode::Home;
|
||||
Ok(Self {
|
||||
components: vec![Box::new(home)],
|
||||
should_quit: false,
|
||||
should_suspend: false,
|
||||
config,
|
||||
mode,
|
||||
last_tick_key_events: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> Result<()> {
|
||||
let (action_tx, mut action_rx) = mpsc::unbounded_channel();
|
||||
|
||||
let mut tui = tui::Tui::new()?.mouse(true);
|
||||
|
||||
tui.enter()?;
|
||||
|
||||
for component in self.components.iter_mut() {
|
||||
component.register_action_handler(action_tx.clone())?;
|
||||
}
|
||||
|
||||
for component in self.components.iter_mut() {
|
||||
component.register_config_handler(self.config.clone())?;
|
||||
}
|
||||
|
||||
for component in self.components.iter_mut() {
|
||||
component.init(tui.size()?)?;
|
||||
}
|
||||
|
||||
loop {
|
||||
if let Some(e) = tui.next().await {
|
||||
match e {
|
||||
tui::Event::Quit => action_tx.send(Action::Quit)?,
|
||||
tui::Event::Tick => action_tx.send(Action::Tick)?,
|
||||
tui::Event::Render => action_tx.send(Action::Render)?,
|
||||
tui::Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?,
|
||||
tui::Event::Key(key) => {
|
||||
if let Some(keymap) = self.config.keybindings.get(&self.mode) {
|
||||
if let Some(action) = keymap.get(&vec![key]) {
|
||||
log::info!("Got action: {action:?}");
|
||||
action_tx.send(action.clone())?;
|
||||
} else {
|
||||
// If the key was not handled as a single key action,
|
||||
// then consider it for multi-key combinations.
|
||||
self.last_tick_key_events.push(key);
|
||||
|
||||
// Check for multi-key combinations
|
||||
if let Some(action) = keymap.get(&self.last_tick_key_events) {
|
||||
log::info!("Got action: {action:?}");
|
||||
action_tx.send(action.clone())?;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
for component in self.components.iter_mut() {
|
||||
if let Some(action) = component.handle_events(Some(e.clone()))? {
|
||||
action_tx.send(action)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Ok(action) = action_rx.try_recv() {
|
||||
if action != Action::Tick && action != Action::Render {
|
||||
log::debug!("{action:?}");
|
||||
}
|
||||
match action {
|
||||
Action::Tick => {
|
||||
self.last_tick_key_events.drain(..);
|
||||
}
|
||||
Action::Quit => self.should_quit = true,
|
||||
Action::Suspend => self.should_suspend = true,
|
||||
Action::Resume => self.should_suspend = false,
|
||||
Action::Resize(w, h) => {
|
||||
tui.resize(Rect::new(0, 0, w, h))?;
|
||||
tui.draw(|f| {
|
||||
for component in self.components.iter_mut() {
|
||||
let r = component.draw(f, f.size());
|
||||
if let Err(e) = r {
|
||||
action_tx
|
||||
.send(Action::Error(format!("Failed to draw: {:?}", e)))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
})?;
|
||||
}
|
||||
Action::Render => {
|
||||
tui.draw(|f| {
|
||||
for component in self.components.iter_mut() {
|
||||
let r = component.draw(f, f.size());
|
||||
if let Err(e) = r {
|
||||
action_tx
|
||||
.send(Action::Error(format!("Failed to draw: {:?}", e)))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
})?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
for component in self.components.iter_mut() {
|
||||
if let Some(action) = component.update(action.clone())? {
|
||||
action_tx.send(action)?
|
||||
};
|
||||
}
|
||||
}
|
||||
if self.should_suspend {
|
||||
tui.suspend()?;
|
||||
action_tx.send(Action::Resume)?;
|
||||
tui = tui::Tui::new()?;
|
||||
// tui.mouse(true);
|
||||
tui.enter()?;
|
||||
} else if self.should_quit {
|
||||
tui.stop()?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
tui.exit()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use crate::utils::version;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version = version(), about)]
|
||||
pub struct Cli {
|
||||
/// File that will be run and watched for changes.
|
||||
pub path: PathBuf,
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
use color_eyre::eyre::Result;
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use ratatui::layout::Rect;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
tui::{Event, Frame},
|
||||
};
|
||||
|
||||
pub mod fps;
|
||||
pub mod home;
|
||||
mod list_display;
|
||||
mod map_display;
|
||||
|
||||
/// `Component` is a trait that represents a visual and interactive element of the user interface.
|
||||
/// Implementors of this trait can be registered with the main application loop and will be able to receive events,
|
||||
/// update state, and be rendered on the screen.
|
||||
pub trait Component {
|
||||
/// Register an action handler that can send actions for processing if necessary.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `tx` - An unbounded sender that can send actions.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<()>` - An Ok result or an error.
|
||||
#[allow(unused_variables)]
|
||||
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
/// Register a configuration handler that provides configuration settings if necessary.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `config` - Configuration settings.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<()>` - An Ok result or an error.
|
||||
#[allow(unused_variables)]
|
||||
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
/// Initialize the component with a specified area if necessary.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `area` - Rectangular area to initialize the component within.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<()>` - An Ok result or an error.
|
||||
fn init(&mut self, area: Rect) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
/// Handle incoming events and produce actions if necessary.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event` - An optional event to be processed.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Option<Action>>` - An action to be processed or none.
|
||||
fn handle_events(&mut self, event: Option<Event>) -> Result<Option<Action>> {
|
||||
let r = match event {
|
||||
Some(Event::Key(key_event)) => self.handle_key_events(key_event)?,
|
||||
Some(Event::Mouse(mouse_event)) => self.handle_mouse_events(mouse_event)?,
|
||||
_ => None,
|
||||
};
|
||||
Ok(r)
|
||||
}
|
||||
/// Handle key events and produce actions if necessary.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - A key event to be processed.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Option<Action>>` - An action to be processed or none.
|
||||
#[allow(unused_variables)]
|
||||
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
Ok(None)
|
||||
}
|
||||
/// Handle mouse events and produce actions if necessary.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `mouse` - A mouse event to be processed.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Option<Action>>` - An action to be processed or none.
|
||||
#[allow(unused_variables)]
|
||||
fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Result<Option<Action>> {
|
||||
Ok(None)
|
||||
}
|
||||
/// Update the state of the component based on a received action. (REQUIRED)
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `action` - An action that may modify the state of the component.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Option<Action>>` - An action to be processed or none.
|
||||
#[allow(unused_variables)]
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
Ok(None)
|
||||
}
|
||||
/// Render the component on the screen. (REQUIRED)
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `f` - A frame used for rendering.
|
||||
/// * `area` - The area in which the component should be drawn.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<()>` - An Ok result or an error.
|
||||
fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()>;
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use color_eyre::eyre::Result;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
use super::Component;
|
||||
use crate::{action::Action, tui::Frame};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct FpsCounter {
|
||||
app_start_time: Instant,
|
||||
app_frames: u32,
|
||||
app_fps: f64,
|
||||
|
||||
render_start_time: Instant,
|
||||
render_frames: u32,
|
||||
render_fps: f64,
|
||||
}
|
||||
|
||||
impl Default for FpsCounter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl FpsCounter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
app_start_time: Instant::now(),
|
||||
app_frames: 0,
|
||||
app_fps: 0.0,
|
||||
render_start_time: Instant::now(),
|
||||
render_frames: 0,
|
||||
render_fps: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn app_tick(&mut self) -> Result<()> {
|
||||
self.app_frames += 1;
|
||||
let now = Instant::now();
|
||||
let elapsed = (now - self.app_start_time).as_secs_f64();
|
||||
if elapsed >= 1.0 {
|
||||
self.app_fps = self.app_frames as f64 / elapsed;
|
||||
self.app_start_time = now;
|
||||
self.app_frames = 0;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_tick(&mut self) -> Result<()> {
|
||||
self.render_frames += 1;
|
||||
let now = Instant::now();
|
||||
let elapsed = (now - self.render_start_time).as_secs_f64();
|
||||
if elapsed >= 1.0 {
|
||||
self.render_fps = self.render_frames as f64 / elapsed;
|
||||
self.render_start_time = now;
|
||||
self.render_frames = 0;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for FpsCounter {
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
if let Action::Tick = action {
|
||||
self.app_tick()?
|
||||
};
|
||||
if let Action::Render = action {
|
||||
self.render_tick()?
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> Result<()> {
|
||||
let rects = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![
|
||||
Constraint::Length(1), // first row
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(rect);
|
||||
|
||||
let rect = rects[0];
|
||||
|
||||
let s = format!("{:.2} ticks per sec (app) {:.2} frames per sec (render)", self.app_fps, self.render_fps);
|
||||
let block = Block::default().title(block::Title::from(s.dim()).alignment(Alignment::Right));
|
||||
f.render_widget(block, rect);
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::read_to_string,
|
||||
path::PathBuf,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use color_eyre::eyre::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use dust_lang::{interpret, interpret_with_context, Map, Value};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tui_tree_widget::{Tree, TreeItem, TreeState};
|
||||
|
||||
use super::{map_display::MapDisplay, Component, Frame};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::{Config, KeyBindings},
|
||||
};
|
||||
|
||||
pub struct Home<'a> {
|
||||
command_tx: Option<UnboundedSender<Action>>,
|
||||
config: Config,
|
||||
path: PathBuf,
|
||||
source: String,
|
||||
context: Map,
|
||||
context_tree_state: TreeState<String>,
|
||||
context_display: MapDisplay<'a>,
|
||||
output: dust_lang::Result<Value>,
|
||||
last_modified: SystemTime,
|
||||
}
|
||||
|
||||
impl<'a> Home<'a> {
|
||||
pub fn new(path: PathBuf) -> Result<Self> {
|
||||
let context = Map::new();
|
||||
let context_display = MapDisplay::new(context.clone())?;
|
||||
|
||||
Ok(Home {
|
||||
command_tx: None,
|
||||
config: Config::default(),
|
||||
path,
|
||||
source: String::new(),
|
||||
context,
|
||||
context_display,
|
||||
context_tree_state: TreeState::default(),
|
||||
output: Ok(Value::default()),
|
||||
last_modified: SystemTime::now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Component for Home<'a> {
|
||||
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
|
||||
self.command_tx = Some(tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||
self.config = config;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::Tick => {
|
||||
let modified = self.path.metadata()?.modified()?;
|
||||
|
||||
if modified != self.last_modified {
|
||||
self.source = read_to_string(&self.path)?;
|
||||
self.last_modified = modified;
|
||||
self.output = interpret_with_context(&self.source, self.context.clone());
|
||||
self.context_display = MapDisplay::new(self.context.clone())?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.context_display.update(action)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(area);
|
||||
|
||||
self.context_display.draw(frame, layout[0])?;
|
||||
|
||||
let output_text = match &self.output {
|
||||
Ok(value) => value.to_string(),
|
||||
Err(error) => error.to_string(),
|
||||
};
|
||||
|
||||
frame.render_widget(Paragraph::new(output_text), layout[1]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
use dust_lang::List;
|
||||
|
||||
pub struct ListDisplay {
|
||||
list: List,
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
use color_eyre::Result;
|
||||
use dust_lang::{Map, Type, Value};
|
||||
use lazy_static::lazy_static;
|
||||
use ratatui::{
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
widgets::{Block, Borders},
|
||||
Frame,
|
||||
};
|
||||
use serial_int::SerialGenerator;
|
||||
use std::{hash::Hash, sync::Mutex};
|
||||
use tui_tree_widget::{Tree, TreeItem, TreeState};
|
||||
|
||||
use crate::{action::Action, components::Component};
|
||||
|
||||
lazy_static! {
|
||||
static ref ID_GENERATOR: Mutex<SerialGenerator<usize>> = Mutex::new(SerialGenerator::new());
|
||||
}
|
||||
|
||||
fn create_tree_item<'a>(key: String, value: &Value) -> Result<TreeItem<'a, usize>> {
|
||||
let tree_item = match value {
|
||||
Value::List(list) => {
|
||||
let mut items = Vec::new();
|
||||
|
||||
for (index, value) in list.items().iter().enumerate() {
|
||||
let item = create_tree_item(index.to_string(), value)?;
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
TreeItem::new(ID_GENERATOR.lock().unwrap().generate(), key, items)?
|
||||
}
|
||||
Value::Map(map) => {
|
||||
let mut items = Vec::new();
|
||||
|
||||
for (key, (value, _)) in map.variables()?.iter() {
|
||||
let item = create_tree_item(key.to_string(), value)?;
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
TreeItem::new(ID_GENERATOR.lock().unwrap().generate(), key, items)?
|
||||
}
|
||||
Value::Function(_) => todo!(),
|
||||
Value::String(string) => TreeItem::new_leaf(
|
||||
ID_GENERATOR.lock().unwrap().generate(),
|
||||
format!("{key} <str> = {value}"),
|
||||
),
|
||||
Value::Float(float) => TreeItem::new_leaf(
|
||||
ID_GENERATOR.lock().unwrap().generate(),
|
||||
format!("{key} <float> = {value}"),
|
||||
),
|
||||
Value::Integer(integer) => TreeItem::new_leaf(
|
||||
ID_GENERATOR.lock().unwrap().generate(),
|
||||
format!("{key} <int> = {value}"),
|
||||
),
|
||||
Value::Boolean(_) => todo!(),
|
||||
Value::Option(_) => todo!(),
|
||||
};
|
||||
|
||||
Ok(tree_item)
|
||||
}
|
||||
|
||||
pub struct MapDisplay<'a> {
|
||||
tree_state: TreeState<usize>,
|
||||
items: Vec<TreeItem<'a, usize>>,
|
||||
}
|
||||
|
||||
impl<'a> MapDisplay<'a> {
|
||||
pub fn new(map: Map) -> Result<Self> {
|
||||
let tree_state = TreeState::default();
|
||||
let mut items = Vec::new();
|
||||
|
||||
for (key, (value, _)) in map.variables()?.iter() {
|
||||
let item = create_tree_item(key.to_string(), value)?;
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
Ok(MapDisplay { tree_state, items })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Component for MapDisplay<'a> {
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let tree = Tree::new(self.items.clone())?
|
||||
.block(Block::new().title("context").borders(Borders::ALL))
|
||||
.highlight_style(Style::new().add_modifier(Modifier::BOLD));
|
||||
|
||||
frame.render_stateful_widget(tree, area, &mut self.tree_state);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::Up => self.tree_state.key_up(self.items.as_slice()),
|
||||
Action::Down => self.tree_state.key_down(&self.items),
|
||||
Action::Left => self.tree_state.key_left(),
|
||||
Action::Right => self.tree_state.key_right(),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
@ -1,563 +0,0 @@
|
||||
use std::{collections::HashMap, fmt, path::PathBuf};
|
||||
|
||||
use color_eyre::eyre::Result;
|
||||
use config::Value;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use derive_deref::{Deref, DerefMut};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use serde::{
|
||||
de::{self, Deserializer, MapAccess, Visitor},
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use crate::{action::Action, mode::Mode};
|
||||
|
||||
const CONFIG: &str = include_str!("../../../assets/config.json5");
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct AppConfig {
|
||||
#[serde(default)]
|
||||
pub _data_dir: PathBuf,
|
||||
#[serde(default)]
|
||||
pub _config_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default, flatten)]
|
||||
pub config: AppConfig,
|
||||
#[serde(default)]
|
||||
pub keybindings: KeyBindings,
|
||||
#[serde(default)]
|
||||
pub styles: Styles,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Result<Self, config::ConfigError> {
|
||||
let default_config: Config = json5::from_str(CONFIG).unwrap();
|
||||
let data_dir = crate::utils::get_data_dir();
|
||||
let config_dir = crate::utils::get_config_dir();
|
||||
let mut builder = config::Config::builder()
|
||||
.set_default("_data_dir", data_dir.to_str().unwrap())?
|
||||
.set_default("_config_dir", config_dir.to_str().unwrap())?;
|
||||
|
||||
let config_files = [
|
||||
("config.json5", config::FileFormat::Json5),
|
||||
("config.json", config::FileFormat::Json),
|
||||
("config.yaml", config::FileFormat::Yaml),
|
||||
("config.toml", config::FileFormat::Toml),
|
||||
("config.ini", config::FileFormat::Ini),
|
||||
];
|
||||
let mut found_config = false;
|
||||
for (file, format) in &config_files {
|
||||
builder = builder.add_source(
|
||||
config::File::from(config_dir.join(file))
|
||||
.format(*format)
|
||||
.required(false),
|
||||
);
|
||||
if config_dir.join(file).exists() {
|
||||
found_config = true
|
||||
}
|
||||
}
|
||||
if !found_config {
|
||||
log::error!("No configuration file found. Application may not behave as expected");
|
||||
}
|
||||
|
||||
let mut cfg: Self = builder.build()?.try_deserialize()?;
|
||||
|
||||
for (mode, default_bindings) in default_config.keybindings.iter() {
|
||||
let user_bindings = cfg.keybindings.entry(*mode).or_default();
|
||||
for (key, cmd) in default_bindings.iter() {
|
||||
user_bindings
|
||||
.entry(key.clone())
|
||||
.or_insert_with(|| cmd.clone());
|
||||
}
|
||||
}
|
||||
for (mode, default_styles) in default_config.styles.iter() {
|
||||
let user_styles = cfg.styles.entry(*mode).or_default();
|
||||
for (style_key, style) in default_styles.iter() {
|
||||
user_styles
|
||||
.entry(style_key.clone())
|
||||
.or_insert_with(|| style.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(cfg)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut)]
|
||||
pub struct KeyBindings(pub HashMap<Mode, HashMap<Vec<KeyEvent>, Action>>);
|
||||
|
||||
impl<'de> Deserialize<'de> for KeyBindings {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let parsed_map = HashMap::<Mode, HashMap<String, Action>>::deserialize(deserializer)?;
|
||||
|
||||
let keybindings = parsed_map
|
||||
.into_iter()
|
||||
.map(|(mode, inner_map)| {
|
||||
let converted_inner_map = inner_map
|
||||
.into_iter()
|
||||
.map(|(key_str, cmd)| (parse_key_sequence(&key_str).unwrap(), cmd))
|
||||
.collect();
|
||||
(mode, converted_inner_map)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(KeyBindings(keybindings))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_key_event(raw: &str) -> Result<KeyEvent, String> {
|
||||
let raw_lower = raw.to_ascii_lowercase();
|
||||
let (remaining, modifiers) = extract_modifiers(&raw_lower);
|
||||
parse_key_code_with_modifiers(remaining, modifiers)
|
||||
}
|
||||
|
||||
fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) {
|
||||
let mut modifiers = KeyModifiers::empty();
|
||||
let mut current = raw;
|
||||
|
||||
loop {
|
||||
match current {
|
||||
rest if rest.starts_with("ctrl-") => {
|
||||
modifiers.insert(KeyModifiers::CONTROL);
|
||||
current = &rest[5..];
|
||||
}
|
||||
rest if rest.starts_with("alt-") => {
|
||||
modifiers.insert(KeyModifiers::ALT);
|
||||
current = &rest[4..];
|
||||
}
|
||||
rest if rest.starts_with("shift-") => {
|
||||
modifiers.insert(KeyModifiers::SHIFT);
|
||||
current = &rest[6..];
|
||||
}
|
||||
_ => break, // break out of the loop if no known prefix is detected
|
||||
};
|
||||
}
|
||||
|
||||
(current, modifiers)
|
||||
}
|
||||
|
||||
fn parse_key_code_with_modifiers(
|
||||
raw: &str,
|
||||
mut modifiers: KeyModifiers,
|
||||
) -> Result<KeyEvent, String> {
|
||||
let c = match raw {
|
||||
"esc" => KeyCode::Esc,
|
||||
"enter" => KeyCode::Enter,
|
||||
"left" => KeyCode::Left,
|
||||
"right" => KeyCode::Right,
|
||||
"up" => KeyCode::Up,
|
||||
"down" => KeyCode::Down,
|
||||
"home" => KeyCode::Home,
|
||||
"end" => KeyCode::End,
|
||||
"pageup" => KeyCode::PageUp,
|
||||
"pagedown" => KeyCode::PageDown,
|
||||
"backtab" => {
|
||||
modifiers.insert(KeyModifiers::SHIFT);
|
||||
KeyCode::BackTab
|
||||
}
|
||||
"backspace" => KeyCode::Backspace,
|
||||
"delete" => KeyCode::Delete,
|
||||
"insert" => KeyCode::Insert,
|
||||
"f1" => KeyCode::F(1),
|
||||
"f2" => KeyCode::F(2),
|
||||
"f3" => KeyCode::F(3),
|
||||
"f4" => KeyCode::F(4),
|
||||
"f5" => KeyCode::F(5),
|
||||
"f6" => KeyCode::F(6),
|
||||
"f7" => KeyCode::F(7),
|
||||
"f8" => KeyCode::F(8),
|
||||
"f9" => KeyCode::F(9),
|
||||
"f10" => KeyCode::F(10),
|
||||
"f11" => KeyCode::F(11),
|
||||
"f12" => KeyCode::F(12),
|
||||
"space" => KeyCode::Char(' '),
|
||||
"hyphen" => KeyCode::Char('-'),
|
||||
"minus" => KeyCode::Char('-'),
|
||||
"tab" => KeyCode::Tab,
|
||||
c if c.len() == 1 => {
|
||||
let mut c = c.chars().next().unwrap();
|
||||
if modifiers.contains(KeyModifiers::SHIFT) {
|
||||
c = c.to_ascii_uppercase();
|
||||
}
|
||||
KeyCode::Char(c)
|
||||
}
|
||||
_ => return Err(format!("Unable to parse {raw}")),
|
||||
};
|
||||
Ok(KeyEvent::new(c, modifiers))
|
||||
}
|
||||
|
||||
pub fn key_event_to_string(key_event: &KeyEvent) -> String {
|
||||
let char;
|
||||
let key_code = match key_event.code {
|
||||
KeyCode::Backspace => "backspace",
|
||||
KeyCode::Enter => "enter",
|
||||
KeyCode::Left => "left",
|
||||
KeyCode::Right => "right",
|
||||
KeyCode::Up => "up",
|
||||
KeyCode::Down => "down",
|
||||
KeyCode::Home => "home",
|
||||
KeyCode::End => "end",
|
||||
KeyCode::PageUp => "pageup",
|
||||
KeyCode::PageDown => "pagedown",
|
||||
KeyCode::Tab => "tab",
|
||||
KeyCode::BackTab => "backtab",
|
||||
KeyCode::Delete => "delete",
|
||||
KeyCode::Insert => "insert",
|
||||
KeyCode::F(c) => {
|
||||
char = format!("f({c})");
|
||||
&char
|
||||
}
|
||||
KeyCode::Char(c) if c == ' ' => "space",
|
||||
KeyCode::Char(c) => {
|
||||
char = c.to_string();
|
||||
&char
|
||||
}
|
||||
KeyCode::Esc => "esc",
|
||||
KeyCode::Null => "",
|
||||
KeyCode::CapsLock => "",
|
||||
KeyCode::Menu => "",
|
||||
KeyCode::ScrollLock => "",
|
||||
KeyCode::Media(_) => "",
|
||||
KeyCode::NumLock => "",
|
||||
KeyCode::PrintScreen => "",
|
||||
KeyCode::Pause => "",
|
||||
KeyCode::KeypadBegin => "",
|
||||
KeyCode::Modifier(_) => "",
|
||||
};
|
||||
|
||||
let mut modifiers = Vec::with_capacity(3);
|
||||
|
||||
if key_event.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
modifiers.push("ctrl");
|
||||
}
|
||||
|
||||
if key_event.modifiers.intersects(KeyModifiers::SHIFT) {
|
||||
modifiers.push("shift");
|
||||
}
|
||||
|
||||
if key_event.modifiers.intersects(KeyModifiers::ALT) {
|
||||
modifiers.push("alt");
|
||||
}
|
||||
|
||||
let mut key = modifiers.join("-");
|
||||
|
||||
if !key.is_empty() {
|
||||
key.push('-');
|
||||
}
|
||||
key.push_str(key_code);
|
||||
|
||||
key
|
||||
}
|
||||
|
||||
pub fn parse_key_sequence(raw: &str) -> Result<Vec<KeyEvent>, String> {
|
||||
if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() {
|
||||
return Err(format!("Unable to parse `{}`", raw));
|
||||
}
|
||||
let raw = if !raw.contains("><") {
|
||||
let raw = raw.strip_prefix('<').unwrap_or(raw);
|
||||
let raw = raw.strip_prefix('>').unwrap_or(raw);
|
||||
raw
|
||||
} else {
|
||||
raw
|
||||
};
|
||||
let sequences = raw
|
||||
.split("><")
|
||||
.map(|seq| {
|
||||
if let Some(s) = seq.strip_prefix('<') {
|
||||
s
|
||||
} else if let Some(s) = seq.strip_suffix('>') {
|
||||
s
|
||||
} else {
|
||||
seq
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
sequences.into_iter().map(parse_key_event).collect()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut)]
|
||||
pub struct Styles(pub HashMap<Mode, HashMap<String, Style>>);
|
||||
|
||||
impl<'de> Deserialize<'de> for Styles {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let parsed_map = HashMap::<Mode, HashMap<String, String>>::deserialize(deserializer)?;
|
||||
|
||||
let styles = parsed_map
|
||||
.into_iter()
|
||||
.map(|(mode, inner_map)| {
|
||||
let converted_inner_map = inner_map
|
||||
.into_iter()
|
||||
.map(|(str, style)| (str, parse_style(&style)))
|
||||
.collect();
|
||||
(mode, converted_inner_map)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Styles(styles))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_style(line: &str) -> Style {
|
||||
let (foreground, background) =
|
||||
line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len()));
|
||||
let foreground = process_color_string(foreground);
|
||||
let background = process_color_string(&background.replace("on ", ""));
|
||||
|
||||
let mut style = Style::default();
|
||||
if let Some(fg) = parse_color(&foreground.0) {
|
||||
style = style.fg(fg);
|
||||
}
|
||||
if let Some(bg) = parse_color(&background.0) {
|
||||
style = style.bg(bg);
|
||||
}
|
||||
style = style.add_modifier(foreground.1 | background.1);
|
||||
style
|
||||
}
|
||||
|
||||
fn process_color_string(color_str: &str) -> (String, Modifier) {
|
||||
let color = color_str
|
||||
.replace("grey", "gray")
|
||||
.replace("bright ", "")
|
||||
.replace("bold ", "")
|
||||
.replace("underline ", "")
|
||||
.replace("inverse ", "");
|
||||
|
||||
let mut modifiers = Modifier::empty();
|
||||
if color_str.contains("underline") {
|
||||
modifiers |= Modifier::UNDERLINED;
|
||||
}
|
||||
if color_str.contains("bold") {
|
||||
modifiers |= Modifier::BOLD;
|
||||
}
|
||||
if color_str.contains("inverse") {
|
||||
modifiers |= Modifier::REVERSED;
|
||||
}
|
||||
|
||||
(color, modifiers)
|
||||
}
|
||||
|
||||
fn parse_color(s: &str) -> Option<Color> {
|
||||
let s = s.trim_start();
|
||||
let s = s.trim_end();
|
||||
if s.contains("bright color") {
|
||||
let s = s.trim_start_matches("bright ");
|
||||
let c = s
|
||||
.trim_start_matches("color")
|
||||
.parse::<u8>()
|
||||
.unwrap_or_default();
|
||||
Some(Color::Indexed(c.wrapping_shl(8)))
|
||||
} else if s.contains("color") {
|
||||
let c = s
|
||||
.trim_start_matches("color")
|
||||
.parse::<u8>()
|
||||
.unwrap_or_default();
|
||||
Some(Color::Indexed(c))
|
||||
} else if s.contains("gray") {
|
||||
let c = 232
|
||||
+ s.trim_start_matches("gray")
|
||||
.parse::<u8>()
|
||||
.unwrap_or_default();
|
||||
Some(Color::Indexed(c))
|
||||
} else if s.contains("rgb") {
|
||||
let red = (s.as_bytes()[3] as char).to_digit(10).unwrap_or_default() as u8;
|
||||
let green = (s.as_bytes()[4] as char).to_digit(10).unwrap_or_default() as u8;
|
||||
let blue = (s.as_bytes()[5] as char).to_digit(10).unwrap_or_default() as u8;
|
||||
let c = 16 + red * 36 + green * 6 + blue;
|
||||
Some(Color::Indexed(c))
|
||||
} else if s == "bold black" {
|
||||
Some(Color::Indexed(8))
|
||||
} else if s == "bold red" {
|
||||
Some(Color::Indexed(9))
|
||||
} else if s == "bold green" {
|
||||
Some(Color::Indexed(10))
|
||||
} else if s == "bold yellow" {
|
||||
Some(Color::Indexed(11))
|
||||
} else if s == "bold blue" {
|
||||
Some(Color::Indexed(12))
|
||||
} else if s == "bold magenta" {
|
||||
Some(Color::Indexed(13))
|
||||
} else if s == "bold cyan" {
|
||||
Some(Color::Indexed(14))
|
||||
} else if s == "bold white" {
|
||||
Some(Color::Indexed(15))
|
||||
} else if s == "black" {
|
||||
Some(Color::Indexed(0))
|
||||
} else if s == "red" {
|
||||
Some(Color::Indexed(1))
|
||||
} else if s == "green" {
|
||||
Some(Color::Indexed(2))
|
||||
} else if s == "yellow" {
|
||||
Some(Color::Indexed(3))
|
||||
} else if s == "blue" {
|
||||
Some(Color::Indexed(4))
|
||||
} else if s == "magenta" {
|
||||
Some(Color::Indexed(5))
|
||||
} else if s == "cyan" {
|
||||
Some(Color::Indexed(6))
|
||||
} else if s == "white" {
|
||||
Some(Color::Indexed(7))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_style_default() {
|
||||
let style = parse_style("");
|
||||
assert_eq!(style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_style_foreground() {
|
||||
let style = parse_style("red");
|
||||
assert_eq!(style.fg, Some(Color::Indexed(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_style_background() {
|
||||
let style = parse_style("on blue");
|
||||
assert_eq!(style.bg, Some(Color::Indexed(4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_style_modifiers() {
|
||||
let style = parse_style("underline red on blue");
|
||||
assert_eq!(style.fg, Some(Color::Indexed(1)));
|
||||
assert_eq!(style.bg, Some(Color::Indexed(4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_color_string() {
|
||||
let (color, modifiers) = process_color_string("underline bold inverse gray");
|
||||
assert_eq!(color, "gray");
|
||||
assert!(modifiers.contains(Modifier::UNDERLINED));
|
||||
assert!(modifiers.contains(Modifier::BOLD));
|
||||
assert!(modifiers.contains(Modifier::REVERSED));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_color_rgb() {
|
||||
let color = parse_color("rgb123");
|
||||
let expected = 16 + 1 * 36 + 2 * 6 + 3;
|
||||
assert_eq!(color, Some(Color::Indexed(expected)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_color_unknown() {
|
||||
let color = parse_color("unknown");
|
||||
assert_eq!(color, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config() -> Result<()> {
|
||||
let c = Config::new()?;
|
||||
assert_eq!(
|
||||
c.keybindings
|
||||
.get(&Mode::Home)
|
||||
.unwrap()
|
||||
.get(&parse_key_sequence("<q>").unwrap_or_default())
|
||||
.unwrap(),
|
||||
&Action::Quit
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_keys() {
|
||||
assert_eq!(
|
||||
parse_key_event("a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("enter").unwrap(),
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("esc").unwrap(),
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::empty())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_modifiers() {
|
||||
assert_eq!(
|
||||
parse_key_event("ctrl-a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("alt-enter").unwrap(),
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("shift-esc").unwrap(),
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_modifiers() {
|
||||
assert_eq!(
|
||||
parse_key_event("ctrl-alt-a").unwrap(),
|
||||
KeyEvent::new(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::CONTROL | KeyModifiers::ALT
|
||||
)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("ctrl-shift-enter").unwrap(),
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reverse_multiple_modifiers() {
|
||||
assert_eq!(
|
||||
key_event_to_string(&KeyEvent::new(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::CONTROL | KeyModifiers::ALT
|
||||
)),
|
||||
"ctrl-alt-a".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_keys() {
|
||||
assert!(parse_key_event("invalid-key").is_err());
|
||||
assert!(parse_key_event("ctrl-invalid-key").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_case_insensitivity() {
|
||||
assert_eq!(
|
||||
parse_key_event("CTRL-a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("AlT-eNtEr").unwrap(),
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
|
||||
);
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
#![allow(dead_code)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(unused_variables)]
|
||||
|
||||
pub mod action;
|
||||
pub mod app;
|
||||
pub mod cli;
|
||||
pub mod components;
|
||||
pub mod config;
|
||||
pub mod mode;
|
||||
pub mod tui;
|
||||
pub mod utils;
|
||||
|
||||
use std::env;
|
||||
|
||||
use clap::Parser;
|
||||
use cli::Cli;
|
||||
use color_eyre::eyre::Result;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
utils::{initialize_logging, initialize_panic_handler, version},
|
||||
};
|
||||
|
||||
async fn tokio_main() -> Result<()> {
|
||||
initialize_panic_handler()?;
|
||||
initialize_logging()?;
|
||||
|
||||
env::set_var("DUST_OUTPUT_MODE", "SILENT");
|
||||
|
||||
let args = Cli::parse();
|
||||
let mut app = App::new(args.path)?;
|
||||
app.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
if let Err(e) = tokio_main().await {
|
||||
eprintln!("{} error: Something went wrong", env!("CARGO_PKG_NAME"));
|
||||
Err(e)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum Mode {
|
||||
#[default]
|
||||
Home,
|
||||
}
|
@ -1,271 +0,0 @@
|
||||
use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use color_eyre::eyre::Result;
|
||||
use crossterm::{
|
||||
cursor,
|
||||
event::{
|
||||
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
|
||||
Event as CrosstermEvent, KeyEvent, KeyEventKind, MouseEvent,
|
||||
},
|
||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use ratatui::backend::CrosstermBackend as Backend;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::{
|
||||
sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub type IO = std::io::Stderr;
|
||||
pub fn io() -> IO {
|
||||
std::io::stderr()
|
||||
}
|
||||
pub type Frame<'a> = ratatui::Frame<'a>;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum Event {
|
||||
Init,
|
||||
Quit,
|
||||
Error,
|
||||
Closed,
|
||||
Tick,
|
||||
Render,
|
||||
FocusGained,
|
||||
FocusLost,
|
||||
Paste(String),
|
||||
Key(KeyEvent),
|
||||
Mouse(MouseEvent),
|
||||
Resize(u16, u16),
|
||||
}
|
||||
|
||||
pub struct Tui {
|
||||
pub terminal: ratatui::Terminal<Backend<IO>>,
|
||||
pub task: JoinHandle<()>,
|
||||
pub cancellation_token: CancellationToken,
|
||||
pub event_rx: UnboundedReceiver<Event>,
|
||||
pub event_tx: UnboundedSender<Event>,
|
||||
pub frame_rate: f64,
|
||||
pub tick_rate: f64,
|
||||
pub mouse: bool,
|
||||
pub paste: bool,
|
||||
}
|
||||
|
||||
impl Tui {
|
||||
pub fn new() -> Result<Self> {
|
||||
let tick_rate = 2.0;
|
||||
let frame_rate = 30.0;
|
||||
let terminal = ratatui::Terminal::new(Backend::new(io()))?;
|
||||
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
||||
let cancellation_token = CancellationToken::new();
|
||||
let task = tokio::spawn(async {});
|
||||
let mouse = false;
|
||||
let paste = false;
|
||||
|
||||
Ok(Self {
|
||||
terminal,
|
||||
task,
|
||||
cancellation_token,
|
||||
event_rx,
|
||||
event_tx,
|
||||
frame_rate,
|
||||
tick_rate,
|
||||
mouse,
|
||||
paste,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn tick_rate(mut self, tick_rate: f64) -> Self {
|
||||
self.tick_rate = tick_rate;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn frame_rate(mut self, frame_rate: f64) -> Self {
|
||||
self.frame_rate = frame_rate;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mouse(mut self, mouse: bool) -> Self {
|
||||
self.mouse = mouse;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn paste(mut self, paste: bool) -> Self {
|
||||
self.paste = paste;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
let tick_delay = std::time::Duration::from_secs_f64(1.0 / self.tick_rate);
|
||||
let render_delay = std::time::Duration::from_secs_f64(1.0 / self.frame_rate);
|
||||
|
||||
self.cancel();
|
||||
self.cancellation_token = CancellationToken::new();
|
||||
|
||||
let _cancellation_token = self.cancellation_token.clone();
|
||||
let _event_tx = self.event_tx.clone();
|
||||
|
||||
self.task = tokio::spawn(async move {
|
||||
let mut reader = crossterm::event::EventStream::new();
|
||||
let mut tick_interval = tokio::time::interval(tick_delay);
|
||||
let mut render_interval = tokio::time::interval(render_delay);
|
||||
|
||||
_event_tx.send(Event::Init).unwrap();
|
||||
|
||||
loop {
|
||||
let tick_delay = tick_interval.tick();
|
||||
let render_delay = render_interval.tick();
|
||||
let crossterm_event = reader.next().fuse();
|
||||
|
||||
tokio::select! {
|
||||
_ = _cancellation_token.cancelled() => {
|
||||
break;
|
||||
}
|
||||
maybe_event = crossterm_event => {
|
||||
match maybe_event {
|
||||
Some(Ok(evt)) => {
|
||||
match evt {
|
||||
CrosstermEvent::Key(key) => {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
_event_tx.send(Event::Key(key)).unwrap();
|
||||
}
|
||||
},
|
||||
CrosstermEvent::Mouse(mouse) => {
|
||||
_event_tx.send(Event::Mouse(mouse)).unwrap();
|
||||
},
|
||||
CrosstermEvent::Resize(x, y) => {
|
||||
_event_tx.send(Event::Resize(x, y)).unwrap();
|
||||
},
|
||||
CrosstermEvent::FocusLost => {
|
||||
_event_tx.send(Event::FocusLost).unwrap();
|
||||
},
|
||||
CrosstermEvent::FocusGained => {
|
||||
_event_tx.send(Event::FocusGained).unwrap();
|
||||
},
|
||||
CrosstermEvent::Paste(s) => {
|
||||
_event_tx.send(Event::Paste(s)).unwrap();
|
||||
},
|
||||
}
|
||||
}
|
||||
Some(Err(_)) => {
|
||||
_event_tx.send(Event::Error).unwrap();
|
||||
}
|
||||
None => {},
|
||||
}
|
||||
},
|
||||
_ = tick_delay => {
|
||||
_event_tx.send(Event::Tick).unwrap();
|
||||
},
|
||||
_ = render_delay => {
|
||||
_event_tx.send(Event::Render).unwrap();
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn stop(&self) -> Result<()> {
|
||||
self.cancel();
|
||||
|
||||
let mut counter = 0;
|
||||
|
||||
while !self.task.is_finished() {
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
|
||||
counter += 1;
|
||||
|
||||
if counter > 50 {
|
||||
self.task.abort();
|
||||
}
|
||||
|
||||
if counter > 100 {
|
||||
log::error!("Failed to abort task in 100 milliseconds for unknown reason");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn enter(&mut self) -> Result<()> {
|
||||
crossterm::terminal::enable_raw_mode()?;
|
||||
crossterm::execute!(io(), EnterAlternateScreen, cursor::Hide)?;
|
||||
|
||||
if self.mouse {
|
||||
crossterm::execute!(io(), EnableMouseCapture)?;
|
||||
}
|
||||
|
||||
if self.paste {
|
||||
crossterm::execute!(io(), EnableBracketedPaste)?;
|
||||
}
|
||||
|
||||
self.start();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exit(&mut self) -> Result<()> {
|
||||
self.stop()?;
|
||||
|
||||
if crossterm::terminal::is_raw_mode_enabled()? {
|
||||
self.flush()?;
|
||||
|
||||
if self.paste {
|
||||
crossterm::execute!(io(), DisableBracketedPaste)?;
|
||||
}
|
||||
|
||||
if self.mouse {
|
||||
crossterm::execute!(io(), DisableMouseCapture)?;
|
||||
}
|
||||
|
||||
crossterm::execute!(io(), LeaveAlternateScreen, cursor::Show)?;
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cancel(&self) {
|
||||
self.cancellation_token.cancel();
|
||||
}
|
||||
|
||||
pub fn suspend(&mut self) -> Result<()> {
|
||||
self.exit()?;
|
||||
#[cfg(not(windows))]
|
||||
signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn resume(&mut self) -> Result<()> {
|
||||
self.enter()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn next(&mut self) -> Option<Event> {
|
||||
self.event_rx.recv().await
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Tui {
|
||||
type Target = ratatui::Terminal<Backend<IO>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.terminal
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Tui {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.terminal
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Tui {
|
||||
fn drop(&mut self) {
|
||||
self.exit().unwrap();
|
||||
}
|
||||
}
|
@ -1,169 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use color_eyre::eyre::Result;
|
||||
use directories::ProjectDirs;
|
||||
use lazy_static::lazy_static;
|
||||
use tracing::error;
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::{
|
||||
self, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer,
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string();
|
||||
pub static ref DATA_FOLDER: Option<PathBuf> =
|
||||
std::env::var(format!("{}_DATA", PROJECT_NAME.clone()))
|
||||
.ok()
|
||||
.map(PathBuf::from);
|
||||
pub static ref CONFIG_FOLDER: Option<PathBuf> =
|
||||
std::env::var(format!("{}_CONFIG", PROJECT_NAME.clone()))
|
||||
.ok()
|
||||
.map(PathBuf::from);
|
||||
pub static ref LOG_ENV: String = format!("{}_LOGLEVEL", PROJECT_NAME.clone());
|
||||
pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME"));
|
||||
}
|
||||
|
||||
fn project_directory() -> Option<ProjectDirs> {
|
||||
ProjectDirs::from("com", "kdheepak", env!("CARGO_PKG_NAME"))
|
||||
}
|
||||
|
||||
pub fn initialize_panic_handler() -> Result<()> {
|
||||
let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default()
|
||||
.panic_section(format!(
|
||||
"This is a bug. Consider reporting it at {}",
|
||||
env!("CARGO_PKG_REPOSITORY")
|
||||
))
|
||||
.capture_span_trace_by_default(false)
|
||||
.display_location_section(false)
|
||||
.display_env_section(false)
|
||||
.into_hooks();
|
||||
eyre_hook.install()?;
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
if let Ok(mut t) = crate::tui::Tui::new() {
|
||||
if let Err(r) = t.exit() {
|
||||
error!("Unable to exit Terminal: {:?}", r);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
use human_panic::{handle_dump, print_msg, Metadata};
|
||||
let meta = Metadata {
|
||||
version: env!("CARGO_PKG_VERSION").into(),
|
||||
name: env!("CARGO_PKG_NAME").into(),
|
||||
authors: env!("CARGO_PKG_AUTHORS").replace(':', ", ").into(),
|
||||
homepage: env!("CARGO_PKG_HOMEPAGE").into(),
|
||||
};
|
||||
|
||||
let file_path = handle_dump(&meta, panic_info);
|
||||
// prints human-panic message
|
||||
print_msg(file_path, &meta)
|
||||
.expect("human-panic: printing error message to console failed");
|
||||
eprintln!("{}", panic_hook.panic_report(panic_info)); // prints color-eyre stack trace to stderr
|
||||
}
|
||||
let msg = format!("{}", panic_hook.panic_report(panic_info));
|
||||
log::error!("Error: {}", strip_ansi_escapes::strip_str(msg));
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
// Better Panic stacktrace that is only enabled when debugging.
|
||||
better_panic::Settings::auto()
|
||||
.most_recent_first(false)
|
||||
.lineno_suffix(true)
|
||||
.verbosity(better_panic::Verbosity::Full)
|
||||
.create_panic_handler()(panic_info);
|
||||
}
|
||||
|
||||
std::process::exit(libc::EXIT_FAILURE);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_data_dir() -> PathBuf {
|
||||
let directory = if let Some(s) = DATA_FOLDER.clone() {
|
||||
s
|
||||
} else if let Some(proj_dirs) = project_directory() {
|
||||
proj_dirs.data_local_dir().to_path_buf()
|
||||
} else {
|
||||
PathBuf::from(".").join(".data")
|
||||
};
|
||||
directory
|
||||
}
|
||||
|
||||
pub fn get_config_dir() -> PathBuf {
|
||||
let directory = if let Some(s) = CONFIG_FOLDER.clone() {
|
||||
s
|
||||
} else if let Some(proj_dirs) = project_directory() {
|
||||
proj_dirs.config_local_dir().to_path_buf()
|
||||
} else {
|
||||
PathBuf::from(".").join(".config")
|
||||
};
|
||||
directory
|
||||
}
|
||||
|
||||
pub fn initialize_logging() -> Result<()> {
|
||||
let directory = get_data_dir();
|
||||
std::fs::create_dir_all(directory.clone())?;
|
||||
let log_path = directory.join(LOG_FILE.clone());
|
||||
let log_file = std::fs::File::create(log_path)?;
|
||||
std::env::set_var(
|
||||
"RUST_LOG",
|
||||
std::env::var("RUST_LOG")
|
||||
.or_else(|_| std::env::var(LOG_ENV.clone()))
|
||||
.unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME"))),
|
||||
);
|
||||
let file_subscriber = tracing_subscriber::fmt::layer()
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_writer(log_file)
|
||||
.with_target(false)
|
||||
.with_ansi(false)
|
||||
.with_filter(tracing_subscriber::filter::EnvFilter::from_default_env());
|
||||
tracing_subscriber::registry()
|
||||
.with(file_subscriber)
|
||||
.with(ErrorLayer::default())
|
||||
.init();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Similar to the `std::dbg!` macro, but generates `tracing` events rather
|
||||
/// than printing to stdout.
|
||||
///
|
||||
/// By default, the verbosity level for the generated events is `DEBUG`, but
|
||||
/// this can be customized.
|
||||
#[macro_export]
|
||||
macro_rules! trace_dbg {
|
||||
(target: $target:expr, level: $level:expr, $ex:expr) => {{
|
||||
match $ex {
|
||||
value => {
|
||||
tracing::event!(target: $target, $level, ?value, stringify!($ex));
|
||||
value
|
||||
}
|
||||
}
|
||||
}};
|
||||
(level: $level:expr, $ex:expr) => {
|
||||
trace_dbg!(target: module_path!(), level: $level, $ex)
|
||||
};
|
||||
(target: $target:expr, $ex:expr) => {
|
||||
trace_dbg!(target: $target, level: tracing::Level::DEBUG, $ex)
|
||||
};
|
||||
($ex:expr) => {
|
||||
trace_dbg!(level: tracing::Level::DEBUG, $ex)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn version() -> String {
|
||||
let author = clap::crate_authors!();
|
||||
|
||||
// let current_exe_path = PathBuf::from(clap::crate_name!()).display().to_string();
|
||||
let config_dir_path = get_config_dir().display().to_string();
|
||||
let data_dir_path = get_data_dir().display().to_string();
|
||||
|
||||
format!(
|
||||
"\
|
||||
authors: {author}
|
||||
|
||||
Config directory: {config_dir_path}
|
||||
Data directory: {data_dir_path}"
|
||||
)
|
||||
}
|
@ -1,26 +1,5 @@
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use crate::{BuiltInFunction, Error, Map, Result, Type, Value};
|
||||
|
||||
lazy_static! {
|
||||
static ref OUTPUT_MODE: OutputMode = {
|
||||
if let Ok(variable) = std::env::var("DUST_OUTPUT_MODE") {
|
||||
if variable == "SILENT" {
|
||||
OutputMode::Silent
|
||||
} else {
|
||||
OutputMode::Normal
|
||||
}
|
||||
} else {
|
||||
OutputMode::Normal
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub enum OutputMode {
|
||||
Normal,
|
||||
Silent,
|
||||
}
|
||||
|
||||
pub struct Output;
|
||||
|
||||
impl BuiltInFunction for Output {
|
||||
@ -33,9 +12,7 @@ impl BuiltInFunction for Output {
|
||||
|
||||
let value = arguments.first().unwrap();
|
||||
|
||||
if let OutputMode::Normal = *OUTPUT_MODE {
|
||||
println!("{value}");
|
||||
}
|
||||
println!("{value}");
|
||||
|
||||
Ok(Value::default())
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user