Initial Rust CLI/TUI Rebuild (Seeded Sim + CSV Export)
This commit is contained in:
parent
faf0929bb6
commit
dae34cce41
16 changed files with 2739 additions and 0 deletions
83
src/ui/dashboard.rs
Normal file
83
src/ui/dashboard.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
use ratatui::layout::{Constraint, Direction, Layout};
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::instance::SimStatus;
|
||||
use crate::ui::widgets::status_badge;
|
||||
|
||||
pub fn render(f: &mut Frame<'_>, area: Rect, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(55), Constraint::Percentage(45)])
|
||||
.split(area);
|
||||
|
||||
let mut items: Vec<ListItem> = Vec::new();
|
||||
if app.instances.is_empty() {
|
||||
items.push(ListItem::new(
|
||||
"No instances yet. Press n, l, or o to create one.",
|
||||
));
|
||||
} else {
|
||||
for (idx, inst) in app.instances.iter().enumerate() {
|
||||
let marker = if idx == app.selected { ">" } else { " " };
|
||||
let line = format!(
|
||||
"{} sim-{} [{}] {} | {}",
|
||||
marker,
|
||||
inst.id,
|
||||
inst.sim_type.as_str(),
|
||||
status_badge(&inst.status),
|
||||
inst.progress_text()
|
||||
);
|
||||
let mut item = ListItem::new(line);
|
||||
if idx == app.selected {
|
||||
item = item.style(
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
let list = List::new(items).block(Block::default().title("Instances").borders(Borders::ALL));
|
||||
f.render_widget(list, chunks[0]);
|
||||
|
||||
let detail_text = if let Some(inst) = app.selected_instance() {
|
||||
let status = match &inst.status {
|
||||
SimStatus::Pending => "pending",
|
||||
SimStatus::Running { .. } => "running",
|
||||
SimStatus::Completed => "completed",
|
||||
};
|
||||
format!(
|
||||
"ID: sim-{}\nType: {}\nStatus: {}\nSeed: {}\nTeams:\n- {}\n- {}{}\n\nOutcome:\n{}\n\nTip: Press Enter or v to open live detail view.",
|
||||
inst.id,
|
||||
inst.sim_type.as_str(),
|
||||
status,
|
||||
inst.seed,
|
||||
inst.teams.first().cloned().unwrap_or_default(),
|
||||
inst.teams.get(1).cloned().unwrap_or_default(),
|
||||
if inst.teams.len() > 2 {
|
||||
format!(
|
||||
"\n- {}\n- {}",
|
||||
inst.teams.get(2).cloned().unwrap_or_default(),
|
||||
inst.teams.get(3).cloned().unwrap_or_default()
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
inst.outcome_summary()
|
||||
)
|
||||
} else {
|
||||
"No selection".to_string()
|
||||
};
|
||||
|
||||
let details = Paragraph::new(detail_text)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Selected Instance")
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.wrap(ratatui::widgets::Wrap { trim: true });
|
||||
f.render_widget(details, chunks[1]);
|
||||
}
|
||||
109
src/ui/detail.rs
Normal file
109
src/ui/detail.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
use ratatui::layout::{Constraint, Direction, Layout};
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Tabs};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::instance::SimStatus;
|
||||
|
||||
pub fn render(f: &mut Frame<'_>, area: Rect, app: &App) {
|
||||
let Some(inst) = app.selected_instance() else {
|
||||
let empty = Paragraph::new("No selected instance")
|
||||
.block(Block::default().title("Detail").borders(Borders::ALL));
|
||||
f.render_widget(empty, area);
|
||||
return;
|
||||
};
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(8),
|
||||
Constraint::Length(8),
|
||||
])
|
||||
.split(area);
|
||||
|
||||
let score = Paragraph::new(inst.scoreboard.clone())
|
||||
.block(
|
||||
Block::default()
|
||||
.title(format!("sim-{} Scoreboard", inst.id))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.style(Style::default().fg(Color::White).bg(Color::Black));
|
||||
f.render_widget(score, chunks[0]);
|
||||
|
||||
let middle = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(60), Constraint::Percentage(40)])
|
||||
.split(chunks[1]);
|
||||
|
||||
let logs: Vec<ListItem> = inst
|
||||
.logs
|
||||
.iter()
|
||||
.rev()
|
||||
.take((middle[0].height as usize).saturating_sub(2))
|
||||
.rev()
|
||||
.map(|line| ListItem::new(line.clone()))
|
||||
.collect();
|
||||
let log_widget =
|
||||
List::new(logs).block(Block::default().title("Live Log").borders(Borders::ALL));
|
||||
f.render_widget(log_widget, middle[0]);
|
||||
|
||||
let mut right_lines: Vec<ListItem> = Vec::new();
|
||||
let status = match &inst.status {
|
||||
SimStatus::Pending => "pending",
|
||||
SimStatus::Running { .. } => "running",
|
||||
SimStatus::Completed => "completed",
|
||||
};
|
||||
right_lines.push(ListItem::new(format!("Status: {}", status)));
|
||||
right_lines.push(ListItem::new(format!("Seed: {}", inst.seed)));
|
||||
right_lines.push(ListItem::new(format!("Mode: {}", inst.sim_type.as_str())));
|
||||
right_lines.push(ListItem::new(""));
|
||||
right_lines.push(ListItem::new("Teams:"));
|
||||
for t in &inst.teams {
|
||||
right_lines.push(ListItem::new(format!("- {}", t)));
|
||||
}
|
||||
let side = List::new(right_lines).block(
|
||||
Block::default()
|
||||
.title("Instance Info")
|
||||
.borders(Borders::ALL),
|
||||
);
|
||||
f.render_widget(side, middle[1]);
|
||||
|
||||
let bottom = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Percentage(34),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
])
|
||||
.split(chunks[2]);
|
||||
|
||||
let stats = List::new(
|
||||
inst.stats_lines
|
||||
.iter()
|
||||
.map(|s| ListItem::new(s.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.block(Block::default().title("Stats").borders(Borders::ALL));
|
||||
let comp = List::new(
|
||||
inst.competition_lines
|
||||
.iter()
|
||||
.map(|s| ListItem::new(s.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.block(Block::default().title("Competition").borders(Borders::ALL));
|
||||
let hist = List::new(
|
||||
inst.history_lines
|
||||
.iter()
|
||||
.map(|s| ListItem::new(s.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.block(Block::default().title("History").borders(Borders::ALL));
|
||||
|
||||
f.render_widget(stats, bottom[0]);
|
||||
f.render_widget(comp, bottom[1]);
|
||||
f.render_widget(hist, bottom[2]);
|
||||
|
||||
let tabs = Tabs::new(vec!["Dashboard", "Detail"]).select(1);
|
||||
let _ = tabs;
|
||||
}
|
||||
41
src/ui/mod.rs
Normal file
41
src/ui/mod.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
pub mod dashboard;
|
||||
pub mod detail;
|
||||
pub mod modal;
|
||||
pub mod widgets;
|
||||
|
||||
use ratatui::layout::{Constraint, Direction, Layout};
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
||||
|
||||
use crate::app::App;
|
||||
|
||||
pub fn draw(f: &mut Frame<'_>, app: &App) {
|
||||
let areas = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(10),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.split(f.area());
|
||||
|
||||
let header = Paragraph::new("MentalNet SoccerCloud | n=single l=league4 o=knockout4 s=start c=clone d=delete e=export v=detail q=quit")
|
||||
.block(Block::default().title("Dashboard").borders(Borders::ALL))
|
||||
.style(Style::default().fg(Color::Cyan));
|
||||
f.render_widget(header, areas[0]);
|
||||
|
||||
if app.show_detail {
|
||||
detail::render(f, areas[1], app);
|
||||
} else {
|
||||
dashboard::render(f, areas[1], app);
|
||||
}
|
||||
|
||||
let footer = Paragraph::new(format!(
|
||||
"{} | speed={} (1/2/4/0)",
|
||||
app.status_line,
|
||||
app.speed.label()
|
||||
))
|
||||
.block(Block::default().borders(Borders::ALL).title("Status"))
|
||||
.style(Style::default().fg(Color::Green));
|
||||
f.render_widget(footer, areas[2]);
|
||||
}
|
||||
2
src/ui/modal.rs
Normal file
2
src/ui/modal.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// Placeholder module for future interactive creation modal.
|
||||
// Current MVP uses keyboard shortcuts for fast instance creation.
|
||||
9
src/ui/widgets.rs
Normal file
9
src/ui/widgets.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
use crate::instance::SimStatus;
|
||||
|
||||
pub fn status_badge(status: &SimStatus) -> &'static str {
|
||||
match status {
|
||||
SimStatus::Pending => "PENDING",
|
||||
SimStatus::Running { .. } => "RUNNING",
|
||||
SimStatus::Completed => "COMPLETED",
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue