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 3 : int tui_app_init(TuiApp *app, int rows, int cols) {
14 3 : if (!app) return -1;
15 3 : memset(app, 0, sizeof(*app));
16 3 : if (screen_init(&app->screen, rows, cols) != 0) return -1;
17 3 : layout_compute(&app->layout, rows, cols, 28);
18 3 : dialog_pane_init(&app->dialogs);
19 3 : history_pane_init(&app->history);
20 3 : status_row_init(&app->status);
21 3 : app->focus = TUI_FOCUS_DIALOGS;
22 3 : app->rows = rows;
23 3 : app->cols = cols;
24 : /* Wire viewport heights so list_views know how much they render. */
25 3 : app->dialogs.lv.rows_visible = app->layout.dialogs.rows;
26 3 : app->history.lv.rows_visible = app->layout.history.rows;
27 3 : return 0;
28 : }
29 :
30 3 : void tui_app_free(TuiApp *app) {
31 3 : if (!app) return;
32 3 : screen_free(&app->screen);
33 : }
34 :
35 0 : int tui_app_resize(TuiApp *app, int rows, int cols) {
36 0 : if (!app) return -1;
37 0 : screen_free(&app->screen);
38 0 : if (screen_init(&app->screen, rows, cols) != 0) return -1;
39 0 : layout_compute(&app->layout, rows, cols, 28);
40 0 : list_view_set_viewport(&app->dialogs.lv, app->layout.dialogs.rows);
41 0 : list_view_set_viewport(&app->history.lv, app->layout.history.rows);
42 0 : app->rows = rows;
43 0 : app->cols = cols;
44 0 : return 0;
45 : }
46 :
47 1 : static ListView *focused_list_view(TuiApp *app) {
48 1 : return (app->focus == TUI_FOCUS_DIALOGS)
49 : ? &app->dialogs.lv
50 1 : : &app->history.lv;
51 : }
52 :
53 2 : TuiEvent tui_app_handle_key(TuiApp *app, TermKey key) {
54 2 : if (!app) return TUI_EVENT_NONE;
55 :
56 2 : switch (key) {
57 0 : case TERM_KEY_QUIT:
58 : case TERM_KEY_ESC:
59 0 : return TUI_EVENT_QUIT;
60 :
61 0 : case TERM_KEY_LEFT:
62 0 : if (app->focus != TUI_FOCUS_DIALOGS) {
63 0 : app->focus = TUI_FOCUS_DIALOGS;
64 0 : app->status.mode = STATUS_MODE_DIALOGS;
65 0 : return TUI_EVENT_REDRAW;
66 : }
67 0 : return TUI_EVENT_NONE;
68 :
69 0 : case TERM_KEY_RIGHT:
70 0 : if (app->focus != TUI_FOCUS_HISTORY) {
71 0 : app->focus = TUI_FOCUS_HISTORY;
72 0 : app->status.mode = STATUS_MODE_HISTORY;
73 0 : return TUI_EVENT_REDRAW;
74 : }
75 0 : return TUI_EVENT_NONE;
76 :
77 0 : case TERM_KEY_PREV_LINE:
78 0 : list_view_move_up(focused_list_view(app));
79 0 : return TUI_EVENT_REDRAW;
80 :
81 1 : case TERM_KEY_NEXT_LINE:
82 1 : list_view_move_down(focused_list_view(app));
83 1 : 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 0 : case TERM_KEY_HOME:
94 0 : list_view_home(focused_list_view(app));
95 0 : return TUI_EVENT_REDRAW;
96 :
97 0 : case TERM_KEY_END:
98 0 : list_view_end(focused_list_view(app));
99 0 : return TUI_EVENT_REDRAW;
100 :
101 1 : case TERM_KEY_ENTER:
102 1 : if (app->focus == TUI_FOCUS_DIALOGS
103 1 : && 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 1 : app->focus = TUI_FOCUS_HISTORY;
108 1 : app->status.mode = STATUS_MODE_HISTORY;
109 1 : return TUI_EVENT_OPEN_DIALOG;
110 : }
111 0 : return TUI_EVENT_NONE;
112 :
113 0 : default:
114 0 : return TUI_EVENT_NONE;
115 : }
116 : }
117 :
118 2 : TuiEvent tui_app_handle_char(TuiApp *app, int ch) {
119 2 : if (!app) return TUI_EVENT_NONE;
120 2 : switch (ch) {
121 1 : case 'q': case 'Q':
122 1 : return TUI_EVENT_QUIT;
123 1 : case 'j':
124 1 : return tui_app_handle_key(app, TERM_KEY_NEXT_LINE);
125 0 : case 'k':
126 0 : return tui_app_handle_key(app, TERM_KEY_PREV_LINE);
127 0 : case 'h':
128 0 : return tui_app_handle_key(app, TERM_KEY_LEFT);
129 0 : case 'l':
130 0 : 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 3 : void tui_app_paint(TuiApp *app) {
141 3 : if (!app) return;
142 3 : screen_clear_back(&app->screen);
143 3 : if (pane_is_valid(&app->layout.dialogs)) {
144 3 : dialog_pane_render(&app->dialogs, &app->layout.dialogs,
145 : &app->screen,
146 3 : app->focus == TUI_FOCUS_DIALOGS);
147 : }
148 3 : if (pane_is_valid(&app->layout.history)) {
149 3 : history_pane_render(&app->history, &app->layout.history,
150 : &app->screen);
151 : }
152 3 : if (pane_is_valid(&app->layout.status)) {
153 3 : status_row_render(&app->status, &app->layout.status,
154 : &app->screen);
155 : }
156 : }
|