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
10
11
12
13
14 public final class SysmonView {
15
16 private SysmonView() {}
17
18 private static final int PADDING = 4;
19
20
21
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
34
35 public static View<SysmonState> clear() {
36 return state -> CLEAR + CURSOR_HOME;
37 }
38
39
40
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
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
82
83 private static String renderTuiGauges(SystemMetrics metrics, int boxWidth) {
84 StringBuilder sb = new StringBuilder();
85 int innerWidth = boxWidth - 2;
86
87 int barWidth = innerWidth - 16;
88
89 String title = "─ System Monitor ";
90
91
92 sb.append(color(" " + BOX_TOP_LEFT + title +
93 repeat(BOX_HORIZONTAL, innerWidth - title.length()) + BOX_TOP_RIGHT, CYAN)).append("\n");
94
95
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
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
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
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
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
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 + " ";
143
144
145 sb.append(color(" " + BOX_TOP_LEFT + titlePrefix +
146 repeat(BOX_HORIZONTAL, innerWidth - titlePrefix.length()) + BOX_TOP_RIGHT, borderColor))
147 .append("\n");
148
149
150
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
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
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
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
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
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
229
230 public static View<SysmonState> prompt() {
231 return state -> color(" > ", CYAN, BOLD);
232 }
233 }