2nd commit

This commit is contained in:
2026-03-21 12:35:22 +00:00
commit ba2523fc29
6 changed files with 1973 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/target
Generated
+1730
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "klipper-tui"
version = "0.1.0"
edition = "2024"
[dependencies]
ratatui = "0.30.0"
crossterm = "0.29.0"
ratatui-crossterm = "0.1.0"
+19
View File
@@ -0,0 +1,19 @@
pub enum CurrentScreen {
Main,
Editing,
Exiting,
}
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`.
}
+10
View File
@@ -0,0 +1,10 @@
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use std::io::{stdout, Write};
fn main() {
let backend = CrosstermBackend::new(stdout());
let mut terminal = Terminal::new(backend).unwrap();
//todo: setup moonraker interface
//todo: setup custom klipper ui
}
+204
View File
@@ -0,0 +1,204 @@
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap},
Frame,
};
use crate::app::{App, CurrentScreen, CurrentlyEditing};
// ANCHOR: method_sig
pub fn ui(frame: &mut Frame, app: &App) {
// ANCHOR_END: method_sig
// Create the layout sections.
// ANCHOR: ui_layout
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(1),
Constraint::Length(3),
])
.split(frame.area());
// ANCHOR_END: ui_layout
// ANCHOR: title_paragraph
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]);
// ANCHOR_END: title_paragraph
// ANCHOR: key_value_list
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]);
// ANCHOR_END: key_value_list
// ANCHOR: lower_navigation_current_screen
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))
}
CurrentScreen::Exiting => Span::styled("Exiting", Style::default().fg(Color::LightRed)),
}
.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));
// ANCHOR_END: lower_navigation_current_screen
// ANCHOR: lower_navigation_key_hint
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));
// ANCHOR_END: lower_navigation_key_hint
// ANCHOR: lower_navigation_layout
let footer_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[2]);
// ANCHOR_END: lower_navigation_layout
// ANCHOR: lower_navigation_rendering
frame.render_widget(mode_footer, footer_chunks[0]);
frame.render_widget(key_notes_footer, footer_chunks[1]);
// ANCHOR_END: lower_navigation_rendering
// ANCHOR: editing_popup
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);
// ANCHOR_END: editing_popup
// ANCHOR: popup_layout
let popup_chunks = Layout::default()
.direction(Direction::Horizontal)
.margin(1)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area);
// ANCHOR_END: popup_layout
// ANCHOR: key_value_blocks
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]);
}
// ANCHOR_END: key_value_blocks
// ANCHOR: exit_screen
if let CurrentScreen::Exiting = app.current_screen {
frame.render_widget(Clear, frame.area()); //this clears the entire screen and anything already drawn
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),
);
// the `trim: false` will stop the text from being cut off when over the edge of the block
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);
}
// ANCHOR_END: exit_screen
}
// ANCHOR: centered_rect
/// 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
}