summaryrefslogtreecommitdiff
path: root/makima/src/daemon/tui/ui.rs
blob: 40033440e0bacd8a17958035f2c2a81ee206e5cd (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
//! TUI rendering.

use ratatui::{
    layout::{Alignment, Constraint, Direction, Layout, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span, Text},
    widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap},
    Frame,
};

use super::app::{App, InputMode, ViewType};
use super::event::get_help_text;

/// Main render function
pub fn render(frame: &mut Frame, app: &App) {
    // Create main layout
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(3),  // Header
            Constraint::Min(10),    // Main content
            Constraint::Length(3),  // Status/Help
        ])
        .split(frame.area());

    render_header(frame, app, chunks[0]);
    render_main_content(frame, app, chunks[1]);
    render_footer(frame, app, chunks[2]);

    // Render confirmation dialog if in confirm mode
    if app.input_mode == InputMode::Confirm {
        render_confirm_dialog(frame, app);
    }
}

/// Render header with title and search bar
fn render_header(frame: &mut Frame, app: &App, area: Rect) {
    let title = match app.view_type {
        ViewType::Tasks => "Tasks",
        ViewType::Contracts => "Contracts",
        ViewType::Files => "Files",
    };

    let header_text = if app.input_mode == InputMode::Search || !app.search_query.is_empty() {
        format!("{} [Search: {}]", title, app.search_query)
    } else {
        format!("{} ({} items)", title, app.filtered_items.len())
    };

    let header = Paragraph::new(header_text)
        .style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
        .block(Block::default()
            .borders(Borders::ALL)
            .border_style(Style::default().fg(if app.input_mode == InputMode::Search {
                Color::Yellow
            } else {
                Color::White
            })));

    frame.render_widget(header, area);
}

/// Render main content (list + optional preview)
fn render_main_content(frame: &mut Frame, app: &App, area: Rect) {
    if app.preview_visible && !app.preview_content.is_empty() {
        // Split horizontally: list on left, preview on right
        let chunks = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([
                Constraint::Percentage(50),
                Constraint::Percentage(50),
            ])
            .split(area);

        render_list(frame, app, chunks[0]);
        render_preview(frame, app, chunks[1]);
    } else {
        render_list(frame, app, area);
    }
}

/// Render the item list
fn render_list(frame: &mut Frame, app: &App, area: Rect) {
    let items: Vec<ListItem> = app.filtered_items
        .iter()
        .enumerate()
        .map(|(i, item)| {
            let is_selected = i == app.selected_index;

            // Build the display line
            let status_str = item.status
                .as_ref()
                .map(|s| format!(" [{}]", s))
                .unwrap_or_default();

            let content = format!("{}{}", item.name, status_str);

            let style = if is_selected {
                Style::default()
                    .fg(Color::Black)
                    .bg(Color::Cyan)
                    .add_modifier(Modifier::BOLD)
            } else {
                let status_color = item.status.as_ref().map(|s| {
                    match s.to_lowercase().as_str() {
                        "running" | "active" => Color::Green,
                        "pending" | "waiting" => Color::Yellow,
                        "completed" | "done" => Color::Blue,
                        "failed" | "error" => Color::Red,
                        _ => Color::White,
                    }
                }).unwrap_or(Color::White);

                Style::default().fg(status_color)
            };

            ListItem::new(Line::from(vec![
                Span::styled(content, style),
            ]))
        })
        .collect();

    let list = List::new(items)
        .block(Block::default()
            .borders(Borders::ALL)
            .title(format!(" {} ", app.view_type.as_str())));

    frame.render_widget(list, area);
}

/// Render the preview panel
fn render_preview(frame: &mut Frame, app: &App, area: Rect) {
    let preview = Paragraph::new(Text::raw(&app.preview_content))
        .wrap(Wrap { trim: false })
        .block(Block::default()
            .borders(Borders::ALL)
            .title(" Preview "));

    frame.render_widget(preview, area);
}

/// Render footer with help text and status
fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
    let help_text = get_help_text(app.input_mode);

    let status_text = app.status_message
        .as_ref()
        .map(|s| format!(" | {}", s))
        .unwrap_or_default();

    let footer_text = format!("{}{}", help_text, status_text);

    let footer = Paragraph::new(footer_text)
        .style(Style::default().fg(Color::DarkGray))
        .block(Block::default().borders(Borders::ALL));

    frame.render_widget(footer, area);
}

/// Render confirmation dialog as a centered popup
fn render_confirm_dialog(frame: &mut Frame, app: &App) {
    let item_name = app.get_pending_delete_name()
        .unwrap_or_else(|| "this item".to_string());

    // Calculate popup size and position
    let area = frame.area();
    let popup_width = 50.min(area.width.saturating_sub(4));
    let popup_height = 7;

    let popup_x = (area.width.saturating_sub(popup_width)) / 2;
    let popup_y = (area.height.saturating_sub(popup_height)) / 2;

    let popup_area = Rect {
        x: popup_x,
        y: popup_y,
        width: popup_width,
        height: popup_height,
    };

    // Clear the area behind the popup
    frame.render_widget(Clear, popup_area);

    // Build popup content
    let text = vec![
        Line::from(""),
        Line::from(Span::styled(
            "Delete Confirmation",
            Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
        )),
        Line::from(""),
        Line::from(format!("Delete '{}'?", item_name)),
        Line::from(""),
        Line::from(vec![
            Span::styled("y", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
            Span::raw(": confirm  "),
            Span::styled("n", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
            Span::raw(": cancel"),
        ]),
    ];

    let popup = Paragraph::new(text)
        .alignment(Alignment::Center)
        .block(Block::default()
            .borders(Borders::ALL)
            .border_style(Style::default().fg(Color::Red))
            .title(" Confirm "));

    frame.render_widget(popup, popup_area);
}