Skip to content

Commit

Permalink
call ratatui by fzf-make -r/--r/r
Browse files Browse the repository at this point in the history
  • Loading branch information
kyu08 committed Oct 17, 2023
1 parent 9d134cc commit de0e16b
Show file tree
Hide file tree
Showing 9 changed files with 630 additions and 17 deletions.
277 changes: 261 additions & 16 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ regex = "1.7.1"
skim = "0.10.4"
uuid = { version = "1.4.1", features = ["serde", "v4"] }
colored = "2"
ratatui = "0.23.0"
crossterm = "0.27.0"
serde = { version = "1.0.181", features = ["derive"] }
serde_json = "1.0.104"
3 changes: 2 additions & 1 deletion src/controller/controller_main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::sync::Arc;
use std::{collections::HashMap, env};

use crate::usecases::{fzf_make_main, help, invalid_arg, usecase, version};
use crate::usecases::{fzf_make_main, fzf_make_ratatui_main, help, invalid_arg, usecase, version};

pub fn run() {
let command_line_args = env::args().collect();
Expand Down Expand Up @@ -33,6 +33,7 @@ fn usecases() -> HashMap<&'static str, Arc<dyn usecase::Usecase>> {
Arc::new(help::Help::new()),
Arc::new(invalid_arg::InvalidArg::new()),
Arc::new(version::Version::new()),
Arc::new(fzf_make_ratatui_main::FzfMakeRatatui::new()),
];

let mut usecases_hash_map = HashMap::new();
Expand Down
58 changes: 58 additions & 0 deletions src/usecases/fzf_make_ratatui/app.rs
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(())
}
}
3 changes: 3 additions & 0 deletions src/usecases/fzf_make_ratatui/mod.rs
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;
122 changes: 122 additions & 0 deletions src/usecases/fzf_make_ratatui/ratatui.rs
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);
}
}
}
}
_ => {}
},
_ => {}
}
}
}
}
155 changes: 155 additions & 0 deletions src/usecases/fzf_make_ratatui/ui.rs
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
}
23 changes: 23 additions & 0 deletions src/usecases/fzf_make_ratatui_main.rs
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();
}
}
2 changes: 2 additions & 0 deletions src/usecases/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub(super) mod fzf_make;
pub(super) mod fzf_make_main;
pub(super) mod fzf_make_ratatui;
pub(super) mod fzf_make_ratatui_main;
pub(super) mod help;
pub(super) mod invalid_arg;
pub(super) mod usecase;
Expand Down

0 comments on commit de0e16b

Please sign in to comment.