thd75_tui/ui/
channels.rs

1use ratatui::Frame;
2use ratatui::layout::Rect;
3use ratatui::style::{Color, Modifier, Style};
4use ratatui::text::{Line, Span};
5use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph};
6
7use crate::app::{App, ChannelEditField, InputMode, McpState, Pane};
8
9pub(crate) fn render_list(app: &App, frame: &mut Frame<'_>, area: Rect) {
10    let title = if let InputMode::Search(ref buf) = app.input_mode {
11        format!(" Search: {buf}▎ ")
12    } else if !app.search_filter.is_empty() {
13        format!(" Channels [filter: {}] ", app.search_filter)
14    } else {
15        " Channels ".to_string()
16    };
17
18    let block = Block::default()
19        .title(title)
20        .borders(Borders::ALL)
21        .border_style(super::border_style(app, Pane::Main));
22
23    if let McpState::Loaded { image, .. } = &app.mcp {
24        let channels = image.channels();
25        let used = app.filtered_channels();
26        let items: Vec<ListItem<'_>> = used
27            .iter()
28            .map(|&i| {
29                let entry = channels.get(i);
30                let name = entry.as_ref().map(|e| e.name.clone()).unwrap_or_default();
31                let freq = entry
32                    .as_ref()
33                    .map(|e| format!("{:.3}", e.flash.rx_frequency.as_mhz()))
34                    .unwrap_or_default();
35                ListItem::new(Line::from(vec![
36                    Span::styled(format!("{i:>4}: "), Style::default().fg(Color::DarkGray)),
37                    Span::styled(format!("{name:<12}"), Style::default().fg(Color::White)),
38                    Span::styled(format!(" {freq}"), Style::default().fg(Color::Cyan)),
39                ]))
40            })
41            .collect();
42
43        let mut list_state = ListState::default();
44        list_state.select(Some(
45            app.channel_list_index.min(items.len().saturating_sub(1)),
46        ));
47
48        let list = List::new(items)
49            .block(block)
50            .highlight_style(
51                Style::default()
52                    .fg(Color::Black)
53                    .bg(Color::Cyan)
54                    .add_modifier(Modifier::BOLD),
55            )
56            .highlight_symbol("▸ ");
57
58        frame.render_stateful_widget(list, area, &mut list_state);
59    } else {
60        let msg = " No MCP data loaded.\n Press [m] then [r] to read from radio.";
61        frame.render_widget(Paragraph::new(msg).block(block), area);
62    }
63}
64
65pub(crate) fn render_detail(app: &App, frame: &mut Frame<'_>, area: Rect) {
66    let block = Block::default()
67        .title(" Detail ")
68        .borders(Borders::ALL)
69        .border_style(super::border_style(app, Pane::Detail));
70
71    match &app.mcp {
72        McpState::Loaded { image, .. } => {
73            let channels = image.channels();
74            let used = app.filtered_channels();
75            if let Some(&ch_num) = used.get(app.channel_list_index)
76                && let Some(entry) = channels.get(ch_num)
77            {
78                let fc = &entry.flash;
79
80                // Tone/squelch summary string
81                let tone_info = if fc.tone_enabled {
82                    format!("CTCSS TX {}", fc.tone_code.index())
83                } else if fc.ctcss_enabled {
84                    format!("CTCSS {}/{}", fc.tone_code.index(), fc.ctcss_code.index())
85                } else if fc.dtcs_enabled {
86                    format!("DCS {:03}", u16::from(fc.dcs_code.index()))
87                } else {
88                    "None".to_string()
89                };
90
91                // Duplex direction string
92                let duplex_info = match fc.duplex {
93                    kenwood_thd75::types::FlashDuplex::Simplex => "Simplex".to_string(),
94                    kenwood_thd75::types::FlashDuplex::Plus => {
95                        format!("+{:.3} MHz", fc.tx_offset.as_mhz())
96                    }
97                    kenwood_thd75::types::FlashDuplex::Minus => {
98                        format!("-{:.3} MHz", fc.tx_offset.as_mhz())
99                    }
100                };
101
102                let mut lines = vec![
103                    Line::from(vec![
104                        Span::styled("  Channel: ", Style::default().fg(Color::DarkGray)),
105                        Span::styled(format!("{ch_num}"), Style::default().fg(Color::White)),
106                    ]),
107                    Line::from(vec![
108                        Span::styled("  Name:    ", Style::default().fg(Color::DarkGray)),
109                        Span::styled(
110                            entry.name.clone(),
111                            Style::default()
112                                .fg(Color::Cyan)
113                                .add_modifier(Modifier::BOLD),
114                        ),
115                    ]),
116                    Line::from(""),
117                    Line::from(vec![
118                        Span::styled("  RX:      ", Style::default().fg(Color::DarkGray)),
119                        Span::styled(
120                            format!("{:.6} MHz", fc.rx_frequency.as_mhz()),
121                            Style::default().fg(Color::Green),
122                        ),
123                    ]),
124                    Line::from(vec![
125                        Span::styled("  Duplex:  ", Style::default().fg(Color::DarkGray)),
126                        Span::styled(duplex_info, Style::default().fg(Color::Yellow)),
127                    ]),
128                    Line::from(vec![
129                        Span::styled("  Mode:    ", Style::default().fg(Color::DarkGray)),
130                        Span::styled(format!("{}", fc.mode), Style::default().fg(Color::White)),
131                    ]),
132                    Line::from(vec![
133                        Span::styled("  Tone:    ", Style::default().fg(Color::DarkGray)),
134                        Span::styled(tone_info, Style::default().fg(Color::White)),
135                    ]),
136                ];
137
138                lines.push(Line::from(""));
139                if app.channel_edit_mode {
140                    lines.push(Line::from(Span::styled(
141                        "  ── Edit Mode ──",
142                        Style::default()
143                            .fg(Color::Yellow)
144                            .add_modifier(Modifier::BOLD),
145                    )));
146
147                    let fields = [
148                        ChannelEditField::Frequency,
149                        ChannelEditField::Name,
150                        ChannelEditField::Mode,
151                        ChannelEditField::ToneMode,
152                        ChannelEditField::ToneFreq,
153                        ChannelEditField::Duplex,
154                        ChannelEditField::Offset,
155                    ];
156                    for field in fields {
157                        let marker = if field == app.channel_edit_field {
158                            "\u{25b8} "
159                        } else {
160                            "  "
161                        };
162                        let color = if field == app.channel_edit_field {
163                            Color::Cyan
164                        } else {
165                            Color::DarkGray
166                        };
167                        lines.push(Line::from(Span::styled(
168                            format!("  {marker}{:<12}", field.label()),
169                            Style::default().fg(color),
170                        )));
171                    }
172
173                    if !app.channel_edit_buffer.is_empty() {
174                        lines.push(Line::from(""));
175                        lines.push(Line::from(vec![
176                            Span::styled("  Input: ", Style::default().fg(Color::DarkGray)),
177                            Span::styled(
178                                format!("{}\u{258e}", app.channel_edit_buffer),
179                                Style::default().fg(Color::White),
180                            ),
181                        ]));
182                    }
183
184                    lines.push(Line::from(""));
185                    lines.push(Line::from(Span::styled(
186                        "  Tab=next  Enter=apply  Esc=cancel",
187                        Style::default().fg(Color::DarkGray),
188                    )));
189                } else {
190                    lines.push(Line::from(vec![Span::styled(
191                        format!(
192                            "  [Enter] Tune Band {}  [e] Edit",
193                            if app.target_band == kenwood_thd75::types::Band::B {
194                                "B"
195                            } else {
196                                "A"
197                            }
198                        ),
199                        Style::default().fg(Color::DarkGray),
200                    )]));
201                }
202                frame.render_widget(Paragraph::new(lines).block(block), area);
203                return;
204            }
205            frame.render_widget(Paragraph::new("  No channel selected").block(block), area);
206        }
207        _ => {
208            frame.render_widget(Paragraph::new("").block(block), area);
209        }
210    }
211}