-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
630 additions
and
17 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
use serde_json::Result; | ||
use std::collections::HashMap; | ||
|
||
pub enum CurrentScreen { | ||
Main, | ||
Editing, | ||
} | ||
|
||
pub enum CurrentlyEditing { | ||
Key, | ||
Value, | ||
} | ||
|
||
pub struct App { | ||
pub key_input: String, // the currently being edited json key. | ||
pub value_input: String, // the currently being edited json value. | ||
pub pairs: HashMap<String, String>, // The representation of our key and value pairs with serde Serialize support | ||
pub current_screen: CurrentScreen, // the current screen the user is looking at, and will later determine what is rendered. | ||
pub currently_editing: Option<CurrentlyEditing>, // the optional state containing which of the key or value pair the user is editing. It is an option, because when the user is not directly editing a key-value pair, this will be set to `None`. | ||
} | ||
|
||
impl App { | ||
pub fn new() -> App { | ||
App { | ||
key_input: String::new(), | ||
value_input: String::new(), | ||
pairs: HashMap::new(), | ||
current_screen: CurrentScreen::Main, | ||
currently_editing: None, | ||
} | ||
} | ||
|
||
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; | ||
} | ||
|
||
pub fn toggle_editing(&mut self) { | ||
if let Some(edit_mode) = &self.currently_editing { | ||
match edit_mode { | ||
CurrentlyEditing::Key => self.currently_editing = Some(CurrentlyEditing::Value), | ||
CurrentlyEditing::Value => self.currently_editing = Some(CurrentlyEditing::Key), | ||
}; | ||
} else { | ||
self.currently_editing = Some(CurrentlyEditing::Key); | ||
} | ||
} | ||
|
||
pub fn print_json(&self) -> Result<()> { | ||
let output = serde_json::to_string(&self.pairs)?; | ||
println!("{}", output); | ||
Ok(()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
pub(super) mod app; | ||
pub(super) mod ratatui; | ||
pub(super) mod ui; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
use std::{error::Error, io}; | ||
|
||
use crossterm::{ | ||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, | ||
execute, | ||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, | ||
}; | ||
use ratatui::{ | ||
backend::{Backend, CrosstermBackend}, | ||
Terminal, | ||
}; | ||
|
||
use super::{ | ||
app::{App, CurrentScreen, CurrentlyEditing}, | ||
ui::ui, | ||
}; | ||
|
||
// TODO: 画面の枠をつくっていく | ||
// TODO: その中でfzf-makeの実行を試みる | ||
pub fn main() -> Result<(), Box<dyn Error>> { | ||
// setup terminal | ||
enable_raw_mode()?; | ||
let mut stderr = io::stderr(); // This is a special case. Normally using stdout is fine | ||
execute!(stderr, EnterAlternateScreen, EnableMouseCapture)?; | ||
let backend = CrosstermBackend::new(stderr); | ||
let mut terminal = Terminal::new(backend)?; | ||
|
||
// create app and run it | ||
let mut app = App::new(); | ||
let res = run_app(&mut terminal, &mut app); | ||
|
||
// restore terminal | ||
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 { | ||
// Skip events that are not KeyEventKind::Press | ||
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; | ||
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); | ||
} | ||
} | ||
} | ||
} | ||
_ => {} | ||
}, | ||
_ => {} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
use ratatui::{ | ||
backend::Backend, | ||
layout::{Constraint, Direction, Layout, Rect}, | ||
style::{Color, Style}, | ||
text::{Line, Span, Text}, | ||
widgets::{Block, Borders, List, ListItem, Paragraph}, | ||
Frame, | ||
}; | ||
|
||
use super::app::{App, CurrentScreen, CurrentlyEditing}; | ||
|
||
pub fn ui<B: Backend>(f: &mut Frame<B>, app: &App) { | ||
// Create the layout sections. | ||
let chunks = Layout::default() | ||
.direction(Direction::Vertical) | ||
.constraints([ | ||
Constraint::Length(3), | ||
Constraint::Min(1), | ||
Constraint::Length(3), | ||
]) | ||
.split(f.size()); | ||
|
||
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); | ||
|
||
f.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); | ||
|
||
f.render_widget(list, chunks[1]); | ||
let current_navigation_text = vec![ | ||
// The first half of the text | ||
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)) | ||
} | ||
} | ||
.to_owned(), | ||
// A white divider bar to separate the two sections | ||
Span::styled(" | ", Style::default().fg(Color::White)), | ||
// The final section of the text, with hints on what the user is editing | ||
{ | ||
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), | ||
), | ||
} | ||
}; | ||
|
||
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]); | ||
|
||
f.render_widget(mode_footer, footer_chunks[0]); | ||
f.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, f.size()); | ||
f.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); | ||
f.render_widget(key_text, popup_chunks[0]); | ||
|
||
let value_text = Paragraph::new(app.value_input.clone()).block(value_block); | ||
f.render_widget(value_text, popup_chunks[1]); | ||
} | ||
} | ||
|
||
/// helper function to create a centered rect using up certain percentage of the available rect `r` | ||
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { | ||
// Cut the given rectangle into three vertical pieces | ||
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); | ||
|
||
// Then cut the middle vertical piece into three width-wise pieces | ||
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] // Return the middle chunk | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
use crate::usecases::fzf_make_ratatui::ratatui; | ||
use crate::usecases::usecase::Usecase; | ||
|
||
pub struct FzfMakeRatatui; | ||
|
||
impl FzfMakeRatatui { | ||
pub fn new() -> Self { | ||
Self {} | ||
} | ||
} | ||
|
||
impl Usecase for FzfMakeRatatui { | ||
fn command_str(&self) -> Vec<&'static str> { | ||
vec!["--r", "-r", "r"] | ||
} | ||
|
||
// TODO: ratatuiのUIが起動するようにする | ||
// まずはtutorialのコードが動くようにする https://github.com/ratatui-org/ratatui-book/tree/main/src/tutorial/json-editor/ratatui-json-editor-app | ||
// そこからUIを少しずつ作っていく | ||
fn run(&self) { | ||
let _ = ratatui::main(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters