Skip to main content

flow_tui/views/
graph.rs

1use crate::app::App;
2use ratatui::{
3    layout::{Constraint, Direction, Layout},
4    style::{Modifier, Style},
5    text::{Line, Span},
6    widgets::{Block, Borders, List, ListItem, Paragraph},
7    Frame,
8};
9
10pub fn render(frame: &mut Frame, app: &App) {
11    let area = frame.area();
12    let theme = &app.tui_theme;
13
14    // Split main area into graph and footer
15    let chunks = Layout::default()
16        .direction(Direction::Vertical)
17        .constraints([
18            Constraint::Min(0),    // Graph
19            Constraint::Length(1), // Footer
20        ])
21        .split(area);
22
23    let graph_area = chunks[0];
24    let footer_area = chunks[1];
25
26    let block = Block::default()
27        .title("Dependency Graph")
28        .borders(Borders::ALL)
29        .border_style(Style::default().fg(theme.border))
30        .title_style(
31            Style::default()
32                .fg(theme.primary)
33                .add_modifier(Modifier::BOLD),
34        );
35
36    let inner = block.inner(graph_area);
37    frame.render_widget(block, graph_area);
38
39    if app.features.is_empty() {
40        return;
41    }
42
43    // Create a simple vertical layout showing features and their dependencies
44    let mut items = Vec::new();
45
46    for feature in &app.features {
47        // Determine status color
48        let status_color = if feature.passes {
49            theme.done
50        } else if feature.in_progress {
51            theme.in_progress
52        } else {
53            theme.pending
54        };
55
56        // Status icon
57        let icon = if feature.passes {
58            "✓"
59        } else if feature.in_progress {
60            "◉"
61        } else {
62            "○"
63        };
64
65        // Check if blocked
66        let is_blocked = !feature.dependencies.is_empty()
67            && feature.dependencies.iter().any(|dep_id| {
68                app.features
69                    .iter()
70                    .find(|f| f.id == *dep_id)
71                    .is_none_or(|f| !f.passes)
72            });
73
74        let border_color = if is_blocked {
75            theme.blocked
76        } else {
77            status_color
78        };
79
80        // Create feature box
81        items.push(ListItem::new(vec![
82            Line::from(vec![
83                Span::styled("┌─", Style::default().fg(border_color)),
84                Span::styled(
85                    format!(" {} {} ", icon, feature.name),
86                    Style::default()
87                        .fg(status_color)
88                        .add_modifier(Modifier::BOLD),
89                ),
90                Span::styled("─┐", Style::default().fg(border_color)),
91            ]),
92            Line::from(vec![
93                Span::styled("│ ", Style::default().fg(border_color)),
94                Span::styled(&feature.category, Style::default().fg(theme.secondary)),
95                Span::styled(" │", Style::default().fg(border_color)),
96            ]),
97            Line::from(vec![Span::styled(
98                "└──────────────────┘",
99                Style::default().fg(border_color),
100            )]),
101        ]));
102
103        // Show dependencies with arrows
104        if !feature.dependencies.is_empty() {
105            for dep_id in &feature.dependencies {
106                if let Some(dep) = app.features.iter().find(|f| f.id == *dep_id) {
107                    let dep_status = if dep.passes {
108                        "✓"
109                    } else if dep.in_progress {
110                        "◉"
111                    } else {
112                        "○"
113                    };
114
115                    items.push(ListItem::new(Line::from(vec![
116                        Span::styled("  ↓ ", Style::default().fg(theme.accent)),
117                        Span::styled(
118                            format!("{dep_status} depends on: "),
119                            Style::default().fg(theme.muted),
120                        ),
121                        Span::styled(&dep.name, Style::default().fg(theme.foreground)),
122                    ])));
123                }
124            }
125        }
126
127        items.push(ListItem::new(Line::from("")));
128    }
129
130    let list = List::new(items);
131    frame.render_widget(list, inner);
132
133    // Render footer with key hints
134    let footer_text = if let Some(status) = app.get_status_message() {
135        status.to_string()
136    } else {
137        " 1-4:views  r:refresh  t:theme  ?:help  q:quit".to_string()
138    };
139
140    let footer = Paragraph::new(footer_text).style(Style::default().fg(theme.muted));
141    frame.render_widget(footer, footer_area);
142}