1
0

Merge branch 'main' into gui

This commit is contained in:
Jeff 2023-12-30 10:45:38 -05:00
commit fc1d1c9ee9
26 changed files with 1705 additions and 1369 deletions

962
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -5,53 +5,38 @@ version = "0.4.0"
repository = "https://git.jeffa.io/jeff/dust.git"
edition = "2021"
license = "MIT"
authors = ["Jeff Anderson"]
[[bin]]
name = "dust"
path = "src/main.rs"
[[bin]]
name = "tui"
[profile.dev]
opt-level = 1
[profile.dev.package."*"]
opt-level = 3
[dependencies]
ansi_term = "0.12.1"
clap = { version = "4.4.4", features = ["derive"] }
color-eyre = "0.6.2"
comfy-table = "7.0.1"
crossterm = { version = "0.27.0", features = ["serde", "event-stream"] }
csv = "1.2.2"
directories = "5.0.1"
futures = "0.3.30"
git2 = "0.18.1"
lazy_static = "1.4.0"
libc = "0.2.148"
log = "0.4.20"
rand = "0.8.5"
ratatui = "0.25.0"
rayon = "1.8.0"
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"
signal-hook = "0.3.17"
tokio = { version = "1.35.1", features = ["signal-hook-registry", "macros", "signal", "rt", "time", "rt-multi-thread"] }
tokio-util = "0.7.10"
strip-ansi-escapes = "0.2.0"
toml = "0.8.1"
tracing = "0.1.40"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tree-sitter = "0.20.10"
tui-textarea = { version = "0.4.0", features = ["search"] }
egui = "0.24.1"
eframe = { version = "0.24.1", default-features = false, features = [
"default_fonts", # Embed the default egui fonts.
"glow", # Use the glow rendering backend. Alternative: "wgpu".
"persistence", # Enable restoring app state when restarting the app.
] }
log = "0.4"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
env_logger = "0.10"

14
assets/config.json5 Normal file
View File

@ -0,0 +1,14 @@
{
"keybindings": {
"Home": {
"<q>": "Quit", // Quit the application
"<Ctrl-d>": "Quit", // Another way to quit
"<Ctrl-c>": "Quit", // Yet another way to quit
"<Ctrl-z>": "Suspend", // Suspend the application
"<up>": "Up",
"<down>": "Down",
"<left>": "Left",
"<right>": "Right",
},
}
}

25
src/bin/tui/action.rs Normal file
View File

@ -0,0 +1,25 @@
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,
}

View File

@ -1,88 +1,152 @@
use std::path::PathBuf;
use crossterm::event::KeyCode;
use dust_lang::Map;
use ratatui::Frame;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
use color_eyre::eyre::Result;
use crossterm::event::KeyEvent;
use ratatui::prelude::Rect;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use crate::{interpreter_display::InterpreterDisplay, terminal::Terminal, Action, Elm, Result};
use crate::{
action::Action,
components::{fps::FpsCounter, home::Home, Component},
config::Config,
mode::Mode,
tui,
};
pub struct App {
action_rx: UnboundedReceiver<Action>,
action_tx: UnboundedSender<Action>,
interpreter_display: InterpreterDisplay,
should_quit: bool,
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(
action_rx: UnboundedReceiver<Action>,
action_tx: UnboundedSender<Action>,
path: PathBuf,
) -> Result<Self> {
let interpreter_display = InterpreterDisplay::new(Map::new(), path)?;
Ok(App {
action_rx,
action_tx,
interpreter_display,
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 mut terminal = Terminal::new()?.tick_rate(4.0).frame_rate(30.0);
let (action_tx, mut action_rx) = mpsc::unbounded_channel();
terminal.enter()?;
let mut tui = tui::Tui::new()?.mouse(true);
loop {
if self.should_quit {
break;
}
tui.enter()?;
if let Some(action) = terminal.next().await {
self.action_tx.send(action)?;
} else {
continue;
};
while let Ok(action) = self.action_rx.try_recv() {
if let Action::Render = action {
terminal.draw(|frame| {
self.view(frame);
})?;
}
let next_action = self.update(action)?;
if let Some(action) = next_action {
self.action_tx.send(action)?;
}
}
for component in self.components.iter_mut() {
component.register_action_handler(action_tx.clone())?;
}
terminal.exit()?;
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(())
}
}
impl Elm for App {
fn update(&mut self, message: Action) -> Result<Option<Action>> {
match message {
Action::Quit => self.should_quit = true,
Action::Key(key_event) => {
if let KeyCode::Esc = key_event.code {
return Ok(Some(Action::Quit));
}
}
_ => {}
}
self.interpreter_display.update(message)
}
fn view(&self, frame: &mut Frame) {
self.interpreter_display.view(frame)
}
}

12
src/bin/tui/cli.rs Normal file
View File

@ -0,0 +1,12 @@
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,
}

126
src/bin/tui/components.rs Normal file
View File

@ -0,0 +1,126 @@
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<()>;
}

View File

@ -0,0 +1,90 @@
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(())
}
}

View File

@ -0,0 +1,99 @@
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(())
}
}

View File

@ -0,0 +1,5 @@
use dust_lang::List;
pub struct ListDisplay {
list: List,
}

View File

@ -0,0 +1,106 @@
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)
}
}

563
src/bin/tui/config.rs Normal file
View File

@ -0,0 +1,563 @@
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)
);
}
}

View File

@ -1,3 +0,0 @@
pub type Result<T> = std::result::Result<T, Error>;
pub enum Error {}

View File

@ -1,53 +0,0 @@
use std::{fs::read_to_string, path::PathBuf, time::SystemTime};
use dust_lang::{Interpreter, Map, Value};
use ratatui::Frame;
use crate::{value_display::ValueDisplay, Action, Elm, Result};
pub struct InterpreterDisplay {
interpreter: Interpreter,
path: PathBuf,
value_display: ValueDisplay,
modified: SystemTime,
}
impl InterpreterDisplay {
pub fn new(context: Map, path: PathBuf) -> Result<Self> {
let interpreter = Interpreter::new(context)?;
let value_display = ValueDisplay::new(Value::default());
let modified = SystemTime::now();
Ok(Self {
interpreter,
path,
value_display,
modified,
})
}
}
impl Elm for InterpreterDisplay {
fn update(&mut self, message: Action) -> Result<Option<Action>> {
match message {
Action::Tick => {
let last_modified = self.path.metadata()?.modified()?;
if last_modified != self.modified {
let source = read_to_string(&self.path)?;
let value = self.interpreter.run(&source)?;
self.value_display = ValueDisplay::new(value);
self.modified = last_modified;
}
}
_ => {}
}
self.value_display.update(message)
}
fn view(&self, frame: &mut Frame) {
self.value_display.view(frame)
}
}

View File

@ -1,83 +0,0 @@
use std::path::PathBuf;
use color_eyre::eyre::Result;
use directories::ProjectDirs;
use lazy_static::lazy_static;
pub use tracing::error;
use tracing_error::ErrorLayer;
use tracing_subscriber::{self, layer::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 LOG_ENV: String = format!("{}_LOGLEVEL", PROJECT_NAME.clone());
pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME"));
}
pub fn project_directory() -> Option<ProjectDirs> {
ProjectDirs::from("io", "jeffa", env!("CARGO_PKG_NAME"))
}
pub fn get_data_dir() -> PathBuf {
if let Some(path) = DATA_FOLDER.clone() {
path
} else if let Some(proj_dirs) = project_directory() {
proj_dirs.data_local_dir().to_path_buf()
} else {
PathBuf::from(".").join(".data")
}
}
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)
};
}

View File

@ -1,67 +1,46 @@
pub mod app;
pub mod interpreter_display;
pub mod log;
pub mod terminal;
pub mod value_display;
#![allow(dead_code)]
#![allow(unused_imports)]
#![allow(unused_variables)]
use std::path::PathBuf;
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,
log::{get_data_dir, initialize_logging},
utils::{initialize_logging, initialize_panic_handler, version},
};
use clap::Parser;
use color_eyre::Result;
use crossterm::event::{KeyEvent, MouseEvent};
use dust_lang::Value;
use ratatui::Frame;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
/// File with source to be run and watched by the shell.
path: Option<String>,
}
async fn tokio_main() -> Result<()> {
initialize_panic_handler()?;
initialize_logging()?;
pub trait Elm {
fn update(&mut self, message: Action) -> Result<Option<Action>>;
fn view(&self, frame: &mut Frame);
}
env::set_var("DUST_OUTPUT_MODE", "SILENT");
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Action {
Init,
Quit,
Error,
Closed,
Tick,
Render,
FocusGained,
FocusLost,
Paste(String),
Key(KeyEvent),
Mouse(MouseEvent),
Resize(u16, u16),
UpdateValue(Value),
let args = Cli::parse();
let mut app = App::new(args.path)?;
app.run().await?;
Ok(())
}
#[tokio::main]
async fn main() {
initialize_logging().unwrap();
let args = Cli::parse();
let (action_tx, action_rx) = mpsc::unbounded_channel();
let path = if let Some(path) = args.path {
PathBuf::from(path)
async fn main() -> Result<()> {
if let Err(e) = tokio_main().await {
eprintln!("{} error: Something went wrong", env!("CARGO_PKG_NAME"));
Err(e)
} else {
PathBuf::from(format!("{}/scratch.ds", get_data_dir().to_string_lossy()))
};
let mut app = App::new(action_rx, action_tx, path).unwrap();
let run_result = app.run().await;
if let Err(report) = run_result {
eprintln!("{report}")
Ok(())
}
}

7
src/bin/tui/mode.rs Normal file
View File

@ -0,0 +1,7 @@
use serde::{Deserialize, Serialize};
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Mode {
#[default]
Home,
}

View File

@ -1,229 +0,0 @@
use std::{
io::{stderr, Stderr},
ops::{Deref, DerefMut},
time::Duration,
};
use color_eyre::eyre::Result;
use crossterm::{
cursor,
event::{
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
Event, KeyEventKind,
},
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
};
use futures::{FutureExt, StreamExt};
use ratatui::prelude::CrosstermBackend;
use tokio::{
sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
task::JoinHandle,
};
use tokio_util::sync::CancellationToken;
use crate::{log, Action};
pub struct Terminal {
pub terminal: ratatui::Terminal<CrosstermBackend<Stderr>>,
pub task: JoinHandle<()>,
pub cancellation_token: CancellationToken,
pub event_rx: UnboundedReceiver<Action>,
pub event_tx: UnboundedSender<Action>,
pub frame_rate: f64,
pub tick_rate: f64,
pub mouse: bool,
pub paste: bool,
}
impl Terminal {
pub fn new() -> Result<Self> {
let tick_rate = 4.0;
let frame_rate = 60.0;
let terminal = ratatui::Terminal::new(CrosstermBackend::new(stderr()))?;
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(Action::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 {
Event::Key(key) => {
if key.kind == KeyEventKind::Press {
_event_tx.send(Action::Key(key)).unwrap();
}
},
Event::Mouse(mouse) => {
_event_tx.send(Action::Mouse(mouse)).unwrap();
},
Event::Resize(x, y) => {
_event_tx.send(Action::Resize(x, y)).unwrap();
},
Event::FocusLost => {
_event_tx.send(Action::FocusLost).unwrap();
},
Event::FocusGained => {
_event_tx.send(Action::FocusGained).unwrap();
},
Event::Paste(s) => {
_event_tx.send(Action::Paste(s)).unwrap();
},
}
}
Some(Err(_)) => {
_event_tx.send(Action::Error).unwrap();
}
None => {},
}
},
_ = tick_delay => {
_event_tx.send(Action::Tick).unwrap();
},
_ = render_delay => {
_event_tx.send(Action::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!(std::io::stderr(), EnterAlternateScreen, cursor::Hide)?;
if self.mouse {
crossterm::execute!(std::io::stderr(), EnableMouseCapture)?;
}
if self.paste {
crossterm::execute!(std::io::stderr(), 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!(std::io::stderr(), DisableBracketedPaste)?;
}
if self.mouse {
crossterm::execute!(std::io::stderr(), DisableMouseCapture)?;
}
crossterm::execute!(std::io::stderr(), 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<Action> {
self.event_rx.recv().await
}
}
impl Deref for Terminal {
type Target = ratatui::Terminal<CrosstermBackend<Stderr>>;
fn deref(&self) -> &Self::Target {
&self.terminal
}
}
impl DerefMut for Terminal {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.terminal
}
}
impl Drop for Terminal {
fn drop(&mut self) {
self.exit().unwrap();
}
}

View File

@ -1,10 +1,3 @@
pub mod app;
pub mod buffer;
pub mod error;
pub mod interpreter_display;
pub mod log;
pub mod value_display;
use std::{
ops::{Deref, DerefMut},
time::Duration,
@ -19,9 +12,8 @@ use crossterm::{
},
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
};
use dust_lang::Value;
use futures::{FutureExt, StreamExt};
use ratatui::{backend::CrosstermBackend as Backend, Frame};
use ratatui::backend::CrosstermBackend as Backend;
use serde::{Deserialize, Serialize};
use tokio::{
sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
@ -29,13 +21,14 @@ use tokio::{
};
use tokio_util::sync::CancellationToken;
pub trait Elm {
fn update(&mut self, message: Action) -> Result<Option<Action>>;
fn view(&self, frame: &mut Frame);
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 Action {
pub enum Event {
Init,
Quit,
Error,
@ -48,15 +41,14 @@ pub enum Action {
Key(KeyEvent),
Mouse(MouseEvent),
Resize(u16, u16),
UpdateValue(Value),
}
pub struct Tui {
pub terminal: ratatui::Terminal<Backend<std::io::Stderr>>,
pub terminal: ratatui::Terminal<Backend<IO>>,
pub task: JoinHandle<()>,
pub cancellation_token: CancellationToken,
pub event_rx: UnboundedReceiver<Action>,
pub event_tx: UnboundedSender<Action>,
pub event_rx: UnboundedReceiver<Event>,
pub event_tx: UnboundedSender<Event>,
pub frame_rate: f64,
pub tick_rate: f64,
pub mouse: bool,
@ -65,14 +57,15 @@ pub struct Tui {
impl Tui {
pub fn new() -> Result<Self> {
let tick_rate = 4.0;
let frame_rate = 60.0;
let terminal = ratatui::Terminal::new(Backend::new(std::io::stderr()))?;
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,
@ -109,19 +102,25 @@ impl Tui {
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(Action::Init).unwrap();
_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;
@ -132,37 +131,37 @@ impl Tui {
match evt {
CrosstermEvent::Key(key) => {
if key.kind == KeyEventKind::Press {
_event_tx.send(Action::Key(key)).unwrap();
_event_tx.send(Event::Key(key)).unwrap();
}
},
CrosstermEvent::Mouse(mouse) => {
_event_tx.send(Action::Mouse(mouse)).unwrap();
_event_tx.send(Event::Mouse(mouse)).unwrap();
},
CrosstermEvent::Resize(x, y) => {
_event_tx.send(Action::Resize(x, y)).unwrap();
_event_tx.send(Event::Resize(x, y)).unwrap();
},
CrosstermEvent::FocusLost => {
_event_tx.send(Action::FocusLost).unwrap();
_event_tx.send(Event::FocusLost).unwrap();
},
CrosstermEvent::FocusGained => {
_event_tx.send(Action::FocusGained).unwrap();
_event_tx.send(Event::FocusGained).unwrap();
},
CrosstermEvent::Paste(s) => {
_event_tx.send(Action::Paste(s)).unwrap();
_event_tx.send(Event::Paste(s)).unwrap();
},
}
}
Some(Err(_)) => {
_event_tx.send(Action::Error).unwrap();
_event_tx.send(Event::Error).unwrap();
}
None => {},
}
},
_ = tick_delay => {
_event_tx.send(Action::Tick).unwrap();
_event_tx.send(Event::Tick).unwrap();
},
_ = render_delay => {
_event_tx.send(Action::Render).unwrap();
_event_tx.send(Event::Render).unwrap();
},
}
}
@ -171,47 +170,62 @@ impl Tui {
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!(std::io::stderr(), EnterAlternateScreen, cursor::Hide)?;
crossterm::execute!(io(), EnterAlternateScreen, cursor::Hide)?;
if self.mouse {
crossterm::execute!(std::io::stderr(), EnableMouseCapture)?;
crossterm::execute!(io(), EnableMouseCapture)?;
}
if self.paste {
crossterm::execute!(std::io::stderr(), EnableBracketedPaste)?;
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!(std::io::stderr(), DisableBracketedPaste)?;
crossterm::execute!(io(), DisableBracketedPaste)?;
}
if self.mouse {
crossterm::execute!(std::io::stderr(), DisableMouseCapture)?;
crossterm::execute!(io(), DisableMouseCapture)?;
}
crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?;
crossterm::execute!(io(), LeaveAlternateScreen, cursor::Show)?;
crossterm::terminal::disable_raw_mode()?;
}
Ok(())
}
@ -231,13 +245,13 @@ impl Tui {
Ok(())
}
pub async fn next(&mut self) -> Option<Action> {
pub async fn next(&mut self) -> Option<Event> {
self.event_rx.recv().await
}
}
impl Deref for Tui {
type Target = ratatui::Terminal<Backend<std::io::Stderr>>;
type Target = ratatui::Terminal<Backend<IO>>;
fn deref(&self) -> &Self::Target {
&self.terminal

169
src/bin/tui/utils.rs Normal file
View File

@ -0,0 +1,169 @@
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}"
)
}

View File

@ -1,52 +0,0 @@
use dust_lang::Value;
use ratatui::{
style::{Color, Style},
widgets::{Block, Borders, List, Paragraph},
Frame,
};
use crate::{Action, Elm, Result};
pub struct ValueDisplay {
value: Value,
}
impl ValueDisplay {
pub fn new(value: Value) -> Self {
ValueDisplay { value }
}
}
impl Elm for ValueDisplay {
fn update(&mut self, _message: Action) -> Result<Option<Action>> {
Ok(None)
}
fn view(&self, frame: &mut Frame) {
match &self.value {
Value::List(list) => {
let widget = List::new(list.items().iter().map(|value| value.to_string()))
.block(Block::default().title("list").borders(Borders::all()));
frame.render_widget(widget, frame.size());
}
Value::Map(_) => todo!(),
Value::Function(_) => todo!(),
Value::String(string) => {
let widget =
Paragraph::new(string.as_str()).style(Style::default().fg(Color::Green));
frame.render_widget(widget, frame.size());
}
Value::Float(_) => todo!(),
Value::Integer(integer) => {
let widget =
Paragraph::new(integer.to_string()).style(Style::default().fg(Color::Red));
frame.render_widget(widget, frame.size());
}
Value::Boolean(_) => todo!(),
Value::Option(_) => todo!(),
}
}
}

View File

@ -1,4 +1,25 @@
use crate::{BuiltInFunction, Map, Result, Type, Value};
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;
@ -8,11 +29,15 @@ impl BuiltInFunction for Output {
}
fn run(&self, arguments: &[Value], _context: &Map) -> Result<Value> {
for argument in arguments {
println!("{argument}");
Error::expect_argument_amount(self, 1, arguments.len())?;
let value = arguments.first().unwrap();
if let OutputMode::Normal = *OUTPUT_MODE {
println!("{value}");
}
Ok(Value::Option(None))
Ok(Value::default())
}
fn r#type(&self) -> Type {

View File

@ -41,9 +41,6 @@ pub fn interpret(source: &str) -> Result<Value> {
/// );
/// ```
pub fn interpret_with_context(source: &str, context: Map) -> Result<Value> {
let mut parser = Parser::new();
parser.set_language(language())?;
let mut interpreter = Interpreter::new(context)?;
let value = interpreter.run(source)?;
@ -91,7 +88,7 @@ impl Interpreter {
if let Some(abstract_tree) = &self.abstract_tree {
abstract_tree.run(source, &self.context)
} else {
Ok(Value::Option(None))
Ok(Value::none())
}
}

View File

@ -37,13 +37,13 @@ impl Function {
&self.r#type
}
pub fn return_type(&self) -> Result<&Type> {
pub fn return_type(&self) -> &Type {
match &self.r#type {
Type::Function {
parameter_types: _,
return_type,
} => Ok(return_type.as_ref()),
_ => todo!(),
} => return_type.as_ref(),
_ => &Type::None,
}
}
@ -65,10 +65,31 @@ impl Function {
impl Display for Function {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"Function {{ parameters: {:?}, body: {:?} }}",
self.parameters, self.body
)
write!(f, "(")?;
let (parameter_types, return_type) = if let Type::Function {
parameter_types,
return_type,
} = &self.r#type
{
(parameter_types, return_type)
} else {
return Err(fmt::Error);
};
for (index, (parameter, r#type)) in self
.parameters
.iter()
.zip(parameter_types.iter())
.enumerate()
{
write!(f, "{} <{}>", parameter.inner(), r#type)?;
if index != self.parameters.len() - 1 {
write!(f, ", ")?;
}
}
write!(f, ") -> {}", return_type)
}
}

View File

@ -41,7 +41,7 @@ pub enum Value {
impl Default for Value {
fn default() -> Self {
Value::Option(None)
Value::none()
}
}
@ -87,6 +87,10 @@ impl Value {
r#type
}
pub fn none() -> Self {
Value::Option(None)
}
pub fn is_string(&self) -> bool {
matches!(self, Value::String(_))
}

View File

@ -39,6 +39,7 @@
[
"async"
"else"
"else if"
"false"
"for"
"if"