View Javadoc
1   package com.guinetik.hexafun.examples.sysmon;
2   
3   import static com.guinetik.hexafun.examples.sysmon.SysmonKeys.*;
4   import static com.guinetik.hexafun.examples.tui.Ansi.*;
5   
6   import com.guinetik.hexafun.examples.tui.View;
7   
8   /**
9    * Responsive view components for the System Monitor TUI.
10   *
11   * <p>All views use {@code state.width()} to render at the current terminal width.
12   * Width is recalculated on each refresh.</p>
13   */
14  public final class SysmonView {
15  
16      private SysmonView() {}
17  
18      private static final int PADDING = 4; // Left/right padding
19  
20      /**
21       * Compose the full screen view.
22       */
23      public static View<SysmonState> screen() {
24          return clear()
25              .andThen(header())
26              .andThen(metrics())
27              .andThen(menu())
28              .andThen(status())
29              .andThen(prompt());
30      }
31  
32      /**
33       * Clear screen and reset cursor.
34       */
35      public static View<SysmonState> clear() {
36          return state -> CLEAR + CURSOR_HOME;
37      }
38  
39      /**
40       * Render the header box - responsive to terminal width.
41       */
42      public static View<SysmonState> header() {
43          return state -> {
44              int boxWidth = state.width() - PADDING;
45              int innerWidth = boxWidth - 2;
46  
47              return lines(
48                  "",
49                  color("  " + DBOX_TOP_LEFT + repeat(DBOX_HORIZONTAL, innerWidth) + DBOX_TOP_RIGHT, CYAN),
50                  color("  " + DBOX_VERTICAL, CYAN) +
51                      color(center("SYSTEM MONITOR", innerWidth), BOLD, BRIGHT_WHITE) +
52                      color(DBOX_VERTICAL, CYAN),
53                  color("  " + DBOX_VERTICAL, CYAN) +
54                      color(center("HexaFun Adapter Demo", innerWidth), DIM) +
55                      color(DBOX_VERTICAL, CYAN),
56                  color("  " + DBOX_BOTTOM_LEFT + repeat(DBOX_HORIZONTAL, innerWidth) + DBOX_BOTTOM_RIGHT, CYAN),
57                  ""
58              );
59          };
60      }
61  
62      /**
63       * Render the metrics output based on selected format - responsive.
64       */
65      public static View<SysmonState> metrics() {
66          return state -> {
67              SystemMetrics m = state.metrics();
68              int boxWidth = state.width() - PADDING;
69  
70              String output = switch (state.format()) {
71                  case TUI -> renderTuiGauges(m, boxWidth);
72                  case CLI -> formatBox("CLI Output", state.app().adapt(TO_CLI, m), boxWidth, GREEN);
73                  case JSON -> formatBox("JSON Output", state.app().adapt(TO_JSON, m), boxWidth, YELLOW);
74                  case PROMETHEUS -> formatBox("Prometheus Output", state.app().adapt(TO_PROMETHEUS, m), boxWidth, MAGENTA);
75              };
76              return output + "\n";
77          };
78      }
79  
80      /**
81       * Render TUI gauges - responsive bar width.
82       */
83      private static String renderTuiGauges(SystemMetrics metrics, int boxWidth) {
84          StringBuilder sb = new StringBuilder();
85          int innerWidth = boxWidth - 2;
86          // Gauge content: space(1) + label(4) + space(1) + bracket(1) + bar + bracket(1) + percent(5) + warn(2) + space(1) = 16 fixed
87          int barWidth = innerWidth - 16;
88  
89          String title = "─ System Monitor ";  // 17 chars
90  
91          // Header
92          sb.append(color("  " + BOX_TOP_LEFT + title +
93              repeat(BOX_HORIZONTAL, innerWidth - title.length()) + BOX_TOP_RIGHT, CYAN)).append("\n");
94  
95          // CPU gauge
96          sb.append(color("  " + BOX_VERTICAL, CYAN))
97            .append(gauge("CPU ", metrics.cpu(), metrics.cpuWarning(), barWidth))
98            .append(color(BOX_VERTICAL, CYAN)).append("\n");
99  
100         // Memory gauge
101         sb.append(color("  " + BOX_VERTICAL, CYAN))
102           .append(gauge("MEM ", metrics.memory(), metrics.memoryWarning(), barWidth))
103           .append(color(BOX_VERTICAL, CYAN)).append("\n");
104 
105         // Disk gauge
106         sb.append(color("  " + BOX_VERTICAL, CYAN))
107           .append(gauge("DISK", metrics.disk(), metrics.diskWarning(), barWidth))
108           .append(color(BOX_VERTICAL, CYAN)).append("\n");
109 
110         // Footer
111         sb.append(color("  " + BOX_BOTTOM_LEFT + repeat(BOX_HORIZONTAL, innerWidth) +
112             BOX_BOTTOM_RIGHT, CYAN)).append("\n");
113 
114         return sb.toString();
115     }
116 
117     /**
118      * Build a single gauge line with color-coded bar.
119      */
120     private static String gauge(String label, double percent, boolean warning, int barWidth) {
121         int filled = (int) ((barWidth * percent) / 100);
122         int empty = barWidth - filled;
123 
124         String barColor = warning ? RED : GREEN;
125         String warnIcon = warning ? color(" ⚠", YELLOW) : "  ";
126 
127         return " " + color(label, BOLD) + " " +
128             color("[", DIM) +
129             color(repeat(BLOCK_FULL, filled), barColor) +
130             color(repeat(BLOCK_LIGHT, empty), BRIGHT_BLACK) +
131             color("]", DIM) +
132             color(String.format(" %3.0f%%", percent), warning ? RED : WHITE) +
133             warnIcon + " ";
134     }
135 
136     /**
137      * Format content in a responsive box with title.
138      */
139     private static String formatBox(String title, String content, int boxWidth, String borderColor) {
140         StringBuilder sb = new StringBuilder();
141         int innerWidth = boxWidth - 2;
142         String titlePrefix = "─ " + title + " ";  // Calculate actual length
143 
144         // Top border with title
145         sb.append(color("  " + BOX_TOP_LEFT + titlePrefix +
146             repeat(BOX_HORIZONTAL, innerWidth - titlePrefix.length()) + BOX_TOP_RIGHT, borderColor))
147           .append("\n");
148 
149         // Content lines: border(1) + space(1) + content + space(1) + border(1) = boxWidth
150         // So content width = boxWidth - 4 = innerWidth - 2
151         int contentWidth = innerWidth - 2;
152         for (String line : content.split("\n")) {
153             if (!line.isEmpty()) {
154                 String truncated = line.length() > contentWidth
155                     ? line.substring(0, Math.max(0, contentWidth - 3)) + "..."
156                     : line;
157                 sb.append(color("  " + BOX_VERTICAL, borderColor))
158                   .append(" ")
159                   .append(pad(truncated, contentWidth))
160                   .append(" ")
161                   .append(color(BOX_VERTICAL, borderColor))
162                   .append("\n");
163             }
164         }
165 
166         // Bottom border
167         sb.append(color("  " + BOX_BOTTOM_LEFT + repeat(BOX_HORIZONTAL, innerWidth) +
168             BOX_BOTTOM_RIGHT, borderColor))
169           .append("\n");
170 
171         return sb.toString();
172     }
173 
174     /**
175      * Render the format selection menu.
176      */
177     public static View<SysmonState> menu() {
178         return state -> {
179             int boxWidth = state.width() - PADDING;
180             StringBuilder sb = new StringBuilder();
181 
182             sb.append(color("  " + repeat(BOX_HORIZONTAL, boxWidth), DIM))
183               .append("\n\n");
184 
185             // Format options
186             sb.append("  ");
187             int i = 1;
188             for (SysmonFormat f : SysmonFormat.values()) {
189                 String sel = f == state.format() ? "●" : "○";
190                 sb.append(color("[" + i + "]", f.color, BOLD));
191                 sb.append(
192                     color(
193                         " " + sel + " " + f.label + "  ",
194                         f == state.format() ? f.color : BRIGHT_BLACK
195                     )
196                 );
197                 i++;
198             }
199             sb.append("\n\n");
200 
201             // Other options
202             sb.append("  ");
203             sb.append(color("[r]", YELLOW, BOLD))
204               .append(color(" Refresh  ", YELLOW));
205             sb.append(color("[q]", BRIGHT_BLACK, BOLD))
206               .append(color(" Quit", BRIGHT_BLACK));
207             sb.append("\n\n");
208 
209             return sb.toString();
210         };
211     }
212 
213     /**
214      * Render status message.
215      */
216     public static View<SysmonState> status() {
217         return state ->
218             state.status().isEmpty()
219                 ? "\n"
220                 : color(
221                       "  " + ARROW_RIGHT + " " + state.status(),
222                       state.statusColor()
223                   ) +
224                   "\n\n";
225     }
226 
227     /**
228      * Render input prompt.
229      */
230     public static View<SysmonState> prompt() {
231         return state -> color("  > ", CYAN, BOLD);
232     }
233 }