mirror of
https://github.com/andwati/ratatui-json-editor.git
synced 2026-10-18 08:52:52 +00:00
mvp
This commit is contained in:
@@ -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(())
|
||||
+127
-2
@@ -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<dyn Error>> {
|
||||
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<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<bool> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::<ListItem>::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]
|
||||
}
|
||||
Reference in New Issue
Block a user