Merge _configs/ into config/ for single configuration directory. Update all path references. Changes: - Move _configs/* to config/ - Update .gitignore for new patterns - No code references to _configs/ found Impact: -1 root directory (layout_conventions.md compliance)
758 lines
19 KiB
Markdown
758 lines
19 KiB
Markdown
# TUI Integration Example Walkthrough
|
|
|
|
Complete step-by-step guide for creating a new tool TUI from scratch using templates.
|
|
|
|
## Overview
|
|
|
|
This walkthrough shows how to create a TUI for an existing tool. We'll use a hypothetical `note-manager` tool as the example, but the same steps apply to any tool.
|
|
|
|
## Step 1: Project Structure Setup
|
|
|
|
Create the following directory structure within your tool's workspace:
|
|
|
|
```
|
|
note-manager/
|
|
├── crates/
|
|
│ ├── note-manager-core/ # Your existing core library
|
|
│ │ ├── Cargo.toml
|
|
│ │ └── src/
|
|
│ │ ├── lib.rs
|
|
│ │ ├── types.rs
|
|
│ │ └── handlers/
|
|
│ │
|
|
│ └── note-manager-tui/ # NEW: TUI crate (create this)
|
|
│ ├── Cargo.toml
|
|
│ ├── src/
|
|
│ │ ├── lib.rs
|
|
│ │ ├── app.rs
|
|
│ │ ├── screens/
|
|
│ │ │ ├── mod.rs
|
|
│ │ │ ├── list.rs
|
|
│ │ │ ├── create.rs
|
|
│ │ │ └── detail.rs
|
|
│ │ └── integration.rs
|
|
│ └── tests/
|
|
│ └── integration_tests.rs
|
|
│
|
|
├── Cargo.toml (workspace root)
|
|
└── Cargo.lock
|
|
```
|
|
|
|
## Step 2: Create Cargo.toml for TUI Crate
|
|
|
|
Create `crates/note-manager-tui/Cargo.toml`:
|
|
|
|
```toml
|
|
[package]
|
|
name = "note-manager-tui"
|
|
version = "0.1.0"
|
|
edition = "2021"
|
|
|
|
[dependencies]
|
|
note-manager-core = { path = "../note-manager-core" }
|
|
tools-tui-shared = { path = "../../../shared/rust-tui" }
|
|
ratatui = { version = "0.30.0-beta.0", features = ["all-widgets"] }
|
|
crossterm = "0.29"
|
|
serde = { version = "1.0", features = ["derive"] }
|
|
tokio = { version = "1.40", features = ["full"] }
|
|
|
|
[lib]
|
|
path = "src/lib.rs"
|
|
|
|
[dev-dependencies]
|
|
tempfile = "3.12"
|
|
```
|
|
|
|
Update workspace root `Cargo.toml` to include the new crate:
|
|
|
|
```toml
|
|
[workspace]
|
|
members = [
|
|
"crates/note-manager-core",
|
|
"crates/note-manager-tui", # Add this line
|
|
]
|
|
```
|
|
|
|
## Step 3: Implement Application State (app.rs)
|
|
|
|
Create `crates/note-manager-tui/src/app.rs` using the template:
|
|
|
|
```rust
|
|
//! Application state for Note Manager TUI
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use std::fmt;
|
|
|
|
/// Screen types - customize based on your tool's screens
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum ScreenType {
|
|
/// List screen - show all notes
|
|
List,
|
|
/// Create screen - create new note
|
|
Create,
|
|
/// Detail screen - show note details
|
|
Detail,
|
|
}
|
|
|
|
impl fmt::Display for ScreenType {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
ScreenType::List => write!(f, "List"),
|
|
ScreenType::Create => write!(f, "Create"),
|
|
ScreenType::Detail => write!(f, "Detail"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Application state - manages all mutable state
|
|
///
|
|
/// # Design Pattern: R-STATE-SEPARATION
|
|
/// All state mutations go through explicit methods. Rendering functions
|
|
/// only read state (take `&AppState`, never `&mut AppState`).
|
|
#[derive(Clone, Debug)]
|
|
pub struct AppState {
|
|
/// Current screen (navigation state)
|
|
pub current_screen: ScreenType,
|
|
|
|
/// Notes collection
|
|
pub notes: Vec<String>,
|
|
|
|
/// Currently selected note index
|
|
pub selected_index: usize,
|
|
|
|
/// Whether state changed (needs redraw)
|
|
pub dirty: bool,
|
|
|
|
/// Whether application should quit
|
|
pub should_quit: bool,
|
|
|
|
/// Form field: note title
|
|
pub form_title: String,
|
|
|
|
/// Form field: note content
|
|
pub form_content: String,
|
|
}
|
|
|
|
impl AppState {
|
|
/// Create new application state
|
|
pub fn new() -> Self {
|
|
AppState {
|
|
current_screen: ScreenType::List,
|
|
notes: Vec::new(),
|
|
selected_index: 0,
|
|
dirty: true,
|
|
should_quit: false,
|
|
form_title: String::new(),
|
|
form_content: String::new(),
|
|
}
|
|
}
|
|
|
|
// === Note Management ===
|
|
|
|
/// Add a new note
|
|
pub fn add_note(&mut self, title: String) {
|
|
self.notes.push(title);
|
|
self.dirty = true;
|
|
}
|
|
|
|
/// Get currently selected note
|
|
pub fn selected_note(&self) -> Option<&String> {
|
|
self.notes.get(self.selected_index)
|
|
}
|
|
|
|
// === Navigation ===
|
|
|
|
/// Select next note in list
|
|
pub fn select_next(&mut self) {
|
|
if !self.notes.is_empty() {
|
|
self.selected_index = (self.selected_index + 1) % self.notes.len();
|
|
self.dirty = true;
|
|
}
|
|
}
|
|
|
|
/// Select previous note in list
|
|
pub fn select_previous(&mut self) {
|
|
if !self.notes.is_empty() {
|
|
self.selected_index = if self.selected_index == 0 {
|
|
self.notes.len() - 1
|
|
} else {
|
|
self.selected_index - 1
|
|
};
|
|
self.dirty = true;
|
|
}
|
|
}
|
|
|
|
/// Switch to a different screen
|
|
pub fn set_screen(&mut self, screen: ScreenType) {
|
|
if self.current_screen != screen {
|
|
self.current_screen = screen;
|
|
self.dirty = true;
|
|
}
|
|
}
|
|
|
|
// === Form Management ===
|
|
|
|
/// Reset form fields to empty
|
|
pub fn reset_form(&mut self) {
|
|
self.form_title.clear();
|
|
self.form_content.clear();
|
|
self.dirty = true;
|
|
}
|
|
|
|
// === Lifecycle ===
|
|
|
|
/// Request application quit
|
|
pub fn request_quit(&mut self) {
|
|
self.should_quit = true;
|
|
}
|
|
|
|
/// Check if state has changed since last render
|
|
pub fn is_dirty(&self) -> bool {
|
|
self.dirty
|
|
}
|
|
|
|
/// Mark state as rendered (clear dirty flag)
|
|
pub fn reset_dirty(&mut self) {
|
|
self.dirty = false;
|
|
}
|
|
|
|
/// Check if should quit
|
|
pub fn should_quit(&self) -> bool {
|
|
self.should_quit
|
|
}
|
|
}
|
|
|
|
impl Default for AppState {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_app_state_new() {
|
|
let state = AppState::new();
|
|
assert_eq!(state.current_screen, ScreenType::List);
|
|
assert!(state.notes.is_empty());
|
|
assert!(state.dirty);
|
|
assert!(!state.should_quit);
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_note() {
|
|
let mut state = AppState::new();
|
|
state.add_note("Test Note".to_string());
|
|
assert_eq!(state.notes.len(), 1);
|
|
assert!(state.dirty);
|
|
}
|
|
|
|
#[test]
|
|
fn test_select_next() {
|
|
let mut state = AppState::new();
|
|
state.add_note("Note 1".to_string());
|
|
state.add_note("Note 2".to_string());
|
|
state.reset_dirty();
|
|
|
|
state.select_next();
|
|
assert_eq!(state.selected_index, 1);
|
|
assert!(state.dirty);
|
|
}
|
|
|
|
#[test]
|
|
fn test_set_screen() {
|
|
let mut state = AppState::new();
|
|
state.reset_dirty();
|
|
|
|
state.set_screen(ScreenType::Create);
|
|
assert_eq!(state.current_screen, ScreenType::Create);
|
|
assert!(state.dirty);
|
|
}
|
|
|
|
#[test]
|
|
fn test_reset_form() {
|
|
let mut state = AppState::new();
|
|
state.form_title = "Test".to_string();
|
|
state.form_content = "Content".to_string();
|
|
state.reset_dirty();
|
|
|
|
state.reset_form();
|
|
assert!(state.form_title.is_empty());
|
|
assert!(state.form_content.is_empty());
|
|
assert!(state.dirty);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Step 4: Implement Screens
|
|
|
|
Create `crates/note-manager-tui/src/screens/list.rs`:
|
|
|
|
```rust
|
|
//! List screen - shows all notes
|
|
|
|
use crate::app::AppState;
|
|
use ratatui::prelude::*;
|
|
use ratatui::widgets::{Block, Borders, List, ListItem};
|
|
use tools_tui_shared::prelude::*;
|
|
|
|
/// List screen - display and navigate notes
|
|
pub struct ListScreen;
|
|
|
|
impl ListScreen {
|
|
/// Render the list screen
|
|
pub fn render(state: &AppState, frame: &mut ratatui::Frame, area: Rect) {
|
|
let theme = Theme::dark();
|
|
|
|
// Handle empty state
|
|
if state.notes.is_empty() {
|
|
let empty_msg = ratatui::widgets::Paragraph::new("No notes yet. Create one to get started.")
|
|
.style(theme.dimmed_style())
|
|
.alignment(ratatui::layout::Alignment::Center);
|
|
frame.render_widget(empty_msg, area);
|
|
return;
|
|
}
|
|
|
|
// Create list items from state
|
|
let items: Vec<ListItem> = state
|
|
.notes
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(idx, title)| {
|
|
let marker = if idx == state.selected_index { "★ " } else { " " };
|
|
ListItem::new(format!("{}{}", marker, title))
|
|
})
|
|
.collect();
|
|
|
|
// Build list widget
|
|
let list = List::new(items)
|
|
.block(
|
|
Block::default()
|
|
.title("Notes")
|
|
.borders(Borders::ALL)
|
|
.border_style(theme.focused_style()),
|
|
)
|
|
.style(theme.normal_style())
|
|
.row_highlight_style(theme.selected_style());
|
|
|
|
// Render to frame
|
|
frame.render_widget(list, area);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_list_screen_empty() {
|
|
let state = AppState::new();
|
|
assert!(state.notes.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_screen_with_notes() {
|
|
let mut state = AppState::new();
|
|
state.add_note("Note 1".to_string());
|
|
state.add_note("Note 2".to_string());
|
|
assert_eq!(state.notes.len(), 2);
|
|
}
|
|
}
|
|
```
|
|
|
|
Create `crates/note-manager-tui/src/screens/create.rs`:
|
|
|
|
```rust
|
|
//! Create screen - form for creating new notes
|
|
|
|
use crate::app::AppState;
|
|
use ratatui::prelude::*;
|
|
use ratatui::widgets::{Block, Borders, Paragraph};
|
|
use tools_tui_shared::prelude::*;
|
|
|
|
/// Create screen - form for creating new notes
|
|
pub struct CreateScreen;
|
|
|
|
impl CreateScreen {
|
|
/// Render the create screen
|
|
pub fn render(state: &AppState, frame: &mut ratatui::Frame, area: Rect) {
|
|
let theme = Theme::dark();
|
|
|
|
// Split area into form and help sections
|
|
let chunks = Layout::default()
|
|
.direction(ratatui::layout::Direction::Vertical)
|
|
.constraints([Constraint::Percentage(70), Constraint::Percentage(30)])
|
|
.split(area);
|
|
|
|
// Form section
|
|
let form_text = vec![
|
|
Line::from(vec![
|
|
Span::styled("Title: ", theme.focused_style()),
|
|
Span::raw(if state.form_title.is_empty() {
|
|
"<empty>".to_string()
|
|
} else {
|
|
state.form_title.clone()
|
|
}),
|
|
]),
|
|
Line::from(""),
|
|
Line::from(vec![
|
|
Span::styled("Content: ", theme.focused_style()),
|
|
Span::raw(if state.form_content.is_empty() {
|
|
"<empty>".to_string()
|
|
} else {
|
|
state.form_content.clone()
|
|
}),
|
|
]),
|
|
];
|
|
|
|
let form = Paragraph::new(form_text)
|
|
.block(
|
|
Block::default()
|
|
.title("Create Note")
|
|
.borders(Borders::ALL)
|
|
.border_style(theme.focused_style()),
|
|
)
|
|
.style(theme.normal_style());
|
|
|
|
frame.render_widget(form, chunks[0]);
|
|
|
|
// Help section
|
|
let help_text = vec![Line::from(vec![
|
|
Span::styled("Tab", theme.selected_style()),
|
|
Span::raw(" - Next field | "),
|
|
Span::styled("Enter", theme.selected_style()),
|
|
Span::raw(" - Create | "),
|
|
Span::styled("Esc", theme.selected_style()),
|
|
Span::raw(" - Cancel"),
|
|
])];
|
|
|
|
let help = Paragraph::new(help_text)
|
|
.block(Block::default().title("Help").borders(Borders::ALL))
|
|
.style(theme.dimmed_style());
|
|
|
|
frame.render_widget(help, chunks[1]);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_create_screen_empty_form() {
|
|
let state = AppState::new();
|
|
assert!(state.form_title.is_empty());
|
|
assert!(state.form_content.is_empty());
|
|
}
|
|
}
|
|
```
|
|
|
|
Create `crates/note-manager-tui/src/screens/detail.rs`:
|
|
|
|
```rust
|
|
//! Detail screen - shows note details
|
|
|
|
use crate::app::AppState;
|
|
use ratatui::prelude::*;
|
|
use ratatui::widgets::{Block, Borders, Paragraph};
|
|
use tools_tui_shared::prelude::*;
|
|
|
|
/// Detail screen - show note details
|
|
pub struct DetailScreen;
|
|
|
|
impl DetailScreen {
|
|
/// Render the detail screen
|
|
pub fn render(state: &AppState, frame: &mut ratatui::Frame, area: Rect) {
|
|
let theme = Theme::dark();
|
|
|
|
if let Some(note) = state.selected_note() {
|
|
let detail_text = vec![
|
|
Line::from(vec![
|
|
Span::styled("Note: ", theme.selected_style()),
|
|
Span::raw(note),
|
|
]),
|
|
Line::from(""),
|
|
Line::from(vec![
|
|
Span::styled("Status: ", theme.selected_style()),
|
|
Span::raw("Active"),
|
|
]),
|
|
];
|
|
|
|
let detail = Paragraph::new(detail_text)
|
|
.block(
|
|
Block::default()
|
|
.title("Details")
|
|
.borders(Borders::ALL)
|
|
.border_style(theme.focused_style()),
|
|
)
|
|
.style(theme.normal_style());
|
|
|
|
frame.render_widget(detail, area);
|
|
} else {
|
|
let empty = Paragraph::new("No note selected")
|
|
.style(theme.dimmed_style())
|
|
.alignment(ratatui::layout::Alignment::Center);
|
|
frame.render_widget(empty, area);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_detail_screen_no_selection() {
|
|
let state = AppState::new();
|
|
assert!(state.selected_note().is_none());
|
|
}
|
|
}
|
|
```
|
|
|
|
Create `crates/note-manager-tui/src/screens/mod.rs`:
|
|
|
|
```rust
|
|
//! Screen implementations
|
|
|
|
pub mod create;
|
|
pub mod detail;
|
|
pub mod list;
|
|
|
|
pub use create::CreateScreen;
|
|
pub use detail::DetailScreen;
|
|
pub use list::ListScreen;
|
|
```
|
|
|
|
## Step 5: Create Library Entry Point (lib.rs)
|
|
|
|
Create `crates/note-manager-tui/src/lib.rs`:
|
|
|
|
```rust
|
|
#![forbid(unsafe_code)]
|
|
#![warn(missing_docs)]
|
|
|
|
//! Note Manager TUI - Terminal user interface for note management
|
|
|
|
pub mod app;
|
|
pub mod screens;
|
|
|
|
pub use app::{AppState, ScreenType};
|
|
|
|
/// Prelude module - convenient imports for TUI usage
|
|
pub mod prelude {
|
|
pub use crate::{AppState, ScreenType};
|
|
pub use crate::screens::{CreateScreen, DetailScreen, ListScreen};
|
|
}
|
|
```
|
|
|
|
## Step 6: Create Integration Module (integration.rs)
|
|
|
|
Create `crates/note-manager-tui/src/integration.rs` to register with multi-tool dashboard:
|
|
|
|
```rust
|
|
//! Integration with tools-dashboard
|
|
|
|
use crate::app::AppState;
|
|
use std::sync::{Arc, Mutex};
|
|
use tools_tui_shared::integration::{ToolMetrics, ToolSummary, ToolSummaryProvider};
|
|
|
|
/// Note Manager TUI integration for tools-dashboard
|
|
pub struct NoteManagerTui {
|
|
/// Shared state
|
|
state: Arc<Mutex<AppState>>,
|
|
}
|
|
|
|
impl NoteManagerTui {
|
|
/// Create new note manager TUI integration
|
|
pub fn new() -> Self {
|
|
NoteManagerTui {
|
|
state: Arc::new(Mutex::new(AppState::new())),
|
|
}
|
|
}
|
|
|
|
/// Get note count
|
|
pub fn note_count(&self) -> usize {
|
|
self.state.lock().unwrap().notes.len()
|
|
}
|
|
}
|
|
|
|
impl Default for NoteManagerTui {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl ToolSummaryProvider for NoteManagerTui {
|
|
fn name(&self) -> &str {
|
|
"note-manager"
|
|
}
|
|
|
|
fn summary(&self) -> Result<ToolSummary, String> {
|
|
let state = self.state.lock().unwrap();
|
|
let metrics = ToolMetrics::new(
|
|
state.notes.len() as u32,
|
|
state.notes.len() as u32,
|
|
0,
|
|
chrono::Utc::now().to_rfc2822(),
|
|
);
|
|
|
|
Ok(ToolSummary::new(
|
|
"note-manager".to_string(),
|
|
"Note Manager".to_string(),
|
|
"Manage notes with TUI interface".to_string(),
|
|
metrics,
|
|
)
|
|
.with_commands(vec![
|
|
"list".to_string(),
|
|
"create".to_string(),
|
|
"delete".to_string(),
|
|
]))
|
|
}
|
|
|
|
fn execute_command(&self, command: &str, _args: &[String]) -> Result<String, String> {
|
|
match command {
|
|
"list" => Ok(format!("Listed {} notes", self.note_count())),
|
|
"create" => Ok("Creating new note...".to_string()),
|
|
"delete" => Ok("Note deleted".to_string()),
|
|
_ => Err(format!("Unknown command: {}", command)),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_note_manager_tui_creation() {
|
|
let tui = NoteManagerTui::new();
|
|
assert_eq!(tui.name(), "note-manager");
|
|
}
|
|
|
|
#[test]
|
|
fn test_note_manager_empty_summary() {
|
|
let tui = NoteManagerTui::new();
|
|
let summary = tui.summary().unwrap();
|
|
assert_eq!(summary.label, "Note Manager");
|
|
}
|
|
}
|
|
```
|
|
|
|
## Step 7: Create Tests
|
|
|
|
Create `crates/note-manager-tui/tests/integration_tests.rs`:
|
|
|
|
```rust
|
|
//! Integration tests for Note Manager TUI
|
|
|
|
use note_manager_tui::{AppState, ScreenType};
|
|
|
|
#[test]
|
|
fn test_full_workflow() {
|
|
let mut state = AppState::new();
|
|
|
|
// Start on list screen
|
|
assert_eq!(state.current_screen, ScreenType::List);
|
|
|
|
// Create a note
|
|
state.add_note("My First Note".to_string());
|
|
assert_eq!(state.notes.len(), 1);
|
|
|
|
// Navigate to create screen
|
|
state.set_screen(ScreenType::Create);
|
|
assert_eq!(state.current_screen, ScreenType::Create);
|
|
|
|
// Fill form
|
|
state.form_title = "New Note".to_string();
|
|
state.form_content = "Note content".to_string();
|
|
|
|
// Reset form
|
|
state.reset_form();
|
|
assert!(state.form_title.is_empty());
|
|
assert!(state.form_content.is_empty());
|
|
|
|
// Navigate back to list
|
|
state.set_screen(ScreenType::List);
|
|
assert_eq!(state.current_screen, ScreenType::List);
|
|
|
|
// Request quit
|
|
state.request_quit();
|
|
assert!(state.should_quit());
|
|
}
|
|
|
|
#[test]
|
|
fn test_dirty_flag_optimization() {
|
|
let mut state = AppState::new();
|
|
assert!(state.is_dirty());
|
|
|
|
state.reset_dirty();
|
|
assert!(!state.is_dirty());
|
|
|
|
// Any change should mark dirty
|
|
state.add_note("Test".to_string());
|
|
assert!(state.is_dirty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_selection_navigation() {
|
|
let mut state = AppState::new();
|
|
state.add_note("Note 1".to_string());
|
|
state.add_note("Note 2".to_string());
|
|
state.add_note("Note 3".to_string());
|
|
|
|
assert_eq!(state.selected_index, 0);
|
|
|
|
state.select_next();
|
|
assert_eq!(state.selected_index, 1);
|
|
|
|
state.select_next();
|
|
assert_eq!(state.selected_index, 2);
|
|
|
|
// Wraps around
|
|
state.select_next();
|
|
assert_eq!(state.selected_index, 0);
|
|
|
|
state.select_previous();
|
|
assert_eq!(state.selected_index, 2);
|
|
}
|
|
```
|
|
|
|
## Step 8: Verify Everything Compiles and Tests Pass
|
|
|
|
```bash
|
|
cd /Users/Akasha/Tools/note-manager
|
|
cargo check --workspace
|
|
cargo test --workspace
|
|
cargo clippy --all-targets
|
|
cargo fmt --check
|
|
```
|
|
|
|
Expected output:
|
|
```
|
|
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
|
```
|
|
|
|
## Step 9: Run Your TUI
|
|
|
|
If you created a binary entry point in `src/main.rs`, you can run:
|
|
|
|
```bash
|
|
cargo run -p note-manager-tui --release
|
|
```
|
|
|
|
## Summary
|
|
|
|
This walkthrough demonstrates:
|
|
|
|
1. **Directory structure** - How to organize TUI as separate crate
|
|
2. **Cargo.toml** - Proper dependency setup with shared library
|
|
3. **AppState** - Implementation of R-STATE-SEPARATION pattern
|
|
4. **Screens** - Multiple screen implementations following templates
|
|
5. **Integration** - Dashboard registration via ToolSummaryProvider
|
|
6. **Testing** - Comprehensive test coverage (15+ tests minimum)
|
|
7. **Quality** - Zero unsafe code, proper error handling, documentation
|
|
|
|
You can adapt this structure and code for any tool in the Tools ecosystem. The key is following the patterns established in the templates and tools-tui-shared library.
|