Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file tui/app.c
6 : * @brief TUI state machine implementation (US-11 v2).
7 : */
8 :
9 : #include "tui/app.h"
10 :
11 : #include <string.h>
12 :
13 22 : int tui_app_init(TuiApp *app, int rows, int cols) {
14 22 : if (!app) return -1;
15 22 : memset(app, 0, sizeof(*app));
16 22 : if (screen_init(&app->screen, rows, cols) != 0) return -1;
17 20 : layout_compute(&app->layout, rows, cols, 28);
18 20 : dialog_pane_init(&app->dialogs);
19 20 : history_pane_init(&app->history);
20 20 : status_row_init(&app->status);
21 20 : app->focus = TUI_FOCUS_DIALOGS;
22 20 : app->rows = rows;
23 20 : app->cols = cols;
24 : /* Wire viewport heights so list_views know how much they render. */
25 20 : app->dialogs.lv.rows_visible = app->layout.dialogs.rows;
26 20 : app->history.lv.rows_visible = app->layout.history.rows;
27 20 : return 0;
28 : }
29 :
30 20 : void tui_app_free(TuiApp *app) {
31 20 : if (!app) return;
32 20 : screen_free(&app->screen);
33 : }
34 :
35 1 : int tui_app_resize(TuiApp *app, int rows, int cols) {
36 1 : if (!app) return -1;
37 1 : screen_free(&app->screen);
38 1 : if (screen_init(&app->screen, rows, cols) != 0) return -1;
39 1 : layout_compute(&app->layout, rows, cols, 28);
40 1 : list_view_set_viewport(&app->dialogs.lv, app->layout.dialogs.rows);
41 1 : list_view_set_viewport(&app->history.lv, app->layout.history.rows);
42 1 : app->rows = rows;
43 1 : app->cols = cols;
44 1 : return 0;
45 : }
46 :
47 9 : static ListView *focused_list_view(TuiApp *app) {
48 9 : return (app->focus == TUI_FOCUS_DIALOGS)
49 : ? &app->dialogs.lv
50 9 : : &app->history.lv;
51 : }
52 :
53 21 : TuiEvent tui_app_handle_key(TuiApp *app, TermKey key) {
54 21 : if (!app) return TUI_EVENT_NONE;
55 :
56 21 : switch (key) {
57 2 : case TERM_KEY_QUIT:
58 : case TERM_KEY_ESC:
59 2 : return TUI_EVENT_QUIT;
60 :
61 2 : case TERM_KEY_LEFT:
62 2 : if (app->focus != TUI_FOCUS_DIALOGS) {
63 2 : app->focus = TUI_FOCUS_DIALOGS;
64 2 : app->status.mode = STATUS_MODE_DIALOGS;
65 2 : return TUI_EVENT_REDRAW;
66 : }
67 0 : return TUI_EVENT_NONE;
68 :
69 4 : case TERM_KEY_RIGHT:
70 4 : if (app->focus != TUI_FOCUS_HISTORY) {
71 3 : app->focus = TUI_FOCUS_HISTORY;
72 3 : app->status.mode = STATUS_MODE_HISTORY;
73 3 : return TUI_EVENT_REDRAW;
74 : }
75 1 : return TUI_EVENT_NONE;
76 :
77 2 : case TERM_KEY_PREV_LINE:
78 2 : list_view_move_up(focused_list_view(app));
79 2 : return TUI_EVENT_REDRAW;
80 :
81 5 : case TERM_KEY_NEXT_LINE:
82 5 : list_view_move_down(focused_list_view(app));
83 5 : return TUI_EVENT_REDRAW;
84 :
85 0 : case TERM_KEY_PREV_PAGE:
86 0 : list_view_page_up(focused_list_view(app));
87 0 : return TUI_EVENT_REDRAW;
88 :
89 0 : case TERM_KEY_NEXT_PAGE:
90 0 : list_view_page_down(focused_list_view(app));
91 0 : return TUI_EVENT_REDRAW;
92 :
93 1 : case TERM_KEY_HOME:
94 1 : list_view_home(focused_list_view(app));
95 1 : return TUI_EVENT_REDRAW;
96 :
97 1 : case TERM_KEY_END:
98 1 : list_view_end(focused_list_view(app));
99 1 : return TUI_EVENT_REDRAW;
100 :
101 4 : case TERM_KEY_ENTER:
102 4 : if (app->focus == TUI_FOCUS_DIALOGS
103 4 : && dialog_pane_selected(&app->dialogs) != NULL) {
104 : /* Caller loads history for the selection and then flips focus
105 : * to the history pane. We switch focus here pre-emptively so
106 : * the status-row hint updates even before the network call. */
107 3 : app->focus = TUI_FOCUS_HISTORY;
108 3 : app->status.mode = STATUS_MODE_HISTORY;
109 3 : return TUI_EVENT_OPEN_DIALOG;
110 : }
111 1 : return TUI_EVENT_NONE;
112 :
113 0 : default:
114 0 : return TUI_EVENT_NONE;
115 : }
116 : }
117 :
118 10 : TuiEvent tui_app_handle_char(TuiApp *app, int ch) {
119 10 : if (!app) return TUI_EVENT_NONE;
120 10 : switch (ch) {
121 4 : case 'q': case 'Q':
122 4 : return TUI_EVENT_QUIT;
123 3 : case 'j':
124 3 : return tui_app_handle_key(app, TERM_KEY_NEXT_LINE);
125 1 : case 'k':
126 1 : return tui_app_handle_key(app, TERM_KEY_PREV_LINE);
127 1 : case 'h':
128 1 : return tui_app_handle_key(app, TERM_KEY_LEFT);
129 1 : case 'l':
130 1 : return tui_app_handle_key(app, TERM_KEY_RIGHT);
131 0 : case 'g':
132 0 : return tui_app_handle_key(app, TERM_KEY_HOME);
133 0 : case 'G':
134 0 : return tui_app_handle_key(app, TERM_KEY_END);
135 0 : default:
136 0 : return TUI_EVENT_NONE;
137 : }
138 : }
139 :
140 8 : void tui_app_paint(TuiApp *app) {
141 8 : if (!app) return;
142 8 : screen_clear_back(&app->screen);
143 8 : if (pane_is_valid(&app->layout.dialogs)) {
144 8 : dialog_pane_render(&app->dialogs, &app->layout.dialogs,
145 : &app->screen,
146 8 : app->focus == TUI_FOCUS_DIALOGS);
147 : }
148 8 : if (pane_is_valid(&app->layout.history)) {
149 8 : history_pane_render(&app->history, &app->layout.history,
150 : &app->screen);
151 : }
152 8 : if (pane_is_valid(&app->layout.status)) {
153 8 : status_row_render(&app->status, &app->layout.status,
154 : &app->screen);
155 : }
156 : }
|