| Crates.io | roxid |
| lib.rs | roxid |
| version | 0.8.0 |
| created_at | 2025-10-28 01:13:38.159249+00 |
| updated_at | 2025-11-05 06:10:25.541831+00 |
| description | A Terminal User Interface (TUI) for managing and executing YAML-based pipelines |
| homepage | https://github.com/yourusername/roxid |
| repository | https://github.com/trey-herrington/roxid |
| max_upload_size | |
| id | 1903953 |
| size | 87,328 |
A Terminal User Interface (TUI) application built with Ratatui for managing and executing YAML-based pipelines via gRPC.
Just run roxid - the service auto-starts and auto-stops!
# Launch TUI (auto-starts service, stops when you quit)
roxid
# Or run a specific pipeline (auto-starts service, stops when done)
roxid run example-pipeline.yaml
That's it! The service automatically starts when needed and stops when you're done.
For more details, see the Usage section below.
cargo install roxid
roxid
Download from Releases:
roxid-linux-x86_64.tar.gzroxid-macos-x86_64.tar.gzroxid-macos-aarch64.tar.gzroxid-windows-x86_64.exe.zipLinux/macOS:
tar xzf roxid-*.tar.gz
sudo mv roxid-* /usr/local/bin/roxid
chmod +x /usr/local/bin/roxid
Windows: Extract ZIP and add to PATH.
cargo install --git https://github.com/yourusername/roxid
git clone https://github.com/yourusername/roxid
cd roxid
cargo build --release
sudo cp target/release/roxid /usr/local/bin/
roxid --help
# If installed via cargo:
cargo uninstall roxid
# If installed manually:
sudo rm /usr/local/bin/roxid
Just type roxid - service starts automatically and stops when you're done!
# Launch TUI
roxid
# Or run a pipeline
roxid run my-pipeline.yaml
Smart service management:
Pipeline List Screen:
↑ or k - Move selection up↓ or j - Move selection downEnter - Execute selected pipelineq or Esc - Quit applicationPipeline Execution Screen:
q or Esc - Return to pipeline list (only after completion)Run pipelines directly from command line:
roxid run example-pipeline.yaml
For development or debugging, you can manage the service manually:
# Terminal 1: Start service
cargo run --bin pipeline-service
# Terminal 2: Run TUI or CLI
cargo run --bin roxid
cargo run --bin roxid run pipeline.yaml
If running from source, convenience scripts are available:
./start-tui.sh # Starts service, runs TUI, stops service
./run-pipeline.sh file.yaml # Starts service, runs pipeline, stops service
"Connection refused" error:
roxid - it auto-starts the service"Address already in use" error:
pkill -f pipeline-service then retry"No such file or directory" when parsing pipeline:
# Check if service is running
lsof -ti:50051
# Stop service manually (if needed)
pkill -f pipeline-service
# View service logs (when using scripts)
tail -f /tmp/pipeline-service.log
This workspace follows a gRPC-based microservice architecture:
roxid/
├── Cargo.toml # Workspace manifest
├── pipeline-service/ # gRPC service (library + binary)
│ ├── Cargo.toml
│ ├── build.rs # Proto compilation
│ ├── proto/
│ │ └── pipeline.proto # gRPC service definition
│ └── src/
│ ├── lib.rs # Library entry point
│ ├── grpc.rs # Proto conversions and types
│ ├── error.rs # Error types
│ ├── pipeline/ # Pipeline execution engine
│ └── bin/
│ └── server.rs # gRPC server binary
├── roxid-tui/ # Terminal UI (gRPC client)
│ ├── Cargo.toml
│ ├── build.rs # Proto compilation
│ └── src/
│ ├── main.rs # Entry point
│ ├── app.rs # Application state and gRPC client
│ ├── events.rs # Event handling (keyboard, mouse)
│ └── ui/ # UI rendering modules
└── roxid-cli/ # CLI application (gRPC client)
├── Cargo.toml
├── build.rs # Proto compilation
└── src/
└── main.rs # CLI entry point with gRPC client
pipeline-service/)models/: Data structures and domain modelspipeline/: Pipeline execution systemservices/: Business logic implementationserror.rs: Domain-specific error typesroxid-tui/)main.rs: Application entry pointapp.rs: Application state and gRPC client managementevents.rs: User input handlingui/: UI rendering logicroxid-cli/)main.rs: CLI entry point with gRPC client┌─────────────┐ gRPC ┌──────────────────┐
│ roxid-tui │ ◄──────────────────► │ pipeline-service │
│ (client) │ Streaming Events │ (gRPC server) │
└─────────────┘ └──────────────────┘
▲
┌─────────────┐ gRPC │
│ roxid-cli │ ◄────────────────────────────┘
│ (client) │ Streaming Events
└─────────────┘
Key Architectural Benefits:
The service provides two RPCs:
Events streamed during execution:
PipelineStarted: Pipeline execution beginsStepStarted: A step starts executingStepOutput: Real-time output from stepStepCompleted: Step finishes with resultPipelineCompleted: All steps completeInitialization:
Pipeline List State:
Pipeline Execution State:
Event Loop: The main loop continuously:
gRPC Communication:
┌─────────────────┐
│ PipelineList │
│ - Discover YAML │
│ - Navigate │
│ - Select │
└────────┬────────┘
│ Enter
▼
┌─────────────────┐
│ ExecutingPipe │
│ - Run pipeline │
│ - Show progress │
│ - Stream output │
└────────┬────────┘
│ Complete
▼
┌─────────────────┐
│ PipelineList │
│ (return) │
└─────────────────┘
name: my-pipeline
description: Optional description
env:
GLOBAL_VAR: value
steps:
- name: Step name
command: echo "Hello World"
- name: Multi-line script
shell:
script: |
echo "Line 1"
echo "Line 2"
env:
STEP_VAR: value
continue_on_error: true
Create a file ending in .yaml or .yml:
name: my-first-pipeline
description: My first custom pipeline
steps:
- name: Hello World
command: echo "Hello from my pipeline!"
- name: Show Date
command: date
- name: List Files
command: ls -la
use pipeline_rpc::{PipelineHandler, ExecutionEvent};
use color_eyre::Result;
#[tokio::main]
async fn main() -> Result<()> {
// Create RPC handler
let handler = PipelineHandler::new();
// Parse pipeline from file
let pipeline = handler.parse_from_file("pipeline.yaml")?;
// Get working directory
let working_dir = std::env::current_dir()?.to_string_lossy().to_string();
// Execute pipeline through RPC layer
handler.execute_pipeline(pipeline, working_dir, None).await?;
Ok(())
}
use pipeline_rpc::{PipelineHandler, ExecutionEvent};
// Create event channel
let (tx, mut rx) = PipelineHandler::create_event_channel();
// Create handler and parse pipeline
let handler = PipelineHandler::new();
let pipeline = handler.parse_from_file("pipeline.yaml")?;
let working_dir = std::env::current_dir()?.to_string_lossy().to_string();
// Spawn executor
let handle = tokio::spawn(async move {
handler.execute_pipeline(pipeline, working_dir, Some(tx)).await
});
// Monitor progress
while let Some(event) = rx.recv().await {
match event {
ExecutionEvent::StepStarted { step_name, .. } => {
println!("Running: {}", step_name);
}
ExecutionEvent::StepOutput { output, .. } => {
println!(" {}", output);
}
ExecutionEvent::StepCompleted { result, .. } => {
println!("Completed: {:?}", result.status);
}
_ => {}
}
}
let result = handle.await?;
name: quick-test
steps:
- name: Check version
command: rustc --version
- name: Run tests
command: cargo test
name: build-pipeline
env:
RUST_BACKTRACE: "1"
steps:
- name: Format check
command: cargo fmt --check
continue_on_error: true
- name: Build
command: cargo build --all
- name: Test
command: cargo test --all
The pipeline system consists of:
pipeline/parser.rs) - Parses YAML into Rust structspipeline/executor.rs) - Orchestrates step executionpipeline/runners/) - Execute different step types
shell.rs - Runs shell commands and scriptspipeline/models.rs) - Data structures for pipelines, steps, results┌─────────────────────────────────────────────┐
│ Pipeline Runner │
└─────────────────────────────────────────────┘
┌─ Available Pipelines ──────────────────────┐
│ → example-pipeline - A simple example │
│ rust-build-pipeline - Build Rust project│
│ advanced-pipeline - Complex workflow │
└─────────────────────────────────────────────┘
┌─ Help ──────────────────────────────────────┐
│ ↑/↓: Navigate | Enter: Execute | q: Quit │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Executing: example-pipeline │
└─────────────────────────────────────────────┘
┌─ Progress ──────────────────────────────────┐
│ ████████████░░░░░░░░ Step 3/5 │
└─────────────────────────────────────────────┘
┌─ Output ────────────────────────────────────┐
│ [Step 1/5] Check Rust version │
│ rustc 1.70.0 (90c541806 2023-05-31) │
│ ✓ Completed in 0.05s │
│ │
│ [Step 2/5] List files │
│ ✓ Completed in 0.02s │
│ │
│ [Step 3/5] Multi-line script │
│ Starting multi-line script │
└─────────────────────────────────────────────┘
Features:
No Pipelines Found
.yaml or .yml filesname and steps fieldsPipeline Fails to Execute
TUI Doesn't Respond
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum Tab {
#[default]
Counter,
Items,
Settings,
}
// Add to App struct:
struct App {
counter: i32,
items: Vec<String>,
current_tab: Tab,
should_quit: bool,
}
// Handle tab switching:
KeyCode::Char('1') => self.current_tab = Tab::Counter,
KeyCode::Char('2') => self.current_tab = Tab::Items,
KeyCode::Char('3') => self.current_tab = Tab::Settings,
use ratatui::widgets::ListState;
struct App {
// ... existing fields
list_state: ListState,
}
// Handle scrolling:
KeyCode::Up => {
let i = self.list_state.selected().unwrap_or(0);
if i > 0 {
self.list_state.select(Some(i - 1));
}
}
KeyCode::Down => {
let i = self.list_state.selected().unwrap_or(0);
if i < self.items.len() - 1 {
self.list_state.select(Some(i + 1));
}
}
// Render with state:
frame.render_stateful_widget(list, main_chunks[1], &mut self.list_state);
struct App {
// ... existing fields
input: String,
input_mode: bool,
}
// Handle input mode:
KeyCode::Char('i') if !self.input_mode => {
self.input_mode = true;
}
KeyCode::Esc if self.input_mode => {
self.input_mode = false;
}
KeyCode::Char(c) if self.input_mode => {
self.input.push(c);
}
KeyCode::Backspace if self.input_mode => {
self.input.pop();
}
KeyCode::Enter if self.input_mode => {
self.items.push(self.input.clone());
self.input.clear();
self.input_mode = false;
}
use crossterm::event::{MouseEvent, MouseEventKind};
// In handle_events:
Event::Mouse(mouse_event) => self.handle_mouse_event(mouse_event),
// Handler:
fn handle_mouse_event(&mut self, mouse_event: MouseEvent) {
match mouse_event.kind {
MouseEventKind::Down(_) => {
// Handle click at position (mouse_event.column, mouse_event.row)
}
MouseEventKind::ScrollUp => {
// Handle scroll up
}
MouseEventKind::ScrollDown => {
// Handle scroll down
}
_ => {}
}
}
// Add to Cargo.toml:
// tokio = { version = "1", features = ["full"] }
use tokio::sync::mpsc;
use std::time::Duration;
enum AppEvent {
Input(Event),
DataUpdate(String),
}
#[tokio::main]
async fn main() -> Result<()> {
let (tx, mut rx) = mpsc::channel(100);
// Spawn background task
let tx_clone = tx.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
let _ = tx_clone.send(AppEvent::DataUpdate("Updated".to_string())).await;
}
});
// Main event loop
loop {
if event::poll(Duration::from_millis(100))? {
tx.send(AppEvent::Input(event::read()?)).await?;
}
while let Ok(event) = rx.try_recv() {
match event {
AppEvent::Input(e) => { /* handle input */ }
AppEvent::DataUpdate(data) => { /* update state */ }
}
}
}
}
To extend the pipeline system:
pipeline-service/src/pipeline/runners/pipeline-rpc/src/handlers/ to expose new functionalitypipeline-rpc/src/lib.rsRemember: All client functionality must go through the RPC layer. Never import pipeline-service directly in CLI or TUI.
# Build entire workspace
cargo build
# Build specific package
cargo build -p roxid-tui
cargo build -p pipeline-service
cargo build -p roxid
# Start the gRPC service
cargo run --bin pipeline-service
# Run the TUI application (service must be running)
cargo run --bin roxid
# Run the CLI application (service must be running)
cargo run --bin roxid run example-pipeline.yaml
# Run tests for all packages
cargo test
# Run tests for specific package
cargo test -p pipeline-service
# Check code without building
cargo check
This skeleton application is provided as-is for educational and development purposes.