1 package com.guinetik.hexafun.examples.tui;
2
3 import static com.guinetik.hexafun.examples.tui.Ansi.*;
4
5 import java.util.List;
6 import java.util.function.Function;
7
8 /**
9 * Reusable TUI widget builders.
10 *
11 * <p>All methods return Strings (pure functions), keeping side effects
12 * at the edges. Compose these with {@link View} for full screens.
13 *
14 * <p>Example:
15 * <pre class="language-java">{@code
16 * String header = Widgets.header("My App", 80, CYAN);
17 * String progress = Widgets.progressBar(75, 60, GREEN);
18 * String box = Widgets.box("Content here", 40, YELLOW);
19 * }</pre>
20 */
21 public final class Widgets {
22
23 private Widgets() {}
24
25 // ═══════════════════════════════════════════════════════════════════
26 // HEADERS & TITLES
27 // ═══════════════════════════════════════════════════════════════════
28
29 /**
30 * Render a boxed header with title and optional subtitle.
31 *
32 * @param title Main title text
33 * @param subtitle Optional subtitle (can be null)
34 * @param width Total width including borders
35 * @param borderColor ANSI color for the border
36 * @return Rendered header string
37 */
38 public static String header(
39 String title,
40 String subtitle,
41 int width,
42 String borderColor
43 ) {
44 int innerWidth = width - 4;
45 StringBuilder sb = new StringBuilder();
46
47 sb.append("\n");
48 sb
49 .append(
50 color(
51 " " +
52 DBOX_TOP_LEFT +
53 repeat(DBOX_HORIZONTAL, innerWidth) +
54 DBOX_TOP_RIGHT,
55 borderColor
56 )
57 )
58 .append("\n");
59 sb
60 .append(color(" " + DBOX_VERTICAL, borderColor))
61 .append(color(center(title, innerWidth), BOLD, BRIGHT_WHITE))
62 .append(color(DBOX_VERTICAL, borderColor))
63 .append("\n");
64
65 if (subtitle != null && !subtitle.isEmpty()) {
66 sb
67 .append(color(" " + DBOX_VERTICAL, borderColor))
68 .append(color(center(subtitle, innerWidth), DIM))
69 .append(color(DBOX_VERTICAL, borderColor))
70 .append("\n");
71 }
72
73 sb
74 .append(
75 color(
76 " " +
77 DBOX_BOTTOM_LEFT +
78 repeat(DBOX_HORIZONTAL, innerWidth) +
79 DBOX_BOTTOM_RIGHT,
80 borderColor
81 )
82 )
83 .append("\n");
84 sb.append("\n");
85
86 return sb.toString();
87 }
88
89 /**
90 * Simple header without subtitle.
91 */
92 public static String header(String title, int width, String borderColor) {
93 return header(title, null, width, borderColor);
94 }
95
96 /**
97 * Section divider with label.
98 *
99 * @param label Section label
100 * @param width Total width
101 * @return Rendered divider
102 */
103 public static String section(String label, int width) {
104 int labelWidth = label.length() + 2;
105 int lineWidth = width - 4 - labelWidth;
106 return (
107 color(
108 " " +
109 BOX_HORIZONTAL +
110 " " +
111 label +
112 " " +
113 repeat(BOX_HORIZONTAL, lineWidth),
114 DIM
115 ) +
116 "\n\n"
117 );
118 }
119
120 // ═══════════════════════════════════════════════════════════════════
121 // PROGRESS & STATS
122 // ═══════════════════════════════════════════════════════════════════
123
124 /**
125 * Render a progress bar.
126 *
127 * @param percent Completion percentage (0-100)
128 * @param width Bar width in characters
129 * @param fillColor Color for filled portion
130 * @return Rendered progress bar
131 */
132 public static String progressBar(int percent, int width, String fillColor) {
133 int clamped = Math.max(0, Math.min(100, percent));
134 int filled = (width * clamped) / 100;
135 int empty = width - filled;
136
137 return (
138 " " +
139 color(repeat(BLOCK_FULL, filled), fillColor) +
140 color(repeat(BLOCK_LIGHT, empty), BRIGHT_BLACK) +
141 color(String.format(" %3d%%", clamped), DIM) +
142 "\n"
143 );
144 }
145
146 /**
147 * Powerline-style segment for stats bars.
148 *
149 * @param text Segment text
150 * @param bgColor Background color
151 * @param fgColor Foreground color
152 * @return Rendered segment (without separator)
153 */
154 public static String segment(
155 String text,
156 int width,
157 String bgColor,
158 String fgColor
159 ) {
160 return color(center(text, width), bgColor, fgColor, BOLD);
161 }
162
163 /**
164 * Powerline separator arrow.
165 *
166 * @param fromColor Color of previous segment
167 * @param toColor Color of next segment (or null for end)
168 * @return Rendered separator
169 */
170 public static String separator(String fromColor, String toColor) {
171 if (toColor == null) {
172 return color(PL_LEFT, fromColor);
173 }
174 return color(
175 PL_LEFT,
176 fromColor,
177 toColor.replace("\u001B[3", "\u001B[4")
178 ); // Convert fg to bg
179 }
180
181 // ═══════════════════════════════════════════════════════════════════
182 // BOXES & CONTAINERS
183 // ═══════════════════════════════════════════════════════════════════
184
185 /**
186 * Render content in a single-line box.
187 *
188 * @param content Content to box
189 * @param width Box width
190 * @param borderColor Border color
191 * @return Boxed content
192 */
193 public static String box(String content, int width, String borderColor) {
194 int innerWidth = width - 2;
195 StringBuilder sb = new StringBuilder();
196
197 sb
198 .append(
199 color(
200 BOX_TOP_LEFT +
201 repeat(BOX_HORIZONTAL, innerWidth) +
202 BOX_TOP_RIGHT,
203 borderColor
204 )
205 )
206 .append("\n");
207 sb
208 .append(color(BOX_VERTICAL, borderColor))
209 .append(center(content, innerWidth))
210 .append(color(BOX_VERTICAL, borderColor))
211 .append("\n");
212 sb
213 .append(
214 color(
215 BOX_BOTTOM_LEFT +
216 repeat(BOX_HORIZONTAL, innerWidth) +
217 BOX_BOTTOM_RIGHT,
218 borderColor
219 )
220 )
221 .append("\n");
222
223 return sb.toString();
224 }
225
226 /**
227 * Render multiple lines in a box.
228 *
229 * @param lines Content lines
230 * @param width Box width
231 * @param borderColor Border color
232 * @return Boxed content
233 */
234 public static String box(
235 List<String> lines,
236 int width,
237 String borderColor
238 ) {
239 int innerWidth = width - 2;
240 StringBuilder sb = new StringBuilder();
241
242 sb
243 .append(
244 color(
245 BOX_TOP_LEFT +
246 repeat(BOX_HORIZONTAL, innerWidth) +
247 BOX_TOP_RIGHT,
248 borderColor
249 )
250 )
251 .append("\n");
252 for (String line : lines) {
253 String padded = line.length() > innerWidth
254 ? line.substring(0, innerWidth)
255 : pad(line, innerWidth);
256 sb
257 .append(color(BOX_VERTICAL, borderColor))
258 .append(padded)
259 .append(color(BOX_VERTICAL, borderColor))
260 .append("\n");
261 }
262 sb
263 .append(
264 color(
265 BOX_BOTTOM_LEFT +
266 repeat(BOX_HORIZONTAL, innerWidth) +
267 BOX_BOTTOM_RIGHT,
268 borderColor
269 )
270 )
271 .append("\n");
272
273 return sb.toString();
274 }
275
276 /**
277 * Horizontal rule/divider.
278 *
279 * @param width Total width
280 * @return Rendered divider
281 */
282 public static String hr(int width) {
283 return color(" " + repeat(BOX_HORIZONTAL, width - 4), DIM) + "\n";
284 }
285
286 // ═══════════════════════════════════════════════════════════════════
287 // LISTS & ITEMS
288 // ═══════════════════════════════════════════════════════════════════
289
290 /**
291 * Render a numbered list item.
292 *
293 * @param index Item number
294 * @param icon Icon to display
295 * @param iconColor Icon color
296 * @param text Item text
297 * @param textColor Text color
298 * @return Rendered list item
299 */
300 public static String listItem(
301 int index,
302 String icon,
303 String iconColor,
304 String text,
305 String textColor
306 ) {
307 return (
308 " " +
309 color(index + " ", BRIGHT_BLACK) +
310 color(icon + " ", iconColor) +
311 color(text, textColor) +
312 "\n"
313 );
314 }
315
316 /**
317 * Render a simple bullet item.
318 */
319 public static String bulletItem(String text) {
320 return " " + color(BULLET + " ", DIM) + text + "\n";
321 }
322
323 /**
324 * Render a numbered selection item (for menus).
325 */
326 public static String selectionItem(int index, String text) {
327 return color(" [" + index + "] ", BRIGHT_BLACK) + text + "\n";
328 }
329
330 // ═══════════════════════════════════════════════════════════════════
331 // MENUS & PROMPTS
332 // ═══════════════════════════════════════════════════════════════════
333
334 /**
335 * Menu item with key shortcut.
336 *
337 * @param key Shortcut key
338 * @param icon Icon
339 * @param label Menu label
340 * @param keyColor Color for the key
341 * @return Rendered menu item
342 */
343 public static String menuItem(
344 String key,
345 String icon,
346 String label,
347 String keyColor
348 ) {
349 return (
350 color("[" + key + "]", keyColor, BOLD) +
351 color(" " + icon + " " + label + " ", keyColor)
352 );
353 }
354
355 /**
356 * Input prompt.
357 *
358 * @param symbol Prompt symbol
359 * @param promptColor Prompt color
360 * @return Rendered prompt
361 */
362 public static String prompt(String symbol, String promptColor) {
363 return color(" " + symbol + " ", promptColor, BOLD);
364 }
365
366 /**
367 * Default prompt with ">".
368 */
369 public static String prompt(String promptColor) {
370 return prompt(">", promptColor);
371 }
372
373 // ═══════════════════════════════════════════════════════════════════
374 // STATUS & MESSAGES
375 // ═══════════════════════════════════════════════════════════════════
376
377 /**
378 * Status message with arrow indicator.
379 *
380 * @param message Status message
381 * @param messageColor Message color
382 * @return Rendered status
383 */
384 public static String status(String message, String messageColor) {
385 if (message == null || message.isEmpty()) return "\n";
386 return color(" " + ARROW_RIGHT + " " + message, messageColor) + "\n\n";
387 }
388
389 /**
390 * Success message.
391 */
392 public static String success(String message) {
393 return status(CHECK + " " + message, GREEN);
394 }
395
396 /**
397 * Error message.
398 */
399 public static String error(String message) {
400 return status(CROSS + " " + message, RED);
401 }
402
403 /**
404 * Warning message.
405 */
406 public static String warning(String message) {
407 return status(BULLET + " " + message, YELLOW);
408 }
409
410 /**
411 * Info message.
412 */
413 public static String info(String message) {
414 return status(ICON_INFO + " " + message, BLUE);
415 }
416 }