SysmonTUI.java

package com.guinetik.hexafun.examples.sysmon;

import static com.guinetik.hexafun.examples.tui.Ansi.*;

import com.guinetik.hexafun.HexaApp;
import com.guinetik.hexafun.examples.tui.View;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * System Monitor TUI - interactive demo of UseCaseHandler and AdapterKey patterns.
 *
 * <p>Demonstrates the adapter pattern: same domain data ({@link SystemMetrics}),
 * four completely different presentations selected via menu.</p>
 *
 * <h2>Architecture</h2>
 * <pre>
 * [SysmonTUI] --renders--> [SysmonView] --uses--> [SysmonState]
 *                                                      |
 *                                                 [HexaApp]
 *                                                /         \
 *                                        [UseCases]    [Adapters]
 * </pre>
 *
 * <p>Run with: {@code java -cp hexafun-examples/target/hexafun-examples-*.jar
 * com.guinetik.hexafun.examples.sysmon.SysmonTUI}</p>
 */
public class SysmonTUI {

    // ═══════════════════════════════════════════════════════════════════
    //  RUNTIME
    // ═══════════════════════════════════════════════════════════════════

    private static final int REFRESH_INTERVAL_MS = 2000;

    private final View<SysmonState> screen;
    private final HexaApp app;
    private InputStream ttyInput;

    public SysmonTUI() {
        this.app = SysmonApp.createApp(new OshiMetricsProvider());
        this.screen = SysmonView.screen();
    }

    private void render(SysmonState state) {
        System.out.print(screen.apply(state));
        System.out.flush();
    }

    /**
     * Read a single keypress with timeout.
     * Reads directly from /dev/tty for immediate response.
     */
    private int readKeyWithTimeout(int timeoutMs) {
        try {
            long deadline = System.currentTimeMillis() + timeoutMs;
            while (System.currentTimeMillis() < deadline) {
                if (ttyInput.available() > 0) {
                    return ttyInput.read();
                }
                Thread.sleep(50);
            }
        } catch (IOException | InterruptedException ignored) {}
        return -1; // Timeout
    }

    /**
     * Set terminal to raw mode and enter alternate screen buffer.
     */
    private void setRawMode() {
        // Enter alternate screen buffer (doesn't pollute scrollback)
        System.out.print(ALT_SCREEN_ON + HIDE_CURSOR);
        System.out.flush();
        try {
            new ProcessBuilder("stty", "-icanon", "-echo")
                .inheritIO()
                .start()
                .waitFor();
        } catch (Exception ignored) {}
    }

    /**
     * Restore terminal to normal mode and exit alternate screen.
     */
    private void restoreTerminal() {
        // Exit alternate screen buffer, show cursor
        System.out.print(SHOW_CURSOR + ALT_SCREEN_OFF);
        System.out.flush();
        try {
            new ProcessBuilder("stty", "sane").inheritIO().start().waitFor();
        } catch (Exception ignored) {}
        try {
            if (ttyInput != null) ttyInput.close();
        } catch (Exception ignored) {}
    }

    /**
     * Main run loop - renders state, waits for input, processes transitions.
     */
    public void run() {
        SysmonState state = SysmonState.initial(app);

        // Open /dev/tty for direct keyboard input
        try {
            ttyInput = new FileInputStream("/dev/tty");
        } catch (Exception e) {
            // Fallback to System.in if /dev/tty not available
            ttyInput = System.in;
        }

        // Set up raw mode and ensure cleanup
        setRawMode();
        Runtime.getRuntime().addShutdownHook(new Thread(this::restoreTerminal));

        try {
            while (state.running()) {
                render(state);
                int key = readKeyWithTimeout(REFRESH_INTERVAL_MS);

                if (key == -1) {
                    // Timeout - auto refresh
                    state = state.refresh();
                } else {
                    state = processKey(state, key);
                }
            }
            render(state);
        } finally {
            restoreTerminal();
        }
    }

    /**
     * Process a keypress and return new state.
     */
    private SysmonState processKey(SysmonState state, int key) {
        char c = Character.toLowerCase((char) key);

        return switch (c) {
            case 'q', 3 -> state.stop(); // 'q' or Ctrl+C
            case '1' -> state.withFormat(SysmonFormat.TUI);
            case '2' -> state.withFormat(SysmonFormat.CLI);
            case '3' -> state.withFormat(SysmonFormat.JSON);
            case '4' -> state.withFormat(SysmonFormat.PROMETHEUS);
            case 'r', ' ', '\n' -> state.refresh();
            default -> state; // Ignore unknown keys
        };
    }

    // ═══════════════════════════════════════════════════════════════════
    //  MAIN
    // ═══════════════════════════════════════════════════════════════════

    public static void main(String[] args) {
        new SysmonTUI().run();
    }
}