feat: add cr*d via table > update will be implemented tomorrow
This commit is contained in:
parent
035fb15a9d
commit
9f7953740f
244
src/lib.rs
244
src/lib.rs
@ -1,16 +1,20 @@
|
||||
use std::sync::OnceLock;
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use color_eyre::Result;
|
||||
use color_eyre::{Result, eyre::eyre};
|
||||
|
||||
use ratatui::crossterm::event::KeyModifiers;
|
||||
use ratatui::layout::Flex;
|
||||
use ratatui::prelude::*;
|
||||
|
||||
use ratatui::widgets::{Cell, Clear, HighlightSpacing, Paragraph, Row, Table, TableState};
|
||||
use ratatui::{
|
||||
DefaultTerminal, Frame,
|
||||
crossterm::event::{self, Event, KeyCode},
|
||||
style::Color,
|
||||
symbols::border,
|
||||
text::Line,
|
||||
widgets::{Block, HighlightSpacing, List, ListItem, ListState, StatefulWidget, Widget},
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@ -19,106 +23,167 @@ use crate::log::Item;
|
||||
pub mod log;
|
||||
|
||||
pub static APP_NAME: &str = "lw";
|
||||
static CONFIG_PATH: OnceLock<PathBuf> = OnceLock::new();
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct App {
|
||||
logs: Vec<Item>,
|
||||
config: PathBuf,
|
||||
#[serde(skip)]
|
||||
state: ListState,
|
||||
delete: Option<usize>,
|
||||
#[serde(skip)]
|
||||
input: String,
|
||||
#[serde(skip)]
|
||||
state: TableState,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
impl Default for App {
|
||||
#[allow(clippy::expect_used)]
|
||||
fn default() -> Self {
|
||||
let config = Self::config_path();
|
||||
Self::new(config.to_owned()).expect("could not create app struct with default config")
|
||||
}
|
||||
}
|
||||
fn popup_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect {
|
||||
let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center);
|
||||
let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center);
|
||||
let [area] = vertical.areas(area);
|
||||
let [area] = horizontal.areas(area);
|
||||
area
|
||||
}
|
||||
impl App {
|
||||
pub fn new(config: PathBuf) -> Result<Self> {
|
||||
if config.exists()
|
||||
&& let Ok(v) = fs::read_to_string(config)
|
||||
&& let Ok(app) = serde_json::from_str(&v)
|
||||
{
|
||||
return Ok(app);
|
||||
}
|
||||
Err(eyre!("failed to read config"))
|
||||
}
|
||||
#[allow(clippy::expect_used)]
|
||||
pub fn config_path() -> &'static PathBuf {
|
||||
CONFIG_PATH.get_or_init(|| {
|
||||
if cfg!(windows) {
|
||||
let base = std::env::var("APPDATA").unwrap_or_else(|_| ".".to_string());
|
||||
let dir = PathBuf::from(base).join(APP_NAME);
|
||||
|
||||
if !dir.exists() {
|
||||
fs::create_dir_all(&dir).expect("failed to create app directory");
|
||||
}
|
||||
dir.join("config.json")
|
||||
} else {
|
||||
let base = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||
let dir = PathBuf::from(base).join(".config").join(APP_NAME);
|
||||
if !dir.exists() {
|
||||
fs::create_dir_all(&dir).expect("failed to create app directory");
|
||||
}
|
||||
dir.join("config.json")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame) {
|
||||
if let Some(item) = self.logs.last()
|
||||
&& item.content().is_empty()
|
||||
{
|
||||
let block = Block::bordered().title("New");
|
||||
let area = popup_area(frame.area(), 60, 20);
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(
|
||||
Paragraph::new(Text::from(self.input.clone())).block(block),
|
||||
area,
|
||||
);
|
||||
}
|
||||
self.render(frame.area(), frame.buffer_mut());
|
||||
}
|
||||
|
||||
pub fn run(&mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
// terminal.draw(render)?;
|
||||
terminal.draw(|frame| self.draw(frame))?;
|
||||
if let Ok(event) = event::read()
|
||||
&& let Event::Key(key_event) = event
|
||||
&& key_event.kind == event::KeyEventKind::Press
|
||||
{
|
||||
if let Some(item) = self.logs.last()
|
||||
&& item.content().is_empty()
|
||||
{
|
||||
match key_event.code {
|
||||
KeyCode::Char('o')
|
||||
if key_event.modifiers.contains(KeyModifiers::CONTROL) =>
|
||||
{
|
||||
self.update(item.id(), self.input.clone());
|
||||
self.input = String::new();
|
||||
self.save()?;
|
||||
continue;
|
||||
}
|
||||
KeyCode::Char(key) => {
|
||||
self.input.push(key);
|
||||
continue;
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
self.input.truncate(self.input.len() - 1);
|
||||
continue;
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
self.remove(item.id());
|
||||
self.input = String::new();
|
||||
self.save()?;
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
match key_event.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => break Ok(()),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.state.select_next(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.state.select_previous(),
|
||||
KeyCode::Char('g') | KeyCode::Home => self.state.select_first(),
|
||||
KeyCode::Char('G') | KeyCode::End => self.state.select_last(),
|
||||
KeyCode::Char('o') => self.add(Item::new()),
|
||||
KeyCode::Char('d') => {
|
||||
let curr = self.state.selected();
|
||||
match curr {
|
||||
None => {
|
||||
self.delete = None;
|
||||
continue;
|
||||
}
|
||||
Some(c) => {
|
||||
if self.delete == curr {
|
||||
let id = self.logs[c].id();
|
||||
self.remove(id);
|
||||
self.save()?;
|
||||
} else {
|
||||
self.delete = curr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// match event::read() {
|
||||
// Ok(event) => match event {
|
||||
// Event::Key(key_event) => match key_event.kind {
|
||||
// event::KeyEventKind::Press => {
|
||||
// if key_event.code == KeyCode::Char('q') {
|
||||
// break Ok(());
|
||||
// }
|
||||
// }
|
||||
// _ => {}
|
||||
// },
|
||||
// _ => {}
|
||||
// },
|
||||
// _ => {}
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, item: Item) -> Result<()> {
|
||||
pub fn add(&mut self, item: Item) {
|
||||
self.logs.push(item);
|
||||
self.save()
|
||||
}
|
||||
|
||||
pub fn update<T: AsRef<str>>(&mut self, id: T, content: T) -> Result<()> {
|
||||
pub fn update<T: AsRef<str>>(&mut self, id: T, content: T) {
|
||||
if let Some(item) = self.logs.iter_mut().find(|i| i.id() == id.as_ref()) {
|
||||
item.update(content.as_ref().to_owned());
|
||||
}
|
||||
self.save()
|
||||
}
|
||||
|
||||
pub fn remove<T: AsRef<str>>(&mut self, id: T) -> Result<()> {
|
||||
pub fn remove<T: AsRef<str>>(&mut self, id: T) {
|
||||
self.logs.retain(|i| i.id() != id.as_ref());
|
||||
self.save()
|
||||
}
|
||||
|
||||
pub fn save(&self) -> Result<()> {
|
||||
let output = serde_json::to_string_pretty(&self)?;
|
||||
fs::write(self.config.clone(), output)?;
|
||||
fs::write(Self::config_path(), output)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> Self {
|
||||
#[cfg(unix)]
|
||||
let mut dir = PathBuf::from(std::env::var("HOME").expect("No HOME directory"));
|
||||
#[cfg(windows)]
|
||||
let mut dir = PathBuf::from(std::env::var("APPDATA").expect("No APPDATA directory"));
|
||||
|
||||
dir.push(APP_NAME);
|
||||
|
||||
if !dir.exists() {
|
||||
fs::create_dir_all(dir.clone()).expect("Failed to create App directory");
|
||||
}
|
||||
|
||||
dir.push("config.json");
|
||||
|
||||
if dir.exists()
|
||||
&& let Ok(v) = fs::read_to_string(&dir)
|
||||
&& let Ok(app) = serde_json::from_str(&v)
|
||||
{
|
||||
return app;
|
||||
}
|
||||
|
||||
Self {
|
||||
logs: vec![],
|
||||
config: dir,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut App {
|
||||
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
|
||||
where
|
||||
@ -127,7 +192,7 @@ impl Widget for &mut App {
|
||||
let title = Line::from(" Log Your Work ".bold());
|
||||
|
||||
let instructions = Line::from(vec![
|
||||
Span::raw(" Add "),
|
||||
Span::raw(" New "),
|
||||
Span::styled(
|
||||
"<O>",
|
||||
Style::default()
|
||||
@ -167,28 +232,53 @@ impl Widget for &mut App {
|
||||
let block = Block::bordered()
|
||||
.title(title.centered())
|
||||
.title_bottom(instructions.centered())
|
||||
.border_set(border::THICK);
|
||||
.title_style(Style::new().blue())
|
||||
.border_set(border::THICK)
|
||||
.border_style(Style::new().dark_gray());
|
||||
|
||||
let items: Vec<ListItem> = self
|
||||
let header = ["Log", "Modified", "Created"]
|
||||
.into_iter()
|
||||
.map(Cell::from)
|
||||
.collect::<Row>()
|
||||
.style(Style::default().fg(Color::DarkGray).bold())
|
||||
.height(1);
|
||||
|
||||
let items: Vec<Row> = self
|
||||
.logs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, item)| {
|
||||
ListItem::new(Line::styled(format!("{}", item.content()), Color::White)).bg(
|
||||
if i % 2 == 0 {
|
||||
Color::Black
|
||||
} else {
|
||||
Color::DarkGray
|
||||
},
|
||||
)
|
||||
.map(|item| {
|
||||
[
|
||||
item.content(),
|
||||
item.modified().format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||
item.created().format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|c| Cell::from(Text::from(c)))
|
||||
.collect::<Row>()
|
||||
.style(Style::new().fg(Color::White))
|
||||
.height(4)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let list = List::new(items)
|
||||
.block(block)
|
||||
.highlight_symbol(">")
|
||||
.highlight_spacing(HighlightSpacing::Always);
|
||||
let table = Table::new(
|
||||
items,
|
||||
[
|
||||
Constraint::Min(200),
|
||||
Constraint::Min(10),
|
||||
Constraint::Min(10),
|
||||
],
|
||||
)
|
||||
.block(block)
|
||||
.header(header)
|
||||
.highlight_symbol(">")
|
||||
.row_highlight_style(Style::new().bold().fg(Color::Green))
|
||||
.highlight_spacing(HighlightSpacing::Always);
|
||||
|
||||
StatefulWidget::render(list, area, buf, &mut self.state);
|
||||
// let list = List::new(items)
|
||||
// .block(block)
|
||||
// .highlight_symbol(">")
|
||||
// .highlight_spacing(HighlightSpacing::Always);
|
||||
//
|
||||
StatefulWidget::render(table, area, buf, &mut self.state);
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,13 @@ impl Item {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn modified(&self) -> DateTime<Local> {
|
||||
self.modified
|
||||
}
|
||||
pub fn created(&self) -> DateTime<Local> {
|
||||
self.created
|
||||
}
|
||||
|
||||
pub fn content(&self) -> String {
|
||||
self.content.clone()
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use color_eyre::Result;
|
||||
use lw::{App, log::Item};
|
||||
use lw::App;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
|
Loading…
x
Reference in New Issue
Block a user