From 303d743f7c9e4f0dbf231448d37a838d4cd522f4 Mon Sep 17 00:00:00 2001 From: andwati Date: Thu, 26 Jun 2025 17:53:13 +0300 Subject: [PATCH] mvp --- README.md | 1 + src/{App.rs => app.rs} | 7 +- src/main.rs | 129 ++++++++++++++++++++++++++++++- src/ui.rs | 172 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 305 insertions(+), 4 deletions(-) create mode 100644 README.md rename src/{App.rs => app.rs} (93%) create mode 100644 src/ui.rs diff --git a/README.md b/README.md new file mode 100644 index 0000000..c733eb1 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# json editor \ No newline at end of file diff --git a/src/App.rs b/src/app.rs similarity index 93% rename from src/App.rs rename to src/app.rs index ebfe80a..c7ea776 100644 --- a/src/App.rs +++ b/src/app.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + pub enum CurrentScreen { Main, Editing, @@ -31,6 +33,7 @@ impl App { pub fn save_key_value(&mut self) { self.pairs .insert(self.key_input.clone(), self.value_input.clone()); + self.key_input = String::new(); self.value_input = String::new(); self.currently_editing = None; @@ -43,11 +46,11 @@ impl App { CurrentlyEditing::Value => self.currently_editing = Some(CurrentlyEditing::Key), }; } else { - self.currently_editing = Some(CurrentlyEditing::Key) + self.currently_editing = Some(CurrentlyEditing::Key); } } - pub fn print_json(&self) -> serde::Result<()> { + pub fn print_json(&self) -> serde_json::Result<()> { let output = serde_json::to_string(&self.pairs)?; println!("{}", output); Ok(()) diff --git a/src/main.rs b/src/main.rs index e7a11a9..5e20af2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,128 @@ -fn main() { - println!("Hello, world!"); +use std::{error::Error, io}; + +use ratatui::{ + Terminal, + backend::{Backend, CrosstermBackend}, + crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, + execute, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, + }, +}; + +mod app; +mod ui; +use crate::{ + app::{App, CurrentScreen, CurrentlyEditing}, + ui::ui, +}; + +fn main() -> Result<(), Box> { + enable_raw_mode()?; + let mut stderr = io::stderr(); + execute!(stderr, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stderr); + let mut terminal = Terminal::new(backend)?; + + let mut app = App::new(); + let res = run_app(&mut terminal, &mut app); + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Ok(do_print) = res { + if do_print { + app.print_json()?; + } + } else if let Err(err) = res { + println!("{err:?}"); + } + + Ok(()) +} + +fn run_app(terminal: &mut Terminal, app: &mut App) -> io::Result { + loop { + terminal.draw(|f| ui(f, app))?; + + if let Event::Key(key) = event::read()? { + if key.kind == event::KeyEventKind::Release { + continue; + } + match app.current_screen { + CurrentScreen::Main => match key.code { + KeyCode::Char('e') => { + app.current_screen = CurrentScreen::Editing; + app.currently_editing = Some(CurrentlyEditing::Key); + } + KeyCode::Char('q') => { + app.current_screen = CurrentScreen::Exiting; + } + _ => {} + }, + CurrentScreen::Exiting => match key.code { + KeyCode::Char('y') => { + return Ok(true); + } + KeyCode::Char('n') | KeyCode::Char('q') => { + return Ok(false); + } + _ => {} + }, + CurrentScreen::Editing if key.kind == KeyEventKind::Press => match key.code { + KeyCode::Enter => { + if let Some(editing) = &app.currently_editing { + match editing { + CurrentlyEditing::Key => { + app.currently_editing = Some(CurrentlyEditing::Value); + } + CurrentlyEditing::Value => { + app.save_key_value(); + app.current_screen = CurrentScreen::Main; + } + } + } + } + KeyCode::Backspace => { + if let Some(editing) = &app.currently_editing { + match editing { + CurrentlyEditing::Key => { + app.key_input.pop(); + } + CurrentlyEditing::Value => { + app.value_input.pop(); + } + } + } + } + KeyCode::Esc => { + app.current_screen = CurrentScreen::Main; + app.currently_editing = None; + } + KeyCode::Tab => { + app.toggle_editing(); + } + KeyCode::Char(value) => { + if let Some(editing) = &app.currently_editing { + match editing { + CurrentlyEditing::Key => { + app.key_input.push(value); + } + CurrentlyEditing::Value => { + app.value_input.push(value); + } + } + } + } + _ => {} + }, + _ => {} + } + } + } } diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..910b97b --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,172 @@ +use ratatui::{ + Frame, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Style}, + text::{Line, Span, Text}, + widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap}, +}; + +use crate::app::{App, CurrentScreen, CurrentlyEditing}; + +pub fn ui(frame: &mut Frame, app: &App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Min(1), + Constraint::Length(3), + ]) + .split(frame.area()); + + let title_block = Block::default() + .borders(Borders::ALL) + .style(Style::default()); + + let title = Paragraph::new(Text::styled( + "Create New Json", + Style::default().fg(Color::Green), + )) + .block(title_block); + + frame.render_widget(title, chunks[0]); + let mut list_items = Vec::::new(); + + for key in app.pairs.keys() { + list_items.push(ListItem::new(Line::from(Span::styled( + format!("{: <25} : {}", key, app.pairs.get(key).unwrap()), + Style::default().fg(Color::Yellow), + )))); + } + + let list = List::new(list_items); + + frame.render_widget(list, chunks[1]); + let current_navigation_text = vec![ + match app.current_screen { + CurrentScreen::Main => Span::styled("Normal Mode", Style::default().fg(Color::Green)), + CurrentScreen::Editing => { + Span::styled("Editing Mode", Style::default().fg(Color::Yellow)) + } + CurrentScreen::Exiting => Span::styled("Exiting", Style::default().fg(Color::LightRed)), + } + .to_owned(), + Span::styled(" | ", Style::default().fg(Color::White)), + { + if let Some(editing) = &app.currently_editing { + match editing { + CurrentlyEditing::Key => { + Span::styled("Editing Json Key", Style::default().fg(Color::Green)) + } + CurrentlyEditing::Value => { + Span::styled("Editing Json Value", Style::default().fg(Color::LightGreen)) + } + } + } else { + Span::styled("Not Editing Anything", Style::default().fg(Color::DarkGray)) + } + }, + ]; + + let mode_footer = Paragraph::new(Line::from(current_navigation_text)) + .block(Block::default().borders(Borders::ALL)); + + let current_keys_hint = { + match app.current_screen { + CurrentScreen::Main => Span::styled( + "(q) to quit / (e) to make new pair", + Style::default().fg(Color::Red), + ), + CurrentScreen::Editing => Span::styled( + "(ESC) to cancel/(Tab) to switch boxes/enter to complete", + Style::default().fg(Color::Red), + ), + CurrentScreen::Exiting => Span::styled( + "(q) to quit / (e) to make new pair", + Style::default().fg(Color::Red), + ), + } + }; + + let key_notes_footer = + Paragraph::new(Line::from(current_keys_hint)).block(Block::default().borders(Borders::ALL)); + + let footer_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(chunks[2]); + + frame.render_widget(mode_footer, footer_chunks[0]); + frame.render_widget(key_notes_footer, footer_chunks[1]); + + if let Some(editing) = &app.currently_editing { + let popup_block = Block::default() + .title("Enter a new key-value pair") + .borders(Borders::NONE) + .style(Style::default().bg(Color::DarkGray)); + + let area = centered_rect(60, 25, frame.area()); + frame.render_widget(popup_block, area); + + let popup_chunks = Layout::default() + .direction(Direction::Horizontal) + .margin(1) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(area); + + let mut key_block = Block::default().title("Key").borders(Borders::ALL); + let mut value_block = Block::default().title("Value").borders(Borders::ALL); + + let active_style = Style::default().bg(Color::LightYellow).fg(Color::Black); + + match editing { + CurrentlyEditing::Key => key_block = key_block.style(active_style), + CurrentlyEditing::Value => value_block = value_block.style(active_style), + }; + + let key_text = Paragraph::new(app.key_input.clone()).block(key_block); + frame.render_widget(key_text, popup_chunks[0]); + + let value_text = Paragraph::new(app.value_input.clone()).block(value_block); + frame.render_widget(value_text, popup_chunks[1]); + } + + if let CurrentScreen::Exiting = app.current_screen { + frame.render_widget(Clear, frame.area()); + let popup_block = Block::default() + .title("Y/N") + .borders(Borders::NONE) + .style(Style::default().bg(Color::DarkGray)); + + let exit_text = Text::styled( + "Would you like to output the buffer as json? (y/n)", + Style::default().fg(Color::Red), + ); + + let exit_paragraph = Paragraph::new(exit_text) + .block(popup_block) + .wrap(Wrap { trim: false }); + + let area = centered_rect(60, 25, frame.area()); + frame.render_widget(exit_paragraph, area); + } +} + +fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ]) + .split(r); + + Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ]) + .split(popup_layout[1])[1] +}