View Javadoc
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 }