Initial commit
This commit is contained in:
commit
cf6acd9ae7
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
2277
Cargo.lock
generated
Normal file
2277
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
Cargo.toml
Normal file
24
Cargo.toml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "dust_tui"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.4.12", features = ["derive"] }
|
||||||
|
color-eyre = "0.6.2"
|
||||||
|
crossterm = { version = "0.27.0", features = ["serde", "event-stream"] }
|
||||||
|
directories = "5.0.1"
|
||||||
|
dust-lang = "0.3.9"
|
||||||
|
futures = "0.3.30"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
ratatui = "0.25.0"
|
||||||
|
serde = "1.0.193"
|
||||||
|
serde_derive = "1.0.193"
|
||||||
|
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"
|
||||||
|
tracing = "0.1.40"
|
||||||
|
tracing-error = "0.2.0"
|
||||||
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
94
src/app.rs
Normal file
94
src/app.rs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
use ratatui::{widgets::Paragraph, Frame};
|
||||||
|
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||||
|
|
||||||
|
use crate::{Action, Event, Result, Tui};
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
action_rx: UnboundedReceiver<Action>,
|
||||||
|
action_tx: UnboundedSender<Action>,
|
||||||
|
should_quit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new(action_rx: UnboundedReceiver<Action>, action_tx: UnboundedSender<Action>) -> Self {
|
||||||
|
App {
|
||||||
|
action_rx,
|
||||||
|
action_tx,
|
||||||
|
should_quit: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&mut self) -> Result<()> {
|
||||||
|
let mut tui = Tui::new()?.tick_rate(4.0).frame_rate(30.0);
|
||||||
|
|
||||||
|
tui.enter()?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if self.should_quit {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let event = if let Some(event) = tui.next().await {
|
||||||
|
event
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Event::Quit => self.action_tx.send(Action::Quit)?,
|
||||||
|
Event::Tick => self.action_tx.send(Action::Tick)?,
|
||||||
|
Event::Render => self.action_tx.send(Action::Render)?,
|
||||||
|
Event::Key(_) => {
|
||||||
|
let action = self.handle_event(event)?;
|
||||||
|
|
||||||
|
if let Some(action) = action {
|
||||||
|
self.action_tx.send(action.clone())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
while let Ok(action) = self.action_rx.try_recv() {
|
||||||
|
// application update
|
||||||
|
self.update(action.clone());
|
||||||
|
// render only when we receive Action::Render
|
||||||
|
if let Action::Render = action {
|
||||||
|
tui.draw(|frame| {
|
||||||
|
self.ui(frame);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tui.exit()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, frame: &mut Frame) {
|
||||||
|
frame.render_widget(Paragraph::new("app"), frame.size())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_event(&self, event: Event) -> Result<Option<Action>> {
|
||||||
|
use crossterm::event::KeyCode::*;
|
||||||
|
use Event::*;
|
||||||
|
|
||||||
|
let action = if let Key(key) = &event {
|
||||||
|
match key.code {
|
||||||
|
Esc => Some(Action::Quit),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, action: Action) {
|
||||||
|
match action {
|
||||||
|
Action::Quit => self.should_quit = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
182
src/editor.rs
Normal file
182
src/editor.rs
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
use dust_lang::Result;
|
||||||
|
use ratatui::{
|
||||||
|
layout::{Constraint, Direction, Layout},
|
||||||
|
prelude::*,
|
||||||
|
style::{Modifier, Style},
|
||||||
|
text::{Line, Span},
|
||||||
|
widgets::{Block, Borders, Paragraph},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
fmt::Display,
|
||||||
|
fs::File,
|
||||||
|
io::{self, Write},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
use tui_textarea::{CursorMove, Input, Key, TextArea};
|
||||||
|
|
||||||
|
use super::Action;
|
||||||
|
|
||||||
|
pub struct Editor<'a> {
|
||||||
|
current: usize,
|
||||||
|
buffers: Vec<Buffer<'a>>,
|
||||||
|
message: Option<Cow<'static, str>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Editor<'a> {
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
current: 0,
|
||||||
|
buffers: Vec::new(),
|
||||||
|
message: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_buffer(&self) -> &Buffer {
|
||||||
|
&self.buffers[self.current]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_buffer(&mut self, buffer: Buffer<'a>) {
|
||||||
|
self.buffers.push(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(&mut self, frame: &mut Frame, areas: &[Rect]) -> Option<Action> {
|
||||||
|
let buffer = &self.buffers[self.current];
|
||||||
|
let textarea = &buffer.textarea;
|
||||||
|
let widget = textarea.widget();
|
||||||
|
|
||||||
|
frame.render_widget(widget, areas[0]);
|
||||||
|
|
||||||
|
// Render status line
|
||||||
|
let modified = if buffer.modified { " [modified]" } else { "" };
|
||||||
|
let slot = format!("[{}/{}]", self.current + 1, self.buffers.len());
|
||||||
|
let path_text = if let Some(path) = &buffer.path {
|
||||||
|
format!(" {}{} ", path.display(), modified)
|
||||||
|
} else {
|
||||||
|
"scratch".to_string()
|
||||||
|
};
|
||||||
|
let (row, col) = textarea.cursor();
|
||||||
|
let cursor = format!("({},{})", row + 1, col + 1);
|
||||||
|
|
||||||
|
let status_chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(slot.len() as u16),
|
||||||
|
Constraint::Min(1),
|
||||||
|
Constraint::Length(cursor.len() as u16),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(areas[1]);
|
||||||
|
let status_style = Style::default().add_modifier(Modifier::REVERSED);
|
||||||
|
frame.render_widget(Paragraph::new(slot).style(status_style), status_chunks[0]);
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(path_text).style(status_style),
|
||||||
|
status_chunks[1],
|
||||||
|
);
|
||||||
|
frame.render_widget(Paragraph::new(cursor).style(status_style), status_chunks[2]);
|
||||||
|
|
||||||
|
// Render message at bottom
|
||||||
|
let message = if let Some(message) = self.message.take() {
|
||||||
|
Line::from(Span::raw(message))
|
||||||
|
} else {
|
||||||
|
Line::from(vec![
|
||||||
|
Span::raw("Press "),
|
||||||
|
Span::styled("^Q", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(" to quit, "),
|
||||||
|
Span::styled("^S", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(" to save, "),
|
||||||
|
Span::styled("^G", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(" to search, "),
|
||||||
|
Span::styled("^T", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(" to switch buffer "),
|
||||||
|
Span::styled("^R", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(" to run"),
|
||||||
|
])
|
||||||
|
};
|
||||||
|
frame.render_widget(Paragraph::new(message), areas[2]);
|
||||||
|
|
||||||
|
match crossterm::event::read().unwrap().into() {
|
||||||
|
Input {
|
||||||
|
key: Key::Char('r'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => return Some(Action::Submit),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('q'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => return Some(Action::Quit),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('t'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.current = (self.current + 1) % self.buffers.len();
|
||||||
|
self.message = Some(format!("Switched to buffer #{}", self.current + 1).into());
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('s'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.buffers[self.current].save().unwrap();
|
||||||
|
self.message = Some("Saved!".into());
|
||||||
|
}
|
||||||
|
input => {
|
||||||
|
let buffer = &mut self.buffers[self.current];
|
||||||
|
buffer.modified = buffer.textarea.input(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Buffer<'a> {
|
||||||
|
textarea: TextArea<'a>,
|
||||||
|
path: Option<PathBuf>,
|
||||||
|
modified: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Buffer<'a> {
|
||||||
|
pub fn new(content: String) -> Result<Self> {
|
||||||
|
let mut textarea = TextArea::new(content.lines().map(|line| line.to_string()).collect());
|
||||||
|
|
||||||
|
textarea.set_line_number_style(Style::default().fg(Color::DarkGray));
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
textarea,
|
||||||
|
path: None,
|
||||||
|
modified: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn content(&self) -> String {
|
||||||
|
self.textarea.lines().join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&mut self) -> io::Result<()> {
|
||||||
|
if !self.modified {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = if let Some(path) = &self.path {
|
||||||
|
File::create(path)?
|
||||||
|
} else {
|
||||||
|
File::create("/tmp/dust_buffer")?
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut writer = io::BufWriter::new(file);
|
||||||
|
|
||||||
|
for line in self.textarea.lines() {
|
||||||
|
writer.write_all(line.as_bytes())?;
|
||||||
|
writer.write_all(b"\n")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.modified = false;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
3
src/error.rs
Normal file
3
src/error.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
pub enum Error {}
|
263
src/lib.rs
Normal file
263
src/lib.rs
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
pub mod app;
|
||||||
|
pub mod error;
|
||||||
|
pub mod log;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
#[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),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Tui {
|
||||||
|
pub terminal: ratatui::Terminal<Backend<std::io::Stderr>>,
|
||||||
|
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 = 4.0;
|
||||||
|
let frame_rate = 60.0;
|
||||||
|
let terminal = ratatui::Terminal::new(Backend::new(std::io::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(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!(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<Event> {
|
||||||
|
self.event_rx.recv().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Tui {
|
||||||
|
type Target = ratatui::Terminal<Backend<std::io::Stderr>>;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
84
src/log.rs
Normal file
84
src/log.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_directory() -> Option<ProjectDirs> {
|
||||||
|
ProjectDirs::from("io", "jeffa", env!("CARGO_PKG_NAME"))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
};
|
||||||
|
}
|
13
src/main.rs
Normal file
13
src/main.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
use dust_tui::app::App;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let (action_tx, action_rx) = mpsc::unbounded_channel();
|
||||||
|
let mut app = App::new(action_rx, action_tx);
|
||||||
|
let run_result = app.run().await;
|
||||||
|
|
||||||
|
if let Err(report) = run_result {
|
||||||
|
eprintln!("{report}")
|
||||||
|
}
|
||||||
|
}
|
33
src/output_display.rs
Normal file
33
src/output_display.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
use dust_lang::Value;
|
||||||
|
use ratatui::{prelude::Rect, widgets::Paragraph, Frame};
|
||||||
|
|
||||||
|
pub struct OutputDisplay {
|
||||||
|
values: Vec<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputDisplay {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
OutputDisplay { values: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_value(&mut self, value: Value) {
|
||||||
|
self.values.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(&self, frame: &mut Frame, area: Rect) {
|
||||||
|
for value in &self.values {
|
||||||
|
match value {
|
||||||
|
Value::List(_) => todo!(),
|
||||||
|
Value::Map(_) => todo!(),
|
||||||
|
Value::Function(_) => todo!(),
|
||||||
|
Value::String(string) => frame.render_widget(Paragraph::new(string.as_str()), area),
|
||||||
|
Value::Float(_) => todo!(),
|
||||||
|
Value::Integer(integer) => {
|
||||||
|
frame.render_widget(Paragraph::new(integer.to_string()), area)
|
||||||
|
}
|
||||||
|
Value::Boolean(_) => todo!(),
|
||||||
|
Value::Option(_) => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
0
src/search_box.rs
Normal file
0
src/search_box.rs
Normal file
Loading…
Reference in New Issue
Block a user