Skip to main content

rusticity_term/cfn/
actions.rs

1use crate::app::App;
2use crate::common::{CyclicEnum, InputFocus, PageSize};
3use crate::ui::cfn::{
4    filtered_change_sets, filtered_cloudformation_stacks, filtered_events, filtered_outputs,
5    filtered_parameters, filtered_resources, DetailTab as CfnDetailTab,
6    EventsView as CfnEventsView, State as CfnStateConstants, STATUS_FILTER, VIEW_NESTED,
7};
8
9// ── Filter / reset ────────────────────────────────────────────────────────────
10
11pub fn apply_filter_reset(app: &mut App) {
12    if app.cfn_state.current_stack.is_some() && app.cfn_state.detail_tab == CfnDetailTab::Parameters
13    {
14        app.cfn_state.parameters.reset();
15    } else if app.cfn_state.current_stack.is_some()
16        && app.cfn_state.detail_tab == CfnDetailTab::Outputs
17    {
18        app.cfn_state.outputs.reset();
19    } else if app.cfn_state.current_stack.is_some()
20        && app.cfn_state.detail_tab == CfnDetailTab::Events
21    {
22        app.cfn_state.events.reset();
23    } else if app.cfn_state.current_stack.is_some()
24        && app.cfn_state.detail_tab == CfnDetailTab::ChangeSets
25    {
26        app.cfn_state.change_sets.reset();
27    } else {
28        app.cfn_state.table.reset();
29    }
30}
31
32pub fn reset_on_service_select(app: &mut App) {
33    app.cfn_state.table.reset();
34}
35
36pub fn is_pagination_focused(app: &App) -> bool {
37    app.cfn_state.input_focus == InputFocus::Pagination
38}
39
40pub fn filter_char_push(app: &mut App, c: char) {
41    if app.cfn_state.current_stack.is_some() && app.cfn_state.detail_tab == CfnDetailTab::Parameters
42    {
43        if app.cfn_state.parameters_input_focus == InputFocus::Filter {
44            app.cfn_state.parameters.filter.push(c);
45            app.cfn_state.parameters.selected = 0;
46        }
47    } else if app.cfn_state.current_stack.is_some()
48        && app.cfn_state.detail_tab == CfnDetailTab::Outputs
49    {
50        if app.cfn_state.outputs_input_focus == InputFocus::Filter {
51            app.cfn_state.outputs.filter.push(c);
52            app.cfn_state.outputs.selected = 0;
53        }
54    } else if app.cfn_state.current_stack.is_some()
55        && app.cfn_state.detail_tab == CfnDetailTab::Resources
56    {
57        if app.cfn_state.resources_input_focus == InputFocus::Filter {
58            app.cfn_state.resources.filter.push(c);
59            app.cfn_state.resources.selected = 0;
60        }
61    } else if app.cfn_state.current_stack.is_some()
62        && app.cfn_state.detail_tab == CfnDetailTab::Events
63    {
64        if app.cfn_state.events_input_focus == InputFocus::Filter {
65            app.cfn_state.events.filter.push(c);
66            app.cfn_state.events.selected = 0;
67        }
68    } else if app.cfn_state.current_stack.is_some()
69        && app.cfn_state.detail_tab == CfnDetailTab::ChangeSets
70    {
71        if app.cfn_state.change_sets_input_focus == InputFocus::Filter {
72            app.cfn_state.change_sets.filter.push(c);
73            app.cfn_state.change_sets.selected = 0;
74        }
75    } else if app.cfn_state.input_focus == InputFocus::Filter {
76        app.cfn_state.table.filter.push(c);
77        app.cfn_state.table.selected = 0;
78    }
79}
80
81pub fn filter_char_pop(app: &mut App) {
82    if app.cfn_state.current_stack.is_some() && app.cfn_state.detail_tab == CfnDetailTab::Parameters
83    {
84        if app.cfn_state.parameters_input_focus == InputFocus::Filter {
85            app.cfn_state.parameters.filter.pop();
86            app.cfn_state.parameters.selected = 0;
87        }
88    } else if app.cfn_state.current_stack.is_some()
89        && app.cfn_state.detail_tab == CfnDetailTab::Outputs
90    {
91        if app.cfn_state.outputs_input_focus == InputFocus::Filter {
92            app.cfn_state.outputs.filter.pop();
93            app.cfn_state.outputs.selected = 0;
94        }
95    } else if app.cfn_state.current_stack.is_some()
96        && app.cfn_state.detail_tab == CfnDetailTab::Resources
97    {
98        if app.cfn_state.resources_input_focus == InputFocus::Filter {
99            app.cfn_state.resources.filter.pop();
100            app.cfn_state.resources.selected = 0;
101        }
102    } else if app.cfn_state.current_stack.is_some()
103        && app.cfn_state.detail_tab == CfnDetailTab::Events
104    {
105        if app.cfn_state.events_input_focus == InputFocus::Filter {
106            app.cfn_state.events.filter.pop();
107            app.cfn_state.events.selected = 0;
108        }
109    } else if app.cfn_state.current_stack.is_some()
110        && app.cfn_state.detail_tab == CfnDetailTab::ChangeSets
111    {
112        if app.cfn_state.change_sets_input_focus == InputFocus::Filter {
113            app.cfn_state.change_sets.filter.pop();
114            app.cfn_state.change_sets.selected = 0;
115        }
116    } else if app.cfn_state.input_focus == InputFocus::Filter {
117        app.cfn_state.table.filter.pop();
118        app.cfn_state.table.selected = 0;
119    }
120}
121
122pub fn start_filter(app: &mut App) {
123    if app.cfn_state.current_stack.is_some() && app.cfn_state.detail_tab == CfnDetailTab::Parameters
124    {
125        app.cfn_state.parameters_input_focus = InputFocus::Filter;
126    } else if app.cfn_state.current_stack.is_some()
127        && app.cfn_state.detail_tab == CfnDetailTab::Outputs
128    {
129        app.cfn_state.outputs_input_focus = InputFocus::Filter;
130    } else if app.cfn_state.current_stack.is_some()
131        && app.cfn_state.detail_tab == CfnDetailTab::Events
132    {
133        app.cfn_state.events_input_focus = InputFocus::Filter;
134    } else if app.cfn_state.current_stack.is_some()
135        && app.cfn_state.detail_tab == CfnDetailTab::ChangeSets
136    {
137        app.cfn_state.change_sets_input_focus = InputFocus::Filter;
138    } else {
139        app.cfn_state.input_focus = InputFocus::Filter;
140    }
141}
142
143pub fn next_filter_focus(app: &mut App) {
144    if app.cfn_state.current_stack.is_some() && app.cfn_state.detail_tab == CfnDetailTab::Parameters
145    {
146        app.cfn_state.parameters_input_focus = app
147            .cfn_state
148            .parameters_input_focus
149            .next(&CfnStateConstants::PARAMETERS_FILTER_CONTROLS);
150    } else if app.cfn_state.current_stack.is_some()
151        && app.cfn_state.detail_tab == CfnDetailTab::Outputs
152    {
153        app.cfn_state.outputs_input_focus = app
154            .cfn_state
155            .outputs_input_focus
156            .next(&CfnStateConstants::OUTPUTS_FILTER_CONTROLS);
157    } else if app.cfn_state.current_stack.is_some()
158        && app.cfn_state.detail_tab == CfnDetailTab::Resources
159    {
160        app.cfn_state.resources_input_focus = app
161            .cfn_state
162            .resources_input_focus
163            .next(&CfnStateConstants::RESOURCES_FILTER_CONTROLS);
164    } else if app.cfn_state.current_stack.is_some()
165        && app.cfn_state.detail_tab == CfnDetailTab::Events
166    {
167        app.cfn_state.events_input_focus = app
168            .cfn_state
169            .events_input_focus
170            .next(&CfnStateConstants::EVENTS_FILTER_CONTROLS);
171    } else if app.cfn_state.current_stack.is_some()
172        && app.cfn_state.detail_tab == CfnDetailTab::ChangeSets
173    {
174        app.cfn_state.change_sets_input_focus = app
175            .cfn_state
176            .change_sets_input_focus
177            .next(&CfnStateConstants::CHANGE_SETS_FILTER_CONTROLS);
178    } else {
179        app.cfn_state.input_focus = app
180            .cfn_state
181            .input_focus
182            .next(&CfnStateConstants::FILTER_CONTROLS);
183    }
184}
185
186pub fn prev_filter_focus(app: &mut App) {
187    if app.cfn_state.current_stack.is_some() && app.cfn_state.detail_tab == CfnDetailTab::Parameters
188    {
189        app.cfn_state.parameters_input_focus = app
190            .cfn_state
191            .parameters_input_focus
192            .prev(&CfnStateConstants::PARAMETERS_FILTER_CONTROLS);
193    } else if app.cfn_state.current_stack.is_some()
194        && app.cfn_state.detail_tab == CfnDetailTab::Outputs
195    {
196        app.cfn_state.outputs_input_focus = app
197            .cfn_state
198            .outputs_input_focus
199            .prev(&CfnStateConstants::OUTPUTS_FILTER_CONTROLS);
200    } else if app.cfn_state.current_stack.is_some()
201        && app.cfn_state.detail_tab == CfnDetailTab::Resources
202    {
203        app.cfn_state.resources_input_focus = app
204            .cfn_state
205            .resources_input_focus
206            .prev(&CfnStateConstants::RESOURCES_FILTER_CONTROLS);
207    } else if app.cfn_state.current_stack.is_some()
208        && app.cfn_state.detail_tab == CfnDetailTab::Events
209    {
210        app.cfn_state.events_input_focus = app
211            .cfn_state
212            .events_input_focus
213            .prev(&CfnStateConstants::EVENTS_FILTER_CONTROLS);
214    } else if app.cfn_state.current_stack.is_some()
215        && app.cfn_state.detail_tab == CfnDetailTab::ChangeSets
216    {
217        app.cfn_state.change_sets_input_focus = app
218            .cfn_state
219            .change_sets_input_focus
220            .prev(&CfnStateConstants::CHANGE_SETS_FILTER_CONTROLS);
221    } else {
222        app.cfn_state.input_focus = app
223            .cfn_state
224            .input_focus
225            .prev(&CfnStateConstants::FILTER_CONTROLS);
226    }
227}
228
229pub fn toggle_filter_checkbox(app: &mut App) {
230    match app.cfn_state.input_focus {
231        STATUS_FILTER => {
232            app.cfn_state.status_filter = app.cfn_state.status_filter.next();
233            app.cfn_state.table.reset();
234        }
235        VIEW_NESTED => {
236            app.cfn_state.view_nested = !app.cfn_state.view_nested;
237            // Clear expanded nested stack state
238            app.cfn_state.expanded_items.clear();
239            // Set loading=true to trigger a reload with the new nested setting.
240            app.cfn_state.table.reset();
241            app.cfn_state.table.loading = true;
242        }
243        _ => {}
244    }
245}
246
247pub fn block_column_selector(app: &App) -> bool {
248    app.cfn_state.current_stack.is_some()
249        && (app.cfn_state.detail_tab == CfnDetailTab::Template
250            || app.cfn_state.detail_tab == CfnDetailTab::GitSync)
251}
252
253// ── Navigation ────────────────────────────────────────────────────────────────
254
255pub fn next_item(app: &mut App) {
256    if app.cfn_state.current_stack.is_some() && app.cfn_state.detail_tab == CfnDetailTab::Template {
257        scroll_down_template(app);
258    } else if app.cfn_state.current_stack.is_some()
259        && app.cfn_state.detail_tab == CfnDetailTab::Parameters
260    {
261        let filtered = filtered_parameters(app);
262        app.cfn_state.parameters.next_item(filtered.len());
263    } else if app.cfn_state.current_stack.is_some()
264        && app.cfn_state.detail_tab == CfnDetailTab::Outputs
265    {
266        let filtered = filtered_outputs(app);
267        app.cfn_state.outputs.next_item(filtered.len());
268    } else if app.cfn_state.current_stack.is_some()
269        && app.cfn_state.detail_tab == CfnDetailTab::Resources
270    {
271        let filtered = filtered_resources(app);
272        app.cfn_state.resources.next_item(filtered.len());
273    } else if app.cfn_state.current_stack.is_some()
274        && app.cfn_state.detail_tab == CfnDetailTab::Events
275    {
276        let filtered = filtered_events(app);
277        app.cfn_state.events.next_item(filtered.len());
278    } else if app.cfn_state.current_stack.is_some()
279        && app.cfn_state.detail_tab == CfnDetailTab::ChangeSets
280    {
281        let filtered = filtered_change_sets(app);
282        app.cfn_state.change_sets.next_item(filtered.len());
283    } else {
284        let filtered = filtered_cloudformation_stacks(app);
285        // Compute extra rows from expanded content that appear before the NEXT selected item.
286        let next_selected =
287            (app.cfn_state.table.selected + 1).min(filtered.len().saturating_sub(1));
288        let extra_rows = if let Some(exp_idx) = app.cfn_state.table.expanded_item {
289            let page_size = app.cfn_state.table.page_size.value();
290            let scroll = app.cfn_state.table.scroll_offset;
291            // The expanded item must be on the current page and strictly before next_selected
292            if exp_idx >= scroll && exp_idx < scroll + page_size && exp_idx < next_selected {
293                app.cfn_visible_column_ids.len()
294            } else {
295                0
296            }
297        } else {
298            0
299        };
300        app.cfn_state
301            .table
302            .next_item_with_expansion(filtered.len(), extra_rows);
303    }
304}
305
306pub fn next_item_filter_input(app: &mut App) {
307    if app.cfn_state.input_focus == STATUS_FILTER {
308        app.cfn_state.status_filter = app.cfn_state.status_filter.next();
309        app.cfn_state.table.reset();
310    }
311}
312
313pub fn prev_item(app: &mut App) {
314    if app.cfn_state.current_stack.is_some() && app.cfn_state.detail_tab == CfnDetailTab::Template {
315        scroll_up_template(app);
316    } else if app.cfn_state.current_stack.is_some()
317        && app.cfn_state.detail_tab == CfnDetailTab::Parameters
318    {
319        app.cfn_state.parameters.prev_item();
320    } else if app.cfn_state.current_stack.is_some()
321        && app.cfn_state.detail_tab == CfnDetailTab::Outputs
322    {
323        app.cfn_state.outputs.prev_item();
324    } else if app.cfn_state.current_stack.is_some()
325        && app.cfn_state.detail_tab == CfnDetailTab::Resources
326    {
327        app.cfn_state.resources.prev_item();
328    } else if app.cfn_state.current_stack.is_some()
329        && app.cfn_state.detail_tab == CfnDetailTab::Events
330    {
331        app.cfn_state.events.prev_item();
332    } else if app.cfn_state.current_stack.is_some()
333        && app.cfn_state.detail_tab == CfnDetailTab::ChangeSets
334    {
335        app.cfn_state.change_sets.prev_item();
336    } else {
337        app.cfn_state.table.prev_item();
338    }
339}
340
341pub fn prev_item_filter_input(app: &mut App) {
342    if app.cfn_state.input_focus == STATUS_FILTER {
343        app.cfn_state.status_filter = app.cfn_state.status_filter.prev();
344        app.cfn_state.table.reset();
345    }
346}
347
348pub fn page_down_filter_input(app: &mut App) {
349    if app.cfn_state.current_stack.is_some() && app.cfn_state.detail_tab == CfnDetailTab::Parameters
350    {
351        let page_size = app.cfn_state.parameters.page_size.value();
352        let filtered_count = filtered_parameters(app).len();
353        app.cfn_state.parameters_input_focus.handle_page_down(
354            &mut app.cfn_state.parameters.selected,
355            &mut app.cfn_state.parameters.scroll_offset,
356            page_size,
357            filtered_count,
358        );
359    } else if app.cfn_state.current_stack.is_some()
360        && app.cfn_state.detail_tab == CfnDetailTab::Outputs
361    {
362        let page_size = app.cfn_state.outputs.page_size.value();
363        let filtered_count = filtered_outputs(app).len();
364        app.cfn_state.outputs_input_focus.handle_page_down(
365            &mut app.cfn_state.outputs.selected,
366            &mut app.cfn_state.outputs.scroll_offset,
367            page_size,
368            filtered_count,
369        );
370    } else if app.cfn_state.current_stack.is_some()
371        && app.cfn_state.detail_tab == CfnDetailTab::Resources
372    {
373        let page_size = app.cfn_state.resources.page_size.value();
374        let filtered_count = filtered_resources(app).len();
375        app.cfn_state.resources_input_focus.handle_page_down(
376            &mut app.cfn_state.resources.selected,
377            &mut app.cfn_state.resources.scroll_offset,
378            page_size,
379            filtered_count,
380        );
381    } else if app.cfn_state.current_stack.is_some()
382        && app.cfn_state.detail_tab == CfnDetailTab::Events
383    {
384        let page_size = app.cfn_state.events.page_size.value();
385        let filtered_count = filtered_events(app).len();
386        app.cfn_state.events_input_focus.handle_page_down(
387            &mut app.cfn_state.events.selected,
388            &mut app.cfn_state.events.scroll_offset,
389            page_size,
390            filtered_count,
391        );
392    } else if app.cfn_state.current_stack.is_some()
393        && app.cfn_state.detail_tab == CfnDetailTab::ChangeSets
394    {
395        let page_size = app.cfn_state.change_sets.page_size.value();
396        let filtered_count = filtered_change_sets(app).len();
397        app.cfn_state.change_sets_input_focus.handle_page_down(
398            &mut app.cfn_state.change_sets.selected,
399            &mut app.cfn_state.change_sets.scroll_offset,
400            page_size,
401            filtered_count,
402        );
403    } else {
404        let page_size = app.cfn_state.table.page_size.value();
405        let filtered_count = filtered_cloudformation_stacks(app).len();
406        app.cfn_state.input_focus.handle_page_down(
407            &mut app.cfn_state.table.selected,
408            &mut app.cfn_state.table.scroll_offset,
409            page_size,
410            filtered_count,
411        );
412    }
413}
414
415pub fn page_down_normal(app: &mut App) {
416    if app.cfn_state.current_stack.is_some() && app.cfn_state.detail_tab == CfnDetailTab::Parameters
417    {
418        let filtered = filtered_parameters(app);
419        app.cfn_state.parameters.page_down(filtered.len());
420    } else if app.cfn_state.current_stack.is_some()
421        && app.cfn_state.detail_tab == CfnDetailTab::Outputs
422    {
423        let filtered = filtered_outputs(app);
424        app.cfn_state.outputs.page_down(filtered.len());
425    } else if app.cfn_state.current_stack.is_some()
426        && app.cfn_state.detail_tab == CfnDetailTab::Resources
427    {
428        let filtered = filtered_resources(app);
429        app.cfn_state.resources.page_down(filtered.len());
430    } else if app.cfn_state.current_stack.is_some()
431        && app.cfn_state.detail_tab == CfnDetailTab::Events
432    {
433        let filtered = filtered_events(app);
434        app.cfn_state.events.page_down(filtered.len());
435    } else if app.cfn_state.current_stack.is_some()
436        && app.cfn_state.detail_tab == CfnDetailTab::ChangeSets
437    {
438        let filtered = filtered_change_sets(app);
439        app.cfn_state.change_sets.page_down(filtered.len());
440    } else {
441        let filtered = filtered_cloudformation_stacks(app);
442        app.cfn_state.table.page_down(filtered.len());
443    }
444}
445
446pub fn page_up_filter_input(app: &mut App) {
447    if app.cfn_state.current_stack.is_some() && app.cfn_state.detail_tab == CfnDetailTab::Parameters
448    {
449        let page_size = app.cfn_state.parameters.page_size.value();
450        app.cfn_state.parameters_input_focus.handle_page_up(
451            &mut app.cfn_state.parameters.selected,
452            &mut app.cfn_state.parameters.scroll_offset,
453            page_size,
454        );
455    } else if app.cfn_state.current_stack.is_some()
456        && app.cfn_state.detail_tab == CfnDetailTab::Outputs
457    {
458        let page_size = app.cfn_state.outputs.page_size.value();
459        app.cfn_state.outputs_input_focus.handle_page_up(
460            &mut app.cfn_state.outputs.selected,
461            &mut app.cfn_state.outputs.scroll_offset,
462            page_size,
463        );
464    } else if app.cfn_state.current_stack.is_some()
465        && app.cfn_state.detail_tab == CfnDetailTab::Resources
466    {
467        let page_size = app.cfn_state.resources.page_size.value();
468        app.cfn_state.resources_input_focus.handle_page_up(
469            &mut app.cfn_state.resources.selected,
470            &mut app.cfn_state.resources.scroll_offset,
471            page_size,
472        );
473    } else if app.cfn_state.current_stack.is_some()
474        && app.cfn_state.detail_tab == CfnDetailTab::Events
475    {
476        let page_size = app.cfn_state.events.page_size.value();
477        app.cfn_state.events_input_focus.handle_page_up(
478            &mut app.cfn_state.events.selected,
479            &mut app.cfn_state.events.scroll_offset,
480            page_size,
481        );
482    } else if app.cfn_state.current_stack.is_some()
483        && app.cfn_state.detail_tab == CfnDetailTab::ChangeSets
484    {
485        let page_size = app.cfn_state.change_sets.page_size.value();
486        app.cfn_state.change_sets_input_focus.handle_page_up(
487            &mut app.cfn_state.change_sets.selected,
488            &mut app.cfn_state.change_sets.scroll_offset,
489            page_size,
490        );
491    } else {
492        let page_size = app.cfn_state.table.page_size.value();
493        app.cfn_state.input_focus.handle_page_up(
494            &mut app.cfn_state.table.selected,
495            &mut app.cfn_state.table.scroll_offset,
496            page_size,
497        );
498    }
499}
500
501pub fn page_up_normal(app: &mut App) {
502    if app.cfn_state.current_stack.is_some() && app.cfn_state.detail_tab == CfnDetailTab::Parameters
503    {
504        app.cfn_state.parameters.page_up();
505    } else if app.cfn_state.current_stack.is_some()
506        && app.cfn_state.detail_tab == CfnDetailTab::Outputs
507    {
508        app.cfn_state.outputs.page_up();
509    } else if app.cfn_state.current_stack.is_some()
510        && app.cfn_state.detail_tab == CfnDetailTab::Resources
511    {
512        app.cfn_state.resources.page_up();
513    } else if app.cfn_state.current_stack.is_some()
514        && app.cfn_state.detail_tab == CfnDetailTab::Events
515    {
516        app.cfn_state.events.page_up();
517    } else if app.cfn_state.current_stack.is_some()
518        && app.cfn_state.detail_tab == CfnDetailTab::ChangeSets
519    {
520        app.cfn_state.change_sets.page_up();
521    } else {
522        app.cfn_state.table.page_up();
523    }
524}
525
526pub fn scroll_up_template(app: &mut App) {
527    app.cfn_state.template_scroll = app.cfn_state.template_scroll.saturating_sub(1);
528}
529
530pub fn scroll_down_template(app: &mut App) {
531    let lines = app.cfn_state.template_body.lines().count();
532    let max_scroll = lines.saturating_sub(1);
533    app.cfn_state.template_scroll = (app.cfn_state.template_scroll + 1).min(max_scroll);
534}
535
536pub fn scroll_up_template_fast(app: &mut App) {
537    app.cfn_state.template_scroll = app.cfn_state.template_scroll.saturating_sub(10);
538}
539
540pub fn scroll_down_template_fast(app: &mut App) {
541    let lines = app.cfn_state.template_body.lines().count();
542    let max_scroll = lines.saturating_sub(1);
543    app.cfn_state.template_scroll = (app.cfn_state.template_scroll + 10).min(max_scroll);
544}
545
546pub fn expand_row(app: &mut App) {
547    if app.cfn_state.current_stack.is_none() {
548        app.cfn_state.table.toggle_expand();
549        // If just expanded, scroll up to ensure detail rows are visible
550        if app.cfn_state.table.expanded_item.is_some() {
551            let detail_rows = app.cfn_visible_column_ids.len();
552            let page_size = app.cfn_state.table.page_size.value();
553            app.cfn_state
554                .table
555                .ensure_expansion_visible(detail_rows, page_size);
556        }
557    } else if app.cfn_state.detail_tab == CfnDetailTab::Parameters {
558        app.cfn_state.parameters.toggle_expand();
559    } else if app.cfn_state.detail_tab == CfnDetailTab::Outputs {
560        app.cfn_state.outputs.toggle_expand();
561    } else if app.cfn_state.detail_tab == CfnDetailTab::Resources {
562        app.cfn_state.resources.toggle_expand();
563    } else if app.cfn_state.detail_tab == CfnDetailTab::Events {
564        app.cfn_state.events.toggle_expand();
565    } else if app.cfn_state.detail_tab == CfnDetailTab::ChangeSets {
566        app.cfn_state.change_sets.toggle_expand();
567    }
568}
569
570pub fn prev_pane(app: &mut App) {
571    if app.cfn_state.current_stack.is_none() {
572        app.cfn_state.table.collapse();
573    } else if app.cfn_state.detail_tab == CfnDetailTab::Parameters {
574        app.cfn_state.parameters.collapse();
575    } else if app.cfn_state.detail_tab == CfnDetailTab::Outputs {
576        app.cfn_state.outputs.collapse();
577    } else if app.cfn_state.detail_tab == CfnDetailTab::Resources {
578        app.cfn_state.resources.collapse();
579    } else if app.cfn_state.detail_tab == CfnDetailTab::Events {
580        app.cfn_state.events.collapse();
581    } else if app.cfn_state.detail_tab == CfnDetailTab::ChangeSets {
582        app.cfn_state.change_sets.collapse();
583    }
584}
585
586pub fn collapse_row(app: &mut App) {
587    if app.cfn_state.current_stack.is_some() {
588        match app.cfn_state.detail_tab {
589            CfnDetailTab::Resources => app.cfn_state.resources.collapse(),
590            CfnDetailTab::Parameters => app.cfn_state.parameters.collapse(),
591            CfnDetailTab::Outputs => app.cfn_state.outputs.collapse(),
592            CfnDetailTab::Events => app.cfn_state.events.collapse(),
593            CfnDetailTab::ChangeSets => app.cfn_state.change_sets.collapse(),
594            _ => {}
595        }
596    } else {
597        app.cfn_state.table.collapse();
598    }
599}
600
601pub fn go_to_page(app: &mut App, page: usize) {
602    let filtered_count = filtered_cloudformation_stacks(app).len();
603    app.cfn_state.table.goto_page(page, filtered_count);
604}
605
606// ── Actions ───────────────────────────────────────────────────────────────────
607
608pub fn select_item(app: &mut App) {
609    if app.cfn_state.current_stack.is_none() {
610        let filtered = filtered_cloudformation_stacks(app);
611        if let Some(stack) = app.cfn_state.table.get_selected(&filtered) {
612            let stack_name = stack.name.clone();
613            let mut tags = stack.tags.clone();
614            tags.sort_by(|a, b| a.0.cmp(&b.0));
615            app.cfn_state.current_stack = Some(stack_name);
616            app.cfn_state.tags.items = tags;
617            app.cfn_state.tags.reset();
618            app.cfn_state.table.loading = true;
619            app.update_current_tab_breadcrumb();
620        }
621    }
622}
623
624pub fn go_back(app: &mut App) {
625    app.cfn_state.current_stack = None;
626    app.update_current_tab_breadcrumb();
627}
628
629pub fn next_detail_tab(app: &mut App) {
630    app.cfn_state.detail_tab = app.cfn_state.detail_tab.next();
631}
632
633pub fn prev_detail_tab(app: &mut App) {
634    app.cfn_state.detail_tab = app.cfn_state.detail_tab.prev();
635}
636
637/// Toggle between Table and Timeline view on the Events tab (bound to Tab key).
638pub fn toggle_events_view(app: &mut App) {
639    app.cfn_state.events_view = match app.cfn_state.events_view {
640        CfnEventsView::Table => CfnEventsView::Timeline,
641        CfnEventsView::Timeline => CfnEventsView::Table,
642    };
643}
644
645pub fn yank(app: &App) {
646    use crate::app::copy_to_clipboard;
647    if let Some(stack_name) = &app.cfn_state.current_stack {
648        // On Template tab — copy the full template body
649        if app.cfn_state.detail_tab == CfnDetailTab::Template {
650            if !app.cfn_state.template_body.is_empty() {
651                copy_to_clipboard(&app.cfn_state.template_body);
652            }
653            return;
654        }
655        if let Some(stack) = app
656            .cfn_state
657            .table
658            .items
659            .iter()
660            .find(|s| &s.name == stack_name)
661        {
662            copy_to_clipboard(&stack.stack_id);
663        }
664    } else {
665        let filtered = filtered_cloudformation_stacks(app);
666        if let Some(stack) = app.cfn_state.table.get_selected(&filtered) {
667            copy_to_clipboard(&stack.stack_id);
668        }
669    }
670}
671
672// ── Column preferences ────────────────────────────────────────────────────────
673
674pub fn toggle_column(app: &mut App) {
675    let idx = app.column_selector_index;
676    if app.cfn_state.current_stack.is_some() && app.cfn_state.detail_tab == CfnDetailTab::StackInfo
677    {
678        // Tags: page size only
679        if idx == 4 {
680            app.cfn_state.tags.page_size = PageSize::Ten;
681        } else if idx == 5 {
682            app.cfn_state.tags.page_size = PageSize::TwentyFive;
683        } else if idx == 6 {
684            app.cfn_state.tags.page_size = PageSize::Fifty;
685        } else if idx == 7 {
686            app.cfn_state.tags.page_size = PageSize::OneHundred;
687        }
688    } else if app.cfn_state.current_stack.is_some()
689        && app.cfn_state.detail_tab == CfnDetailTab::Parameters
690    {
691        if idx > 0 && idx <= app.cfn_parameter_column_ids.len() {
692            if let Some(col) = app.cfn_parameter_column_ids.get(idx - 1) {
693                if let Some(pos) = app
694                    .cfn_parameter_visible_column_ids
695                    .iter()
696                    .position(|c| c == col)
697                {
698                    if app.cfn_parameter_visible_column_ids.len() > 1 {
699                        app.cfn_parameter_visible_column_ids.remove(pos);
700                    }
701                } else {
702                    app.cfn_parameter_visible_column_ids.push(col);
703                }
704            }
705        } else if idx == app.cfn_parameter_column_ids.len() + 3 {
706            app.cfn_state.parameters.page_size = PageSize::Ten;
707        } else if idx == app.cfn_parameter_column_ids.len() + 4 {
708            app.cfn_state.parameters.page_size = PageSize::TwentyFive;
709        } else if idx == app.cfn_parameter_column_ids.len() + 5 {
710            app.cfn_state.parameters.page_size = PageSize::Fifty;
711        } else if idx == app.cfn_parameter_column_ids.len() + 6 {
712            app.cfn_state.parameters.page_size = PageSize::OneHundred;
713        }
714    } else if app.cfn_state.current_stack.is_some()
715        && app.cfn_state.detail_tab == CfnDetailTab::Outputs
716    {
717        if idx > 0 && idx <= app.cfn_output_column_ids.len() {
718            if let Some(col) = app.cfn_output_column_ids.get(idx - 1) {
719                if let Some(pos) = app
720                    .cfn_output_visible_column_ids
721                    .iter()
722                    .position(|c| c == col)
723                {
724                    if app.cfn_output_visible_column_ids.len() > 1 {
725                        app.cfn_output_visible_column_ids.remove(pos);
726                    }
727                } else {
728                    app.cfn_output_visible_column_ids.push(col);
729                }
730            }
731        } else if idx == app.cfn_output_column_ids.len() + 3 {
732            app.cfn_state.outputs.page_size = PageSize::Ten;
733        } else if idx == app.cfn_output_column_ids.len() + 4 {
734            app.cfn_state.outputs.page_size = PageSize::TwentyFive;
735        } else if idx == app.cfn_output_column_ids.len() + 5 {
736            app.cfn_state.outputs.page_size = PageSize::Fifty;
737        } else if idx == app.cfn_output_column_ids.len() + 6 {
738            app.cfn_state.outputs.page_size = PageSize::OneHundred;
739        }
740    } else if app.cfn_state.current_stack.is_some()
741        && app.cfn_state.detail_tab == CfnDetailTab::Resources
742    {
743        if idx > 0 && idx <= app.cfn_resource_column_ids.len() {
744            if let Some(col) = app.cfn_resource_column_ids.get(idx - 1) {
745                if let Some(pos) = app
746                    .cfn_resource_visible_column_ids
747                    .iter()
748                    .position(|c| c == col)
749                {
750                    if app.cfn_resource_visible_column_ids.len() > 1 {
751                        app.cfn_resource_visible_column_ids.remove(pos);
752                    }
753                } else {
754                    app.cfn_resource_visible_column_ids.push(col);
755                }
756            }
757        } else if idx == app.cfn_resource_column_ids.len() + 3 {
758            app.cfn_state.resources.page_size = PageSize::Ten;
759        } else if idx == app.cfn_resource_column_ids.len() + 4 {
760            app.cfn_state.resources.page_size = PageSize::TwentyFive;
761        } else if idx == app.cfn_resource_column_ids.len() + 5 {
762            app.cfn_state.resources.page_size = PageSize::Fifty;
763        } else if idx == app.cfn_resource_column_ids.len() + 6 {
764            app.cfn_state.resources.page_size = PageSize::OneHundred;
765        }
766    } else if app.cfn_state.current_stack.is_some()
767        && app.cfn_state.detail_tab == CfnDetailTab::Events
768    {
769        if idx > 0 && idx <= app.cfn_event_column_ids.len() {
770            if let Some(col) = app.cfn_event_column_ids.get(idx - 1) {
771                if let Some(pos) = app
772                    .cfn_event_visible_column_ids
773                    .iter()
774                    .position(|c| c == col)
775                {
776                    if app.cfn_event_visible_column_ids.len() > 1 {
777                        app.cfn_event_visible_column_ids.remove(pos);
778                    }
779                } else {
780                    app.cfn_event_visible_column_ids.push(col);
781                }
782            }
783        } else if idx == app.cfn_event_column_ids.len() + 3 {
784            app.cfn_state.events.page_size = PageSize::Ten;
785        } else if idx == app.cfn_event_column_ids.len() + 4 {
786            app.cfn_state.events.page_size = PageSize::TwentyFive;
787        } else if idx == app.cfn_event_column_ids.len() + 5 {
788            app.cfn_state.events.page_size = PageSize::Fifty;
789        } else if idx == app.cfn_event_column_ids.len() + 6 {
790            app.cfn_state.events.page_size = PageSize::OneHundred;
791        }
792    } else if app.cfn_state.current_stack.is_some()
793        && app.cfn_state.detail_tab == CfnDetailTab::ChangeSets
794    {
795        if idx > 0 && idx <= app.cfn_change_set_column_ids.len() {
796            if let Some(col) = app.cfn_change_set_column_ids.get(idx - 1) {
797                if let Some(pos) = app
798                    .cfn_change_set_visible_column_ids
799                    .iter()
800                    .position(|c| c == col)
801                {
802                    if app.cfn_change_set_visible_column_ids.len() > 1 {
803                        app.cfn_change_set_visible_column_ids.remove(pos);
804                    }
805                } else {
806                    app.cfn_change_set_visible_column_ids.push(col);
807                }
808            }
809        } else if idx == app.cfn_change_set_column_ids.len() + 3 {
810            app.cfn_state.change_sets.page_size = PageSize::Ten;
811        } else if idx == app.cfn_change_set_column_ids.len() + 4 {
812            app.cfn_state.change_sets.page_size = PageSize::TwentyFive;
813        } else if idx == app.cfn_change_set_column_ids.len() + 5 {
814            app.cfn_state.change_sets.page_size = PageSize::Fifty;
815        } else if idx == app.cfn_change_set_column_ids.len() + 6 {
816            app.cfn_state.change_sets.page_size = PageSize::OneHundred;
817        }
818    } else if app.cfn_state.current_stack.is_none() {
819        if idx > 0 && idx <= app.cfn_column_ids.len() {
820            if let Some(col) = app.cfn_column_ids.get(idx - 1) {
821                if let Some(pos) = app.cfn_visible_column_ids.iter().position(|c| c == col) {
822                    if app.cfn_visible_column_ids.len() > 1 {
823                        app.cfn_visible_column_ids.remove(pos);
824                    }
825                } else {
826                    app.cfn_visible_column_ids.push(*col);
827                }
828            }
829        } else if idx == app.cfn_column_ids.len() + 3 {
830            app.cfn_state.table.page_size = PageSize::Ten;
831        } else if idx == app.cfn_column_ids.len() + 4 {
832            app.cfn_state.table.page_size = PageSize::TwentyFive;
833        } else if idx == app.cfn_column_ids.len() + 5 {
834            app.cfn_state.table.page_size = PageSize::Fifty;
835        } else if idx == app.cfn_column_ids.len() + 6 {
836            app.cfn_state.table.page_size = PageSize::OneHundred;
837        }
838    }
839}
840
841pub fn next_preferences(app: &mut App) {
842    let cols = column_count(app);
843    let page_size_idx = cols + 2;
844    if app.column_selector_index < page_size_idx {
845        app.column_selector_index = page_size_idx;
846    } else {
847        app.column_selector_index = 0;
848    }
849}
850
851pub fn prev_preferences(app: &mut App) {
852    let cols = column_count(app);
853    let page_size_idx = cols + 2;
854    if app.column_selector_index >= page_size_idx {
855        app.column_selector_index = 0;
856    } else {
857        app.column_selector_index = page_size_idx;
858    }
859}
860
861pub fn column_selector_max(app: &App) -> usize {
862    column_count(app) + 6
863}
864
865pub fn column_count(app: &App) -> usize {
866    if app.cfn_state.current_stack.is_some() {
867        match app.cfn_state.detail_tab {
868            CfnDetailTab::Parameters => app.cfn_parameter_column_ids.len(),
869            CfnDetailTab::Outputs => app.cfn_output_column_ids.len(),
870            CfnDetailTab::Resources => app.cfn_resource_column_ids.len(),
871            CfnDetailTab::Events => app.cfn_event_column_ids.len(),
872            CfnDetailTab::ChangeSets => app.cfn_change_set_column_ids.len(),
873            _ => 0,
874        }
875    } else {
876        app.cfn_column_ids.len()
877    }
878}
879
880// ── Breadcrumb / console URL ──────────────────────────────────────────────────
881
882pub fn breadcrumb(app: &App) -> Vec<String> {
883    let mut parts = vec!["CloudFormation".to_string()];
884    if let Some(stack_name) = &app.cfn_state.current_stack {
885        parts.push(stack_name.clone());
886    } else {
887        parts.push("Stacks".to_string());
888    }
889    parts
890}
891
892pub fn console_url(app: &App) -> String {
893    use crate::cfn;
894    if let Some(stack_name) = &app.cfn_state.current_stack {
895        if let Some(stack) = app
896            .cfn_state
897            .table
898            .items
899            .iter()
900            .find(|s| &s.name == stack_name)
901        {
902            return cfn::console_url_stack_detail_with_tab(
903                &app.config.region,
904                &stack.stack_id,
905                &app.cfn_state.detail_tab,
906            );
907        }
908    }
909    cfn::console_url_stacks(&app.config.region)
910}