update terminal ui to show nested examples
This commit is contained in:
parent
b31651cfeb
commit
0df79e78c6
@ -82,6 +82,70 @@ pub struct ExampleScript {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExampleTreeNode {
|
||||
File {
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
},
|
||||
Folder {
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
children: Vec<ExampleTreeNode>,
|
||||
expanded: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl ExampleTreeNode {
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
ExampleTreeNode::File { name, .. } => name,
|
||||
ExampleTreeNode::Folder { name, .. } => name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &PathBuf {
|
||||
match self {
|
||||
ExampleTreeNode::File { path, .. } => path,
|
||||
ExampleTreeNode::Folder { path, .. } => path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_file(&self) -> bool {
|
||||
matches!(self, ExampleTreeNode::File { .. })
|
||||
}
|
||||
|
||||
pub fn is_folder(&self) -> bool {
|
||||
matches!(self, ExampleTreeNode::Folder { .. })
|
||||
}
|
||||
|
||||
pub fn is_expanded(&self) -> bool {
|
||||
match self {
|
||||
ExampleTreeNode::File { .. } => false,
|
||||
ExampleTreeNode::Folder { expanded, .. } => *expanded,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_expanded(&mut self) {
|
||||
if let ExampleTreeNode::Folder { expanded, .. } = self {
|
||||
*expanded = !*expanded;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_expanded(&mut self, expand: bool) {
|
||||
if let ExampleTreeNode::Folder { expanded, .. } = self {
|
||||
*expanded = expand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExampleTreeItem {
|
||||
pub node: ExampleTreeNode,
|
||||
pub depth: usize,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JobInfo {
|
||||
pub job: Job,
|
||||
@ -108,6 +172,11 @@ pub struct App {
|
||||
pub status_message: Option<String>,
|
||||
pub last_executed_job: Option<JobInfo>,
|
||||
pub last_refresh: Instant,
|
||||
// New hierarchical examples system
|
||||
pub example_tree: Vec<ExampleTreeNode>,
|
||||
pub example_tree_items: Vec<ExampleTreeItem>,
|
||||
pub example_tree_state: ListState,
|
||||
pub selected_tree_item: Option<ExampleTreeItem>,
|
||||
}
|
||||
|
||||
pub struct ActorTui;
|
||||
@ -138,44 +207,194 @@ impl App {
|
||||
status_message: None,
|
||||
last_executed_job: None,
|
||||
last_refresh: Instant::now(),
|
||||
// Initialize new hierarchical examples system
|
||||
example_tree: Vec::new(),
|
||||
example_tree_items: Vec::new(),
|
||||
example_tree_state: ListState::default(),
|
||||
selected_tree_item: None,
|
||||
};
|
||||
|
||||
if let Some(dir) = example_dir {
|
||||
if let Err(e) = app.load_examples(dir) {
|
||||
error!("Failed to load examples: {}", e);
|
||||
if let Err(e) = app.load_examples_tree(dir) {
|
||||
error!("Failed to load examples tree: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
pub fn load_examples(&mut self, dir: PathBuf) -> Result<()> {
|
||||
pub fn load_examples_tree(&mut self, dir: PathBuf) -> Result<()> {
|
||||
if !dir.exists() {
|
||||
log::warn!("Examples directory does not exist: {:?}", dir);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::info!("Loading examples tree from: {:?}", dir);
|
||||
|
||||
// Load hierarchical tree structure
|
||||
match self.load_example_tree(&dir) {
|
||||
Ok(tree) => {
|
||||
self.example_tree = tree;
|
||||
log::info!("Successfully loaded {} top-level tree nodes", self.example_tree.len());
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to load example tree: {}", e);
|
||||
// Create a simple fallback structure
|
||||
self.example_tree = vec![
|
||||
ExampleTreeNode::File {
|
||||
name: "Error loading examples".to_string(),
|
||||
path: dir.join("error.rhai"),
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
self.rebuild_tree_items();
|
||||
log::info!("Rebuilt tree items: {} total items", self.example_tree_items.len());
|
||||
|
||||
if !self.example_tree_items.is_empty() {
|
||||
self.example_tree_state.select(Some(0));
|
||||
self.selected_tree_item = Some(self.example_tree_items[0].clone());
|
||||
|
||||
// Update selected_example for backward compatibility
|
||||
if let Some(first_file) = self.find_first_file_in_tree() {
|
||||
self.selected_example = Some(ExampleScript {
|
||||
name: first_file.name().to_string(),
|
||||
path: first_file.path().clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_example_tree(&self, dir: &PathBuf) -> Result<Vec<ExampleTreeNode>> {
|
||||
let mut nodes = Vec::new();
|
||||
|
||||
log::debug!("Loading directory: {:?}", dir);
|
||||
|
||||
let mut entries: Vec<_> = fs::read_dir(dir)?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
log::debug!("Found {} entries in {:?}", entries.len(), dir);
|
||||
|
||||
// Sort entries: directories first, then files, both alphabetically
|
||||
entries.sort_by(|a, b| {
|
||||
let a_is_dir = a.path().is_dir();
|
||||
let b_is_dir = b.path().is_dir();
|
||||
|
||||
match (a_is_dir, b_is_dir) {
|
||||
(true, false) => std::cmp::Ordering::Less,
|
||||
(false, true) => std::cmp::Ordering::Greater,
|
||||
_ => a.file_name().cmp(&b.file_name()),
|
||||
}
|
||||
});
|
||||
|
||||
for entry in entries {
|
||||
let path = entry.path();
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
if path.is_dir() {
|
||||
log::debug!("Loading folder: {}", name);
|
||||
let children = self.load_example_tree(&path)?;
|
||||
log::debug!("Folder '{}' has {} children", name, children.len());
|
||||
nodes.push(ExampleTreeNode::Folder {
|
||||
name,
|
||||
path,
|
||||
children,
|
||||
expanded: true, // Expand folders by default to show hierarchy
|
||||
});
|
||||
} else if path.extension().map_or(false, |ext| ext == "rhai") {
|
||||
log::debug!("Loading file: {}", name);
|
||||
nodes.push(ExampleTreeNode::File {
|
||||
name: path.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or(&name)
|
||||
.to_string(),
|
||||
path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(nodes)
|
||||
}
|
||||
|
||||
fn load_flat_examples(&mut self, dir: &PathBuf) -> Result<()> {
|
||||
let mut examples = Vec::new();
|
||||
self.collect_all_rhai_files(&mut examples, dir)?;
|
||||
self.examples = examples;
|
||||
|
||||
if !self.examples.is_empty() {
|
||||
self.example_list_state.select(Some(0));
|
||||
if self.selected_example.is_none() {
|
||||
self.selected_example = Some(self.examples[0].clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_all_rhai_files(&self, examples: &mut Vec<ExampleScript>, dir: &PathBuf) -> Result<()> {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_file() && path.extension().map_or(false, |ext| ext == "rhai") {
|
||||
if path.is_dir() {
|
||||
self.collect_all_rhai_files(examples, &path)?;
|
||||
} else if path.extension().map_or(false, |ext| ext == "rhai") {
|
||||
if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
|
||||
self.examples.push(ExampleScript {
|
||||
examples.push(ExampleScript {
|
||||
name: name.to_string(),
|
||||
path,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !self.examples.is_empty() {
|
||||
self.example_list_state.select(Some(0));
|
||||
self.selected_example = Some(self.examples[0].clone());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rebuild_tree_items(&mut self) {
|
||||
self.example_tree_items.clear();
|
||||
log::info!("Rebuilding tree items from {} root nodes", self.example_tree.len());
|
||||
let mut index = 0;
|
||||
let tree_clone = self.example_tree.clone();
|
||||
for node in &tree_clone {
|
||||
log::info!("Processing root node: {:?}", node.name());
|
||||
self.add_tree_items_recursive(node, 0, &mut index);
|
||||
}
|
||||
log::info!("Final tree items count: {}", self.example_tree_items.len());
|
||||
}
|
||||
|
||||
fn add_tree_items_recursive(&mut self, node: &ExampleTreeNode, depth: usize, index: &mut usize) {
|
||||
// Always add the current node to the flattened list
|
||||
log::debug!("Adding tree item: {} at depth {}", node.name(), depth);
|
||||
self.example_tree_items.push(ExampleTreeItem {
|
||||
node: node.clone(),
|
||||
depth,
|
||||
index: *index,
|
||||
});
|
||||
*index += 1;
|
||||
|
||||
// For folders, add children only if the folder is expanded
|
||||
if let ExampleTreeNode::Folder { children, expanded, .. } = node {
|
||||
log::debug!("Folder '{}' has {} children, expanded: {}", node.name(), children.len(), expanded);
|
||||
if *expanded {
|
||||
for child in children {
|
||||
self.add_tree_items_recursive(child, depth + 1, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_first_file_in_tree(&self) -> Option<&ExampleTreeNode> {
|
||||
for item in &self.example_tree_items {
|
||||
if item.node.is_file() {
|
||||
return Some(&item.node);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn refresh_jobs(&mut self) -> Result<()> {
|
||||
let mut conn = self.redis_client.get_multiplexed_async_connection().await?;
|
||||
|
||||
@ -403,13 +622,13 @@ impl App {
|
||||
}
|
||||
|
||||
pub fn next_example(&mut self) {
|
||||
if self.examples.is_empty() {
|
||||
if self.example_tree_items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let selected = match self.example_list_state.selected() {
|
||||
let i = match self.example_tree_state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.examples.len() - 1 {
|
||||
if i >= self.example_tree_items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
@ -417,27 +636,109 @@ impl App {
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.example_list_state.select(Some(selected));
|
||||
self.selected_example = Some(self.examples[selected].clone());
|
||||
self.example_tree_state.select(Some(i));
|
||||
self.selected_tree_item = Some(self.example_tree_items[i].clone());
|
||||
|
||||
// Update selected_example for backward compatibility
|
||||
if self.example_tree_items[i].node.is_file() {
|
||||
self.selected_example = Some(ExampleScript {
|
||||
name: self.example_tree_items[i].node.name().to_string(),
|
||||
path: self.example_tree_items[i].node.path().clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous_example(&mut self) {
|
||||
if self.examples.is_empty() {
|
||||
if self.example_tree_items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let selected = match self.example_list_state.selected() {
|
||||
let i = match self.example_tree_state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.examples.len() - 1
|
||||
self.example_tree_items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.example_list_state.select(Some(selected));
|
||||
self.selected_example = Some(self.examples[selected].clone());
|
||||
self.example_tree_state.select(Some(i));
|
||||
self.selected_tree_item = Some(self.example_tree_items[i].clone());
|
||||
|
||||
// Update selected_example for backward compatibility
|
||||
if self.example_tree_items[i].node.is_file() {
|
||||
self.selected_example = Some(ExampleScript {
|
||||
name: self.example_tree_items[i].node.name().to_string(),
|
||||
path: self.example_tree_items[i].node.path().clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_selected_folder(&mut self) {
|
||||
if let Some(selected_index) = self.example_tree_state.selected() {
|
||||
if selected_index < self.example_tree_items.len() {
|
||||
let item = &self.example_tree_items[selected_index];
|
||||
if item.node.is_folder() {
|
||||
let path = item.node.path().clone();
|
||||
self.expand_folder_by_path(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collapse_selected_folder(&mut self) {
|
||||
if let Some(selected_index) = self.example_tree_state.selected() {
|
||||
if selected_index < self.example_tree_items.len() {
|
||||
let item = &self.example_tree_items[selected_index];
|
||||
if item.node.is_folder() {
|
||||
let path = item.node.path().clone();
|
||||
self.collapse_folder_by_path(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_selected_folder(&mut self) {
|
||||
if let Some(selected_index) = self.example_tree_state.selected() {
|
||||
if selected_index < self.example_tree_items.len() {
|
||||
let item = &self.example_tree_items[selected_index];
|
||||
if item.node.is_folder() {
|
||||
let path = item.node.path().clone();
|
||||
let is_expanded = item.node.is_expanded();
|
||||
if is_expanded {
|
||||
self.collapse_folder_by_path(&path);
|
||||
} else {
|
||||
self.expand_folder_by_path(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_folder_by_path(&mut self, target_path: &PathBuf) {
|
||||
Self::set_folder_expanded_by_path_static(&mut self.example_tree, target_path, true);
|
||||
self.rebuild_tree_items();
|
||||
}
|
||||
|
||||
fn collapse_folder_by_path(&mut self, target_path: &PathBuf) {
|
||||
Self::set_folder_expanded_by_path_static(&mut self.example_tree, target_path, false);
|
||||
self.rebuild_tree_items();
|
||||
}
|
||||
|
||||
fn set_folder_expanded_by_path_static(nodes: &mut Vec<ExampleTreeNode>, target_path: &PathBuf, expanded: bool) {
|
||||
for node in nodes {
|
||||
match node {
|
||||
ExampleTreeNode::Folder { path, children, expanded: node_expanded, .. } => {
|
||||
if path == target_path {
|
||||
*node_expanded = expanded;
|
||||
return;
|
||||
}
|
||||
Self::set_folder_expanded_by_path_static(children, target_path, expanded);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -868,20 +1169,33 @@ fn render_examples(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
.constraints([Constraint::Percentage(40), Constraint::Percentage(60)])
|
||||
.split(area);
|
||||
|
||||
// Example list
|
||||
let examples: Vec<ListItem> = app.examples
|
||||
// Hierarchical example tree list
|
||||
let tree_items: Vec<ListItem> = app.example_tree_items
|
||||
.iter()
|
||||
.map(|example| {
|
||||
ListItem::new(Line::from(vec![Span::raw(example.name.clone())]))
|
||||
.map(|item| {
|
||||
let indent = " ".repeat(item.depth);
|
||||
let (icon, name) = match &item.node {
|
||||
ExampleTreeNode::File { name, .. } => ("📄", name.as_str()),
|
||||
ExampleTreeNode::Folder { name, expanded, .. } => {
|
||||
if *expanded {
|
||||
("📂", name.as_str())
|
||||
} else {
|
||||
("📁", name.as_str())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let display_text = format!("{}{} {}", indent, icon, name);
|
||||
ListItem::new(Line::from(vec![Span::raw(display_text)]))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let examples_list = List::new(examples)
|
||||
.block(Block::default().borders(Borders::ALL).title("Example Scripts"))
|
||||
let examples_list = List::new(tree_items)
|
||||
.block(Block::default().borders(Borders::ALL).title("Example Scripts (→ expand, ← collapse)"))
|
||||
.highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))
|
||||
.highlight_symbol("> ");
|
||||
|
||||
f.render_stateful_widget(examples_list, chunks[0], &mut app.example_list_state);
|
||||
f.render_stateful_widget(examples_list, chunks[0], &mut app.example_tree_state);
|
||||
|
||||
// Example content
|
||||
if let Some(ref example) = app.selected_example {
|
||||
@ -992,14 +1306,41 @@ async fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> Result
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
KeyCode::Left => {
|
||||
if app.current_tab == TabState::Examples {
|
||||
app.collapse_selected_folder();
|
||||
}
|
||||
}
|
||||
KeyCode::Right => {
|
||||
if app.current_tab == TabState::Examples {
|
||||
app.expand_selected_folder();
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if app.current_tab == TabState::Examples {
|
||||
if let Some(example) = app.selected_example.clone() {
|
||||
if let Err(e) = app.load_example_to_new_job(&example) {
|
||||
app.status_message = Some(format!("Error loading example: {}", e));
|
||||
} else {
|
||||
app.current_tab = TabState::NewJob;
|
||||
app.status_message = Some(format!("Loaded example '{}' into New Job tab", example.name));
|
||||
// Handle hierarchical tree navigation
|
||||
if let Some(selected_index) = app.example_tree_state.selected() {
|
||||
if selected_index < app.example_tree_items.len() {
|
||||
let item = app.example_tree_items[selected_index].clone();
|
||||
match &item.node {
|
||||
ExampleTreeNode::Folder { .. } => {
|
||||
// Toggle folder expand/collapse
|
||||
app.toggle_selected_folder();
|
||||
}
|
||||
ExampleTreeNode::File { path, name, .. } => {
|
||||
// Load file into New Job tab
|
||||
let example = ExampleScript {
|
||||
name: name.clone(),
|
||||
path: path.clone(),
|
||||
};
|
||||
if let Err(e) = app.load_example_to_new_job(&example) {
|
||||
app.status_message = Some(format!("Error loading example: {}", e));
|
||||
} else {
|
||||
app.current_tab = TabState::NewJob;
|
||||
app.status_message = Some(format!("Loaded example '{}' into New Job tab", name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if app.current_tab == TabState::NewJob {
|
||||
|
Loading…
Reference in New Issue
Block a user