View Javadoc
1   package com.guinetik.hexafun.examples.sysmon;
2   
3   import static com.guinetik.hexafun.examples.tui.Ansi.*;
4   
5   import com.guinetik.hexafun.HexaApp;
6   import com.guinetik.hexafun.examples.tui.View;
7   import java.io.FileInputStream;
8   import java.io.IOException;
9   import java.io.InputStream;
10  
11  /**
12   * System Monitor TUI - interactive demo of UseCaseHandler and AdapterKey patterns.
13   *
14   * <p>Demonstrates the adapter pattern: same domain data ({@link SystemMetrics}),
15   * four completely different presentations selected via menu.</p>
16   *
17   * <h2>Architecture</h2>
18   * <pre>
19   * [SysmonTUI] --renders--> [SysmonView] --uses--> [SysmonState]
20   *                                                      |
21   *                                                 [HexaApp]
22   *                                                /         \
23   *                                        [UseCases]    [Adapters]
24   * </pre>
25   *
26   * <p>Run with: {@code java -cp hexafun-examples/target/hexafun-examples-*.jar
27   * com.guinetik.hexafun.examples.sysmon.SysmonTUI}</p>
28   */
29  public class SysmonTUI {
30  
31      // ═══════════════════════════════════════════════════════════════════
32      //  RUNTIME
33      // ═══════════════════════════════════════════════════════════════════
34  
35      private static final int REFRESH_INTERVAL_MS = 2000;
36  
37      private final View<SysmonState> screen;
38      private final HexaApp app;
39      private InputStream ttyInput;
40  
41      public SysmonTUI() {
42          this.app = SysmonApp.createApp(new OshiMetricsProvider());
43          this.screen = SysmonView.screen();
44      }
45  
46      private void render(SysmonState state) {
47          System.out.print(screen.apply(state));
48          System.out.flush();
49      }
50  
51      /**
52       * Read a single keypress with timeout.
53       * Reads directly from /dev/tty for immediate response.
54       */
55      private int readKeyWithTimeout(int timeoutMs) {
56          try {
57              long deadline = System.currentTimeMillis() + timeoutMs;
58              while (System.currentTimeMillis() < deadline) {
59                  if (ttyInput.available() > 0) {
60                      return ttyInput.read();
61                  }
62                  Thread.sleep(50);
63              }
64          } catch (IOException | InterruptedException ignored) {}
65          return -1; // Timeout
66      }
67  
68      /**
69       * Set terminal to raw mode and enter alternate screen buffer.
70       */
71      private void setRawMode() {
72          // Enter alternate screen buffer (doesn't pollute scrollback)
73          System.out.print(ALT_SCREEN_ON + HIDE_CURSOR);
74          System.out.flush();
75          try {
76              new ProcessBuilder("stty", "-icanon", "-echo")
77                  .inheritIO()
78                  .start()
79                  .waitFor();
80          } catch (Exception ignored) {}
81      }
82  
83      /**
84       * Restore terminal to normal mode and exit alternate screen.
85       */
86      private void restoreTerminal() {
87          // Exit alternate screen buffer, show cursor
88          System.out.print(SHOW_CURSOR + ALT_SCREEN_OFF);
89          System.out.flush();
90          try {
91              new ProcessBuilder("stty", "sane").inheritIO().start().waitFor();
92          } catch (Exception ignored) {}
93          try {
94              if (ttyInput != null) ttyInput.close();
95          } catch (Exception ignored) {}
96      }
97  
98      /**
99       * Main run loop - renders state, waits for input, processes transitions.
100      */
101     public void run() {
102         SysmonState state = SysmonState.initial(app);
103 
104         // Open /dev/tty for direct keyboard input
105         try {
106             ttyInput = new FileInputStream("/dev/tty");
107         } catch (Exception e) {
108             // Fallback to System.in if /dev/tty not available
109             ttyInput = System.in;
110         }
111 
112         // Set up raw mode and ensure cleanup
113         setRawMode();
114         Runtime.getRuntime().addShutdownHook(new Thread(this::restoreTerminal));
115 
116         try {
117             while (state.running()) {
118                 render(state);
119                 int key = readKeyWithTimeout(REFRESH_INTERVAL_MS);
120 
121                 if (key == -1) {
122                     // Timeout - auto refresh
123                     state = state.refresh();
124                 } else {
125                     state = processKey(state, key);
126                 }
127             }
128             render(state);
129         } finally {
130             restoreTerminal();
131         }
132     }
133 
134     /**
135      * Process a keypress and return new state.
136      */
137     private SysmonState processKey(SysmonState state, int key) {
138         char c = Character.toLowerCase((char) key);
139 
140         return switch (c) {
141             case 'q', 3 -> state.stop(); // 'q' or Ctrl+C
142             case '1' -> state.withFormat(SysmonFormat.TUI);
143             case '2' -> state.withFormat(SysmonFormat.CLI);
144             case '3' -> state.withFormat(SysmonFormat.JSON);
145             case '4' -> state.withFormat(SysmonFormat.PROMETHEUS);
146             case 'r', ' ', '\n' -> state.refresh();
147             default -> state; // Ignore unknown keys
148         };
149     }
150 
151     // ═══════════════════════════════════════════════════════════════════
152     //  MAIN
153     // ═══════════════════════════════════════════════════════════════════
154 
155     public static void main(String[] args) {
156         new SysmonTUI().run();
157     }
158 }