From 3c5d46b879d0583d10d280f59fb6f41bf3801ccc Mon Sep 17 00:00:00 2001 From: jzitnik-dev Date: Mon, 15 Dec 2025 10:17:42 +0100 Subject: [PATCH] feat: Terminal rendering Now finally we can render cube on a screen with up arrow --- src/main/java/cz/jzitnik/Cli.java | 27 +++++--- .../cz/jzitnik/events/RerenderScreen.java | 13 ++-- .../jzitnik/events/handlers/CliHandler.java | 42 +++++++----- .../events/handlers/ExitEventHandler.java | 1 + .../handlers/KeyboardPressEventHandler.java | 23 +++++++ .../handlers/TerminalResizeEventHandler.java | 28 +++++++- .../java/cz/jzitnik/states/ScreenBuffer.java | 2 +- .../java/cz/jzitnik/states/TerminalState.java | 13 ++++ .../cz/jzitnik/utils/DependencyManager.java | 68 ++++++++++--------- .../cz/jzitnik/utils/events/EventManager.java | 14 +++- 10 files changed, 167 insertions(+), 64 deletions(-) create mode 100644 src/main/java/cz/jzitnik/states/TerminalState.java diff --git a/src/main/java/cz/jzitnik/Cli.java b/src/main/java/cz/jzitnik/Cli.java index f2ce5d3..02398e8 100644 --- a/src/main/java/cz/jzitnik/Cli.java +++ b/src/main/java/cz/jzitnik/Cli.java @@ -6,12 +6,12 @@ import com.googlecode.lanterna.terminal.DefaultTerminalFactory; import com.googlecode.lanterna.terminal.MouseCaptureMode; import cz.jzitnik.annotations.Dependency; import cz.jzitnik.annotations.injectors.InjectDependency; +import cz.jzitnik.annotations.injectors.InjectState; import cz.jzitnik.events.KeyboardPressEvent; import cz.jzitnik.events.MouseAction; -import cz.jzitnik.events.RerenderScreen; import cz.jzitnik.events.TerminalResizeEvent; import cz.jzitnik.states.RunningState; -import cz.jzitnik.utils.StateManager; +import cz.jzitnik.states.TerminalState; import cz.jzitnik.utils.events.EventManager; import lombok.extern.slf4j.Slf4j; @@ -23,8 +23,11 @@ public class Cli implements Runnable { @InjectDependency private EventManager eventManager; - @InjectDependency - private StateManager stateManager; + @InjectState + private TerminalState terminalState; + + @InjectState + private RunningState runningState; @Override public void run() { @@ -33,15 +36,21 @@ public class Cli implements Runnable { try (TerminalScreen terminal = new DefaultTerminalFactory() .setMouseCaptureMode(MouseCaptureMode.CLICK_RELEASE_DRAG_MOVE) .createScreen()) { - stateManager.registerManually(terminal); + terminalState.setTerminalScreen(terminal); + terminalState.setTextGraphics(terminal.newTextGraphics()); terminal.setCursorPosition(null); terminal.doResizeIfNecessary(); - terminal.getTerminal().enterPrivateMode(); - terminal.getTerminal().addResizeListener((_, terminalSize) -> eventManager.emitEvent(new TerminalResizeEvent(terminalSize))); + terminal.getTerminal().addResizeListener((_, terminalSize) -> { + terminal.doResizeIfNecessary(); + eventManager.emitEvent(new TerminalResizeEvent(terminalSize)); + }); + + terminal.startScreen(); + + eventManager.emitEvent(new TerminalResizeEvent(terminal.getTerminalSize())); - RunningState runningState = stateManager.getOrThrow(RunningState.class); while (runningState.isRunning()) { KeyStroke keyStroke = terminal.readInput(); if (keyStroke != null) { @@ -53,8 +62,6 @@ public class Cli implements Runnable { eventManager.emitEvent(new KeyboardPressEvent(keyStroke)); } } - - eventManager.emitEvent(RerenderScreen.full(terminal.getTerminalSize())); } catch (IOException e) { log.error("Terminal error occurred, shutting down CLI thread.", e); throw new RuntimeException(e); diff --git a/src/main/java/cz/jzitnik/events/RerenderScreen.java b/src/main/java/cz/jzitnik/events/RerenderScreen.java index acbc85f..dbaca6c 100644 --- a/src/main/java/cz/jzitnik/events/RerenderScreen.java +++ b/src/main/java/cz/jzitnik/events/RerenderScreen.java @@ -1,5 +1,6 @@ package cz.jzitnik.events; +import com.googlecode.lanterna.TerminalPosition; import com.googlecode.lanterna.TerminalSize; import cz.jzitnik.utils.events.Event; import lombok.AllArgsConstructor; @@ -8,12 +9,16 @@ import lombok.Data; import java.awt.*; public record RerenderScreen(ScreenPart[] parts) implements Event { + public RerenderScreen(ScreenPart part) { + this(new ScreenPart[] { part }); + } + public static RerenderScreen full(TerminalSize terminalSize) { return new RerenderScreen( new ScreenPart[]{ new ScreenPart( - new Point(0, 0), - new Point(terminalSize.getRows() - 1, terminalSize.getColumns() - 1) + new TerminalPosition(0, 0), + new TerminalPosition(terminalSize.getColumns() - 1, terminalSize.getRows() - 1) ) } ); @@ -22,7 +27,7 @@ public record RerenderScreen(ScreenPart[] parts) implements Event { @Data @AllArgsConstructor public static class ScreenPart { - private Point start; - private Point end; + private TerminalPosition start; + private TerminalPosition end; } } diff --git a/src/main/java/cz/jzitnik/events/handlers/CliHandler.java b/src/main/java/cz/jzitnik/events/handlers/CliHandler.java index c971e9e..272aa24 100644 --- a/src/main/java/cz/jzitnik/events/handlers/CliHandler.java +++ b/src/main/java/cz/jzitnik/events/handlers/CliHandler.java @@ -2,22 +2,25 @@ package cz.jzitnik.events.handlers; import com.googlecode.lanterna.TextCharacter; import com.googlecode.lanterna.TextColor; -import com.googlecode.lanterna.screen.TerminalScreen; +import com.googlecode.lanterna.graphics.TextGraphics; +import com.googlecode.lanterna.screen.Screen; import cz.jzitnik.annotations.EventHandler; import cz.jzitnik.annotations.injectors.InjectState; import cz.jzitnik.events.RerenderScreen; import cz.jzitnik.states.ScreenBuffer; +import cz.jzitnik.states.TerminalState; import cz.jzitnik.ui.pixels.Empty; import cz.jzitnik.ui.pixels.Pixel; import cz.jzitnik.utils.DependencyManager; import cz.jzitnik.utils.events.AbstractEventHandler; import java.awt.*; +import java.io.IOException; @EventHandler(RerenderScreen.class) public class CliHandler extends AbstractEventHandler { @InjectState - private TerminalScreen terminalScreen; + private TerminalState terminalState; @InjectState private ScreenBuffer screenBuffer; @@ -30,25 +33,32 @@ public class CliHandler extends AbstractEventHandler { public void handle(RerenderScreen event) { var parts = event.parts(); var buffer = screenBuffer.getBuffer(); + var terminalScreen = terminalState.getTerminalScreen(); + var tg = terminalState.getTextGraphics(); for (RerenderScreen.ScreenPart part : parts) { - Point a = part.getStart(); - Point b = part.getEnd(); + var start = part.getStart(); + var end = part.getEnd(); - for (double y = a.getY(); y < b.getY(); y++) { - for (double x = a.getX(); x < b.getX(); x++) { - Pixel pixel = buffer[(int) y][(int) x]; - terminalScreen.setCharacter( - (int) x, - (int) y, - new TextCharacter( - ' ', - TextColor.ANSI.DEFAULT, - pixel.getClass().equals(Empty.class) ? TextColor.ANSI.DEFAULT : pixel.getColor() - ) - ); + for (int y = start.getRow(); y <= end.getRow(); y++) { + for (int x = start.getColumn(); x <= end.getColumn(); x++) { + Pixel pixel = buffer[y][x]; + TextColor color = pixel.getClass().equals(Empty.class) ? TextColor.ANSI.BLACK : pixel.getColor(); + + drawPixel(tg, x, y, color); } } } + + try { + terminalScreen.refresh(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static void drawPixel(TextGraphics tg, int x, int y, TextColor color) { + tg.setForegroundColor(color); + tg.setCharacter(x, y, '█'); // full block character } } diff --git a/src/main/java/cz/jzitnik/events/handlers/ExitEventHandler.java b/src/main/java/cz/jzitnik/events/handlers/ExitEventHandler.java index 9149ffe..69bf627 100644 --- a/src/main/java/cz/jzitnik/events/handlers/ExitEventHandler.java +++ b/src/main/java/cz/jzitnik/events/handlers/ExitEventHandler.java @@ -18,5 +18,6 @@ public class ExitEventHandler extends AbstractEventHandler { StateManager stateManager = dm.getDependencyOrThrow(StateManager.class); RunningState runningState = stateManager.getOrThrow(RunningState.class); runningState.setRunning(false); + System.exit(0); // Pls don't blame me } } diff --git a/src/main/java/cz/jzitnik/events/handlers/KeyboardPressEventHandler.java b/src/main/java/cz/jzitnik/events/handlers/KeyboardPressEventHandler.java index 50b1ac8..d1ab6da 100644 --- a/src/main/java/cz/jzitnik/events/handlers/KeyboardPressEventHandler.java +++ b/src/main/java/cz/jzitnik/events/handlers/KeyboardPressEventHandler.java @@ -1,9 +1,16 @@ package cz.jzitnik.events.handlers; +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TextColor; import com.googlecode.lanterna.input.KeyStroke; import cz.jzitnik.annotations.EventHandler; +import cz.jzitnik.annotations.injectors.InjectState; import cz.jzitnik.events.ExitEvent; import cz.jzitnik.events.KeyboardPressEvent; +import cz.jzitnik.events.RerenderScreen; +import cz.jzitnik.states.ScreenBuffer; +import cz.jzitnik.ui.pixels.ColoredPixel; +import cz.jzitnik.ui.pixels.Pixel; import cz.jzitnik.utils.DependencyManager; import cz.jzitnik.utils.events.AbstractEventHandler; import cz.jzitnik.utils.events.EventManager; @@ -14,13 +21,29 @@ public class KeyboardPressEventHandler extends AbstractEventHandler { @@ -11,8 +19,26 @@ public class TerminalResizeEventHandler extends AbstractEventHandler clazz = instance.getClass(); + Class clazz = instance.getClass(); - for (Field field : clazz.getDeclaredFields()) { - if (field.isAnnotationPresent(InjectDependency.class)) { - field.setAccessible(true); + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(InjectDependency.class)) { + field.setAccessible(true); - if (!data.containsKey(field.getType())) continue; + if (!data.containsKey(field.getType())) continue; - Object dependency = field.getType() == getClass() ? this : data.get(field.getType()); + Object dependency = field.getType() == getClass() ? this : data.get(field.getType()); - if (!field.getType().isAssignableFrom(dependency.getClass())) continue; + if (!field.getType().isAssignableFrom(dependency.getClass())) continue; - try { - field.set(instance, dependency); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } else if (field.isAnnotationPresent(InjectState.class)) { - field.setAccessible(true); + try { + field.set(instance, dependency); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } else if (field.isAnnotationPresent(InjectState.class)) { + field.setAccessible(true); - Optional stateOptional = stateManager.get(field.getType()); + Optional stateOptional = stateManager.get(field.getType()); - if (stateOptional.isEmpty()) continue; + if (stateOptional.isEmpty()) continue; - try { - field.set(instance, stateOptional.get()); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } else if (field.isAnnotationPresent(InjectConfig.class)) { - field.setAccessible(true); - Optional config = Optional.ofNullable(configs.get(field.getType())); + try { + field.set(instance, stateOptional.get()); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } else if (field.isAnnotationPresent(InjectConfig.class)) { + field.setAccessible(true); + Optional config = Optional.ofNullable(configs.get(field.getType())); - if (config.isEmpty()) continue; + if (config.isEmpty()) continue; - try { - field.set(instance, config.get()); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } + try { + field.set(instance, config.get()); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); } } } diff --git a/src/main/java/cz/jzitnik/utils/events/EventManager.java b/src/main/java/cz/jzitnik/utils/events/EventManager.java index 61617a9..d21ea5e 100644 --- a/src/main/java/cz/jzitnik/utils/events/EventManager.java +++ b/src/main/java/cz/jzitnik/utils/events/EventManager.java @@ -26,6 +26,7 @@ public class EventManager extends Thread { private ExecutorService eventExecutor; private final HashMap, AbstractEventHandler> handlers = new HashMap<>(); private final BlockingQueue eventQueue = new LinkedBlockingQueue<>(); + private final DependencyManager dependencyManager; @SuppressWarnings("unchecked") private AbstractEventHandler getHandler(Class type) { @@ -38,6 +39,7 @@ public class EventManager extends Thread { @SuppressWarnings("unchecked") public EventManager(Reflections reflections, DependencyManager dependencyManager) { + this.dependencyManager = dependencyManager; setDaemon(true); var classes = reflections.getTypesAnnotatedWith(EventHandler.class); @@ -56,6 +58,10 @@ public class EventManager extends Thread { @Override public void run() { + for (Object instance : handlers.values()) { + dependencyManager.inject(instance); + } + eventExecutor = Executors.newFixedThreadPool(eventThreadPoolConfig.getThreadCount()); while (runningState.isRunning()) { try { @@ -76,7 +82,13 @@ public class EventManager extends Thread { AbstractEventHandler handler = getHandler((Class) event.getClass()); if (handler != null) { - eventExecutor.submit(() -> handler.handle(typedEvent)); + eventExecutor.submit(() -> { + try { + handler.handle(typedEvent); + } catch (Exception e) { + e.printStackTrace(System.err); + } + }); } } }