feat: Terminal rendering

Now finally we can render cube on a screen with up arrow
This commit is contained in:
2025-12-15 10:17:42 +01:00
parent da234d9985
commit 3c5d46b879
10 changed files with 167 additions and 64 deletions

View File

@@ -6,12 +6,12 @@ import com.googlecode.lanterna.terminal.DefaultTerminalFactory;
import com.googlecode.lanterna.terminal.MouseCaptureMode; import com.googlecode.lanterna.terminal.MouseCaptureMode;
import cz.jzitnik.annotations.Dependency; import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.injectors.InjectDependency; import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.KeyboardPressEvent; import cz.jzitnik.events.KeyboardPressEvent;
import cz.jzitnik.events.MouseAction; import cz.jzitnik.events.MouseAction;
import cz.jzitnik.events.RerenderScreen;
import cz.jzitnik.events.TerminalResizeEvent; import cz.jzitnik.events.TerminalResizeEvent;
import cz.jzitnik.states.RunningState; import cz.jzitnik.states.RunningState;
import cz.jzitnik.utils.StateManager; import cz.jzitnik.states.TerminalState;
import cz.jzitnik.utils.events.EventManager; import cz.jzitnik.utils.events.EventManager;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -23,8 +23,11 @@ public class Cli implements Runnable {
@InjectDependency @InjectDependency
private EventManager eventManager; private EventManager eventManager;
@InjectDependency @InjectState
private StateManager stateManager; private TerminalState terminalState;
@InjectState
private RunningState runningState;
@Override @Override
public void run() { public void run() {
@@ -33,15 +36,21 @@ public class Cli implements Runnable {
try (TerminalScreen terminal = new DefaultTerminalFactory() try (TerminalScreen terminal = new DefaultTerminalFactory()
.setMouseCaptureMode(MouseCaptureMode.CLICK_RELEASE_DRAG_MOVE) .setMouseCaptureMode(MouseCaptureMode.CLICK_RELEASE_DRAG_MOVE)
.createScreen()) { .createScreen()) {
stateManager.registerManually(terminal); terminalState.setTerminalScreen(terminal);
terminalState.setTextGraphics(terminal.newTextGraphics());
terminal.setCursorPosition(null); terminal.setCursorPosition(null);
terminal.doResizeIfNecessary(); 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()) { while (runningState.isRunning()) {
KeyStroke keyStroke = terminal.readInput(); KeyStroke keyStroke = terminal.readInput();
if (keyStroke != null) { if (keyStroke != null) {
@@ -53,8 +62,6 @@ public class Cli implements Runnable {
eventManager.emitEvent(new KeyboardPressEvent(keyStroke)); eventManager.emitEvent(new KeyboardPressEvent(keyStroke));
} }
} }
eventManager.emitEvent(RerenderScreen.full(terminal.getTerminalSize()));
} catch (IOException e) { } catch (IOException e) {
log.error("Terminal error occurred, shutting down CLI thread.", e); log.error("Terminal error occurred, shutting down CLI thread.", e);
throw new RuntimeException(e); throw new RuntimeException(e);

View File

@@ -1,5 +1,6 @@
package cz.jzitnik.events; package cz.jzitnik.events;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TerminalSize; import com.googlecode.lanterna.TerminalSize;
import cz.jzitnik.utils.events.Event; import cz.jzitnik.utils.events.Event;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@@ -8,12 +9,16 @@ import lombok.Data;
import java.awt.*; import java.awt.*;
public record RerenderScreen(ScreenPart[] parts) implements Event { public record RerenderScreen(ScreenPart[] parts) implements Event {
public RerenderScreen(ScreenPart part) {
this(new ScreenPart[] { part });
}
public static RerenderScreen full(TerminalSize terminalSize) { public static RerenderScreen full(TerminalSize terminalSize) {
return new RerenderScreen( return new RerenderScreen(
new ScreenPart[]{ new ScreenPart[]{
new ScreenPart( new ScreenPart(
new Point(0, 0), new TerminalPosition(0, 0),
new Point(terminalSize.getRows() - 1, terminalSize.getColumns() - 1) new TerminalPosition(terminalSize.getColumns() - 1, terminalSize.getRows() - 1)
) )
} }
); );
@@ -22,7 +27,7 @@ public record RerenderScreen(ScreenPart[] parts) implements Event {
@Data @Data
@AllArgsConstructor @AllArgsConstructor
public static class ScreenPart { public static class ScreenPart {
private Point start; private TerminalPosition start;
private Point end; private TerminalPosition end;
} }
} }

View File

@@ -2,22 +2,25 @@ package cz.jzitnik.events.handlers;
import com.googlecode.lanterna.TextCharacter; import com.googlecode.lanterna.TextCharacter;
import com.googlecode.lanterna.TextColor; 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.EventHandler;
import cz.jzitnik.annotations.injectors.InjectState; import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.RerenderScreen; import cz.jzitnik.events.RerenderScreen;
import cz.jzitnik.states.ScreenBuffer; import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.states.TerminalState;
import cz.jzitnik.ui.pixels.Empty; import cz.jzitnik.ui.pixels.Empty;
import cz.jzitnik.ui.pixels.Pixel; import cz.jzitnik.ui.pixels.Pixel;
import cz.jzitnik.utils.DependencyManager; import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.events.AbstractEventHandler; import cz.jzitnik.utils.events.AbstractEventHandler;
import java.awt.*; import java.awt.*;
import java.io.IOException;
@EventHandler(RerenderScreen.class) @EventHandler(RerenderScreen.class)
public class CliHandler extends AbstractEventHandler<RerenderScreen> { public class CliHandler extends AbstractEventHandler<RerenderScreen> {
@InjectState @InjectState
private TerminalScreen terminalScreen; private TerminalState terminalState;
@InjectState @InjectState
private ScreenBuffer screenBuffer; private ScreenBuffer screenBuffer;
@@ -30,25 +33,32 @@ public class CliHandler extends AbstractEventHandler<RerenderScreen> {
public void handle(RerenderScreen event) { public void handle(RerenderScreen event) {
var parts = event.parts(); var parts = event.parts();
var buffer = screenBuffer.getBuffer(); var buffer = screenBuffer.getBuffer();
var terminalScreen = terminalState.getTerminalScreen();
var tg = terminalState.getTextGraphics();
for (RerenderScreen.ScreenPart part : parts) { for (RerenderScreen.ScreenPart part : parts) {
Point a = part.getStart(); var start = part.getStart();
Point b = part.getEnd(); var end = part.getEnd();
for (double y = a.getY(); y < b.getY(); y++) { for (int y = start.getRow(); y <= end.getRow(); y++) {
for (double x = a.getX(); x < b.getX(); x++) { for (int x = start.getColumn(); x <= end.getColumn(); x++) {
Pixel pixel = buffer[(int) y][(int) x]; Pixel pixel = buffer[y][x];
terminalScreen.setCharacter( TextColor color = pixel.getClass().equals(Empty.class) ? TextColor.ANSI.BLACK : pixel.getColor();
(int) x,
(int) y, drawPixel(tg, x, y, color);
new TextCharacter(
' ',
TextColor.ANSI.DEFAULT,
pixel.getClass().equals(Empty.class) ? TextColor.ANSI.DEFAULT : pixel.getColor()
)
);
} }
} }
} }
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
} }
} }

View File

@@ -18,5 +18,6 @@ public class ExitEventHandler extends AbstractEventHandler<ExitEvent> {
StateManager stateManager = dm.getDependencyOrThrow(StateManager.class); StateManager stateManager = dm.getDependencyOrThrow(StateManager.class);
RunningState runningState = stateManager.getOrThrow(RunningState.class); RunningState runningState = stateManager.getOrThrow(RunningState.class);
runningState.setRunning(false); runningState.setRunning(false);
System.exit(0); // Pls don't blame me
} }
} }

View File

@@ -1,9 +1,16 @@
package cz.jzitnik.events.handlers; package cz.jzitnik.events.handlers;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.input.KeyStroke; import com.googlecode.lanterna.input.KeyStroke;
import cz.jzitnik.annotations.EventHandler; import cz.jzitnik.annotations.EventHandler;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.ExitEvent; import cz.jzitnik.events.ExitEvent;
import cz.jzitnik.events.KeyboardPressEvent; 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.DependencyManager;
import cz.jzitnik.utils.events.AbstractEventHandler; import cz.jzitnik.utils.events.AbstractEventHandler;
import cz.jzitnik.utils.events.EventManager; import cz.jzitnik.utils.events.EventManager;
@@ -14,13 +21,29 @@ public class KeyboardPressEventHandler extends AbstractEventHandler<KeyboardPres
super(dm); super(dm);
} }
@InjectState
private ScreenBuffer screenBuffer;
@Override @Override
public void handle(KeyboardPressEvent event) { public void handle(KeyboardPressEvent event) {
EventManager eventManager = dm.getDependencyOrThrow(EventManager.class); EventManager eventManager = dm.getDependencyOrThrow(EventManager.class);
KeyStroke keyStroke = event.getKeyStroke(); KeyStroke keyStroke = event.getKeyStroke();
Pixel[][] buffer = screenBuffer.getBuffer();
switch (keyStroke.getKeyType()) { switch (keyStroke.getKeyType()) {
case ArrowUp:
var start = new TerminalPosition(0, 0);
var end = new TerminalPosition(20, 10);
for (int y = start.getRow(); y < end.getRow(); y++) {
for (int x = start.getColumn(); x < end.getColumn(); x++) {
buffer[y][x] = new ColoredPixel(TextColor.ANSI.CYAN);
}
}
eventManager.emitEvent(new RerenderScreen(new RerenderScreen.ScreenPart(start, end)));
break;
case Escape: case Escape:
eventManager.emitEvent(new ExitEvent()); eventManager.emitEvent(new ExitEvent());
break; break;

View File

@@ -1,9 +1,17 @@
package cz.jzitnik.events.handlers; package cz.jzitnik.events.handlers;
import com.googlecode.lanterna.TerminalSize;
import cz.jzitnik.annotations.EventHandler; import cz.jzitnik.annotations.EventHandler;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.RerenderScreen;
import cz.jzitnik.events.TerminalResizeEvent; import cz.jzitnik.events.TerminalResizeEvent;
import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.ui.pixels.Empty;
import cz.jzitnik.ui.pixels.Pixel;
import cz.jzitnik.utils.DependencyManager; import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.events.AbstractEventHandler; import cz.jzitnik.utils.events.AbstractEventHandler;
import cz.jzitnik.utils.events.EventManager;
@EventHandler(TerminalResizeEvent.class) @EventHandler(TerminalResizeEvent.class)
public class TerminalResizeEventHandler extends AbstractEventHandler<TerminalResizeEvent> { public class TerminalResizeEventHandler extends AbstractEventHandler<TerminalResizeEvent> {
@@ -11,8 +19,26 @@ public class TerminalResizeEventHandler extends AbstractEventHandler<TerminalRes
super(dm); super(dm);
} }
@InjectDependency
private EventManager eventManager;
@InjectState
private ScreenBuffer screenBuffer;
@Override @Override
public void handle(TerminalResizeEvent event) { public void handle(TerminalResizeEvent event) {
System.out.println("NEWSIZE: " + event.getNewSize()); TerminalSize size = event.getNewSize();
int width = size.getColumns();
int height = size.getRows();
Pixel[][] buffer = new Pixel[height][width];
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
buffer[y][x] = new Empty();
}
}
screenBuffer.setBuffer(buffer);
eventManager.emitEvent(RerenderScreen.full(size));
} }
} }

View File

@@ -7,5 +7,5 @@ import lombok.Data;
@Data @Data
@State @State
public class ScreenBuffer { public class ScreenBuffer {
private Pixel[][] buffer; private Pixel[][] buffer = new Pixel[][] {};
} }

View File

@@ -0,0 +1,13 @@
package cz.jzitnik.states;
import com.googlecode.lanterna.graphics.TextGraphics;
import com.googlecode.lanterna.screen.TerminalScreen;
import cz.jzitnik.annotations.State;
import lombok.Data;
@State
@Data
public class TerminalState {
private TerminalScreen terminalScreen;
private TextGraphics textGraphics;
}

View File

@@ -8,6 +8,7 @@ import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.injectors.InjectConfig; import cz.jzitnik.annotations.injectors.InjectConfig;
import cz.jzitnik.annotations.injectors.InjectDependency; import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState; import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.handlers.CliHandler;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.reflections.Reflections; import org.reflections.Reflections;
@@ -96,8 +97,14 @@ public class DependencyManager {
} }
} }
for (Object instance : data.values()) {
inject(instance);
}
}
public void inject(Object instance) {
StateManager stateManager = (StateManager) data.get(StateManager.class); StateManager stateManager = (StateManager) data.get(StateManager.class);
for (Object instance: data.values()) {
Class<?> clazz = instance.getClass(); Class<?> clazz = instance.getClass();
for (Field field : clazz.getDeclaredFields()) { for (Field field : clazz.getDeclaredFields()) {
@@ -141,5 +148,4 @@ public class DependencyManager {
} }
} }
} }
}
} }

View File

@@ -26,6 +26,7 @@ public class EventManager extends Thread {
private ExecutorService eventExecutor; private ExecutorService eventExecutor;
private final HashMap<Class<? extends Event>, AbstractEventHandler<? extends Event>> handlers = new HashMap<>(); private final HashMap<Class<? extends Event>, AbstractEventHandler<? extends Event>> handlers = new HashMap<>();
private final BlockingQueue<Event> eventQueue = new LinkedBlockingQueue<>(); private final BlockingQueue<Event> eventQueue = new LinkedBlockingQueue<>();
private final DependencyManager dependencyManager;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T extends Event> AbstractEventHandler<T> getHandler(Class<T> type) { private <T extends Event> AbstractEventHandler<T> getHandler(Class<T> type) {
@@ -38,6 +39,7 @@ public class EventManager extends Thread {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public EventManager(Reflections reflections, DependencyManager dependencyManager) { public EventManager(Reflections reflections, DependencyManager dependencyManager) {
this.dependencyManager = dependencyManager;
setDaemon(true); setDaemon(true);
var classes = reflections.getTypesAnnotatedWith(EventHandler.class); var classes = reflections.getTypesAnnotatedWith(EventHandler.class);
@@ -56,6 +58,10 @@ public class EventManager extends Thread {
@Override @Override
public void run() { public void run() {
for (Object instance : handlers.values()) {
dependencyManager.inject(instance);
}
eventExecutor = Executors.newFixedThreadPool(eventThreadPoolConfig.getThreadCount()); eventExecutor = Executors.newFixedThreadPool(eventThreadPoolConfig.getThreadCount());
while (runningState.isRunning()) { while (runningState.isRunning()) {
try { try {
@@ -76,7 +82,13 @@ public class EventManager extends Thread {
AbstractEventHandler<T> handler = getHandler((Class<T>) event.getClass()); AbstractEventHandler<T> handler = getHandler((Class<T>) event.getClass());
if (handler != null) { if (handler != null) {
eventExecutor.submit(() -> handler.handle(typedEvent)); eventExecutor.submit(() -> {
try {
handler.handle(typedEvent);
} catch (Exception e) {
e.printStackTrace(System.err);
}
});
} }
} }
} }