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 26 : int tui_app_init(TuiApp *app, int rows, int cols) {
14 26 : if (!app) return -1;
15 26 : memset(app, 0, sizeof(*app));
16 26 : if (screen_init(&app->screen, rows, cols) != 0) return -1;
17 24 : layout_compute(&app->layout, rows, cols, 28);
18 24 : dialog_pane_init(&app->dialogs);
19 24 : history_pane_init(&app->history);
20 24 : status_row_init(&app->status);
21 24 : app->focus = TUI_FOCUS_DIALOGS;
22 24 : app->rows = rows;
23 24 : app->cols = cols;
24 : /* Wire viewport heights so list_views know how much they render. */
25 24 : app->dialogs.lv.rows_visible = app->layout.dialogs.rows;
26 24 : app->history.lv.rows_visible = app->layout.history.rows;
27 24 : return 0;
28 : }
29 :
30 24 : void tui_app_free(TuiApp *app) {
31 24 : if (!app) return;
32 24 : screen_free(&app->screen);
33 : }
34 :
35 3 : int tui_app_resize(TuiApp *app, int rows, int cols) {
36 3 : if (!app) return -1;
37 3 : screen_free(&app->screen);
38 3 : if (screen_init(&app->screen, rows, cols) != 0) return -1;
39 3 : layout_compute(&app->layout, rows, cols, 28);
40 3 : list_view_set_viewport(&app->dialogs.lv, app->layout.dialogs.rows);
41 3 : list_view_set_viewport(&app->history.lv, app->layout.history.rows);
42 3 : app->rows = rows;
43 3 : app->cols = cols;
44 3 : 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 22 : TuiEvent tui_app_handle_key(TuiApp *app, TermKey key) {
54 22 : if (!app) return TUI_EVENT_NONE;
55 :
56 22 : switch (key) {
57 3 : case TERM_KEY_QUIT:
58 : case TERM_KEY_ESC:
59 3 : 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 13 : TuiEvent tui_app_handle_char(TuiApp *app, int ch) {
119 13 : if (!app) return TUI_EVENT_NONE;
120 13 : switch (ch) {
121 7 : case 'q': case 'Q':
122 7 : 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 14 : void tui_app_paint(TuiApp *app) {
141 14 : if (!app) return;
142 14 : screen_clear_back(&app->screen);
143 14 : if (pane_is_valid(&app->layout.dialogs)) {
144 14 : dialog_pane_render(&app->dialogs, &app->layout.dialogs,
145 : &app->screen,
146 14 : app->focus == TUI_FOCUS_DIALOGS);
147 : }
148 14 : if (pane_is_valid(&app->layout.history)) {
149 14 : history_pane_render(&app->history, &app->layout.history,
150 : &app->screen);
151 : }
152 14 : if (pane_is_valid(&app->layout.status)) {
153 14 : status_row_render(&app->status, &app->layout.status,
154 : &app->screen);
155 : }
156 : }
|