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 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);

View File

@@ -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;
}
}

View File

@@ -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<RerenderScreen> {
@InjectState
private TerminalScreen terminalScreen;
private TerminalState terminalState;
@InjectState
private ScreenBuffer screenBuffer;
@@ -30,25 +33,32 @@ public class CliHandler extends AbstractEventHandler<RerenderScreen> {
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
}
}

View File

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

View File

@@ -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<KeyboardPres
super(dm);
}
@InjectState
private ScreenBuffer screenBuffer;
@Override
public void handle(KeyboardPressEvent event) {
EventManager eventManager = dm.getDependencyOrThrow(EventManager.class);
KeyStroke keyStroke = event.getKeyStroke();
Pixel[][] buffer = screenBuffer.getBuffer();
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:
eventManager.emitEvent(new ExitEvent());
break;

View File

@@ -1,9 +1,17 @@
package cz.jzitnik.events.handlers;
import com.googlecode.lanterna.TerminalSize;
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.states.ScreenBuffer;
import cz.jzitnik.ui.pixels.Empty;
import cz.jzitnik.ui.pixels.Pixel;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.events.AbstractEventHandler;
import cz.jzitnik.utils.events.EventManager;
@EventHandler(TerminalResizeEvent.class)
public class TerminalResizeEventHandler extends AbstractEventHandler<TerminalResizeEvent> {
@@ -11,8 +19,26 @@ public class TerminalResizeEventHandler extends AbstractEventHandler<TerminalRes
super(dm);
}
@InjectDependency
private EventManager eventManager;
@InjectState
private ScreenBuffer screenBuffer;
@Override
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
@State
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.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.handlers.CliHandler;
import lombok.extern.slf4j.Slf4j;
import org.reflections.Reflections;
@@ -96,48 +97,53 @@ public class DependencyManager {
}
}
for (Object instance : data.values()) {
inject(instance);
}
}
public void inject(Object instance) {
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()) {
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);
}
}
}

View File

@@ -26,6 +26,7 @@ public class EventManager extends Thread {
private ExecutorService eventExecutor;
private final HashMap<Class<? extends Event>, AbstractEventHandler<? extends Event>> handlers = new HashMap<>();
private final BlockingQueue<Event> eventQueue = new LinkedBlockingQueue<>();
private final DependencyManager dependencyManager;
@SuppressWarnings("unchecked")
private <T extends Event> AbstractEventHandler<T> getHandler(Class<T> 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<T> handler = getHandler((Class<T>) event.getClass());
if (handler != null) {
eventExecutor.submit(() -> handler.handle(typedEvent));
eventExecutor.submit(() -> {
try {
handler.handle(typedEvent);
} catch (Exception e) {
e.printStackTrace(System.err);
}
});
}
}
}