Commit 091d6babd52d8bd01db863115f7da97dabdf9a85

Commits
[COMMIT BEGIN]
commit 091d6babd52d8bd01db863115f7da97dabdf9a85
Author: 0x4248 <[email protected]>
Date:   Fri Mar 6 21:59:24 2026 +0000

    nova: init

diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/BIOS.java b/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/BIOS.java
deleted file mode 100644
index ecbef49..0000000
--- a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/BIOS.java
+++ /dev/null
@@ -1,237 +0,0 @@
-package com.github._0x4248.nova.BIOS;
-
-import com.github._0x4248.nova.BIOS.machines.Machine;
-import com.github._0x4248.nova.BIOS.machines.StandardMachine;
-
-import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.time.LocalTime;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.Enumeration;
-import java.util.List;
-import java.util.jar.Attributes;
-import java.util.jar.JarEntry;
-import java.util.jar.JarFile;
-
-public class BIOS {
-	private static volatile BiosRuntime activeRuntime;
-	private static final int LOG_FOREGROUND = 15;
-	private static final int LOG_BACKGROUND = 1;
-	private static final int HEADER_FOREGROUND = 14;
-	private static final DateTimeFormatter CLOCK_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
-
-	private final Machine machine;
-	private final BiosRuntime runtime;
-	private final List<String> logLines;
-	private final int maxLogLines;
-
-	public BIOS() {
-		this(new StandardMachine());
-	}
-
-	public BIOS(Machine machine) {
-		this.machine = machine;
-		this.runtime = new BiosRuntime(machine);
-		activeRuntime = this.runtime;
-		this.logLines = new ArrayList<>();
-		this.maxLogLines = 22;
-
-		initializeScreen();
-	}
-
-	public static BiosRuntime getRuntime() {
-		return activeRuntime;
-	}
-
-	public boolean bootApplication(Path applicationJarPath, String[] applicationArgs) {
-		if (applicationJarPath == null) {
-			log("Boot failed: no application path provided.");
-			return false;
-		}
-
-		if (!Files.exists(applicationJarPath) || !Files.isRegularFile(applicationJarPath)) {
-			log("Boot failed: application not found at " + applicationJarPath.toAbsolutePath());
-			return false;
-		}
-
-		log("Boot device detected: " + applicationJarPath.getFileName());
-
-		String mainClassName;
-		try {
-			mainClassName = resolveMainClass(applicationJarPath);
-		} catch (IOException e) {
-			log("Boot failed: cannot read application jar metadata.");
-			return false;
-		}
-
-		if (mainClassName == null || mainClassName.isBlank()) {
-			log("Boot failed: no boot entry found (manifest Main-Class, Boot, or Main).");
-			return false;
-		}
-
-		log("Entrypoint: " + mainClassName);
-		log("Handing off control to application...");
-		runtime.beep(70, 720);
-
-		URL jarUrl;
-		try {
-			jarUrl = applicationJarPath.toUri().toURL();
-		} catch (IOException e) {
-			log("Boot failed: invalid application jar URL.");
-			return false;
-		}
-
-		try (URLClassLoader classLoader = new URLClassLoader(new URL[]{jarUrl}, BIOS.class.getClassLoader())) {
-			invokeMainClass(mainClassName, classLoader, applicationArgs);
-			System.out.println("[BIOS] Application exited.");
-			return true;
-		} catch (ClassNotFoundException e) {
-			log("Boot failed: main class not found: " + mainClassName);
-			return false;
-		} catch (NoSuchMethodException e) {
-			log("Boot failed: class has no main(String[] args): " + mainClassName);
-			return false;
-		} catch (IllegalAccessException e) {
-			log("Boot failed: cannot access main method: " + mainClassName);
-			return false;
-		} catch (InvocationTargetException e) {
-			log("Application crashed: " + e.getTargetException());
-			return false;
-		} catch (IOException e) {
-			log("Boot failed: cannot open classloader.");
-			return false;
-		}
-	}
-
-	public boolean bootInternalApplication(String className, String[] applicationArgs) {
-		if (className == null || className.isBlank()) {
-			log("Boot failed: no internal class name provided.");
-			return false;
-		}
-
-		log("Boot source: internal ROM application");
-		log("Entrypoint: " + className);
-		log("Handing off control to application...");
-		runtime.beep(70, 720);
-
-		try {
-			invokeMainClass(className, BIOS.class.getClassLoader(), applicationArgs);
-			System.out.println("[BIOS] Application exited.");
-			return true;
-		} catch (ClassNotFoundException e) {
-			log("Boot failed: main class not found: " + className);
-			return false;
-		} catch (NoSuchMethodException e) {
-			log("Boot failed: class has no main(String[] args): " + className);
-			return false;
-		} catch (IllegalAccessException e) {
-			log("Boot failed: cannot access main method: " + className);
-			return false;
-		} catch (InvocationTargetException e) {
-			log("Application crashed: " + e.getTargetException());
-			return false;
-		}
-	}
-
-	private void initializeScreen() {
-		runtime.clear(LOG_BACKGROUND);
-		runtime.drawText(8, 8, machine.biosLabel, HEADER_FOREGROUND, LOG_BACKGROUND, false);
-		runtime.drawText(8, 20, "Boot sequence start", LOG_FOREGROUND, LOG_BACKGROUND, false);
-		present();
-		runtime.beep(60, 520);
-	}
-
-	private void log(String message) {
-		String timestamp = LocalTime.now().format(CLOCK_FORMAT);
-		String line = "[" + timestamp + "] " + message;
-		System.out.println("[BIOS] " + message);
-
-		logLines.add(line);
-		if (logLines.size() > maxLogLines) {
-			logLines.remove(0);
-		}
-
-		renderLogScreen();
-		present();
-	}
-
-	private void renderLogScreen() {
-		runtime.clear(LOG_BACKGROUND);
-		runtime.drawText(8, 8, machine.biosLabel, HEADER_FOREGROUND, LOG_BACKGROUND, false);
-		runtime.drawText(8, 20, "Boot log", HEADER_FOREGROUND, LOG_BACKGROUND, false);
-
-		int y = 36;
-		int maxCharsPerLine = Math.max(1, runtime.getTextColumns() - 2);
-		for (String line : logLines) {
-			String clipped = line.length() > maxCharsPerLine ? line.substring(0, maxCharsPerLine) : line;
-			runtime.drawText(8, y, clipped, LOG_FOREGROUND, LOG_BACKGROUND, false);
-			y += 8;
-		}
-	}
-
-	private void present() {
-		runtime.present();
-	}
-
-	private void invokeMainClass(String className, ClassLoader classLoader, String[] applicationArgs)
-			throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
-		Class<?> mainClass = Class.forName(className, true, classLoader);
-		Method mainMethod = mainClass.getMethod("main", String[].class);
-
-		int modifiers = mainMethod.getModifiers();
-		if (!Modifier.isPublic(modifiers) || !Modifier.isStatic(modifiers)) {
-			throw new IllegalAccessException("main method must be public static");
-		}
-
-		mainMethod.invoke(null, (Object) (applicationArgs == null ? new String[0] : applicationArgs));
-	}
-
-	private String resolveMainClass(Path applicationJarPath) throws IOException {
-		try (JarFile jarFile = new JarFile(applicationJarPath.toFile())) {
-			if (jarFile.getManifest() != null) {
-				Attributes mainAttributes = jarFile.getManifest().getMainAttributes();
-				String manifestMainClass = mainAttributes.getValue(Attributes.Name.MAIN_CLASS);
-				if (manifestMainClass != null && !manifestMainClass.isBlank()) {
-					return manifestMainClass.trim();
-				}
-			}
-
-			String bootClass = null;
-			String mainClass = null;
-
-			Enumeration<JarEntry> entries = jarFile.entries();
-			while (entries.hasMoreElements()) {
-				JarEntry entry = entries.nextElement();
-				String name = entry.getName();
-				if (!name.endsWith(".class") || name.contains("$") || name.startsWith("META-INF/")) {
-					continue;
-				}
-
-				String className = name.substring(0, name.length() - 6).replace('/', '.');
-				String simpleName = className.substring(className.lastIndexOf('.') + 1);
-
-				if (simpleName.equalsIgnoreCase("Boot")) {
-					bootClass = className;
-					break;
-				}
-
-				if (mainClass == null && simpleName.equalsIgnoreCase("Main")) {
-					mainClass = className;
-				}
-			}
-
-			if (bootClass != null) {
-				return bootClass;
-			}
-
-			return mainClass;
-		}
-	}
-}
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/BiosRuntime.java b/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/BiosRuntime.java
deleted file mode 100644
index fae6889..0000000
--- a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/BiosRuntime.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package com.github._0x4248.nova.BIOS;
-
-import com.github._0x4248.nova.BIOS.VGA.VGA;
-import com.github._0x4248.nova.BIOS.machines.Machine;
-import com.github._0x4248.nova.Core.Gui;
-
-public class BiosRuntime {
-
-    private final Machine machine;
-    private final VGA vga;
-    private final Gui screen;
-
-    public BiosRuntime(Machine machine) {
-        this.machine = machine;
-        this.vga = new VGA();
-        this.screen = new Gui(vga.getWidth() * machine.screenScale, vga.getHeight() * machine.screenScale);
-    }
-
-    public Machine getMachine() {
-        return machine;
-    }
-
-    public int getWidth() {
-        return vga.getWidth();
-    }
-
-    public int getHeight() {
-        return vga.getHeight();
-    }
-
-    public int getTextColumns() {
-        return getWidth() / 8;
-    }
-
-    public void clear(int colorIndex) {
-        vga.clear(colorIndex);
-    }
-
-    public void drawText(int x, int y, String text, int foregroundColor, int backgroundColor, boolean transparentBackground) {
-        vga.drawText(x, y, text, foregroundColor, backgroundColor, transparentBackground);
-    }
-
-    public void setPixel(int x, int y, int colorIndex) {
-        vga.setPixel(x, y, colorIndex);
-    }
-
-    public void present() {
-        vga.blitToGui(screen, machine.screenScale);
-    }
-
-    public void beep(int lengthMs, int pitchHz) {
-        if (!machine.supportsSound) {
-            return;
-        }
-        screen.biosBeep(lengthMs, pitchHz);
-    }
-
-    public boolean hasKeyPress() {
-        if (!machine.supportsKeyboard) {
-            return false;
-        }
-        return screen.hasKeyPress();
-    }
-
-    public Integer pollKeyCode() {
-        if (!machine.supportsKeyboard) {
-            return null;
-        }
-        return screen.pollKeyCode();
-    }
-}
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/VGA/VGA.java b/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/VGA/VGA.java
deleted file mode 100644
index cfa996d..0000000
--- a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/VGA/VGA.java
+++ /dev/null
@@ -1,193 +0,0 @@
-package com.github._0x4248.nova.BIOS.VGA;
-
-import com.github._0x4248.nova.Core.Gui;
-
-import java.awt.image.BufferedImage;
-import java.util.Arrays;
-
-public class VGA {
-
-	public static final int MODE13H_WIDTH = 320;
-	public static final int MODE13H_HEIGHT = 200;
-
-	private final int width;
-	private final int height;
-	private final byte[] framebuffer;
-	private final int[] palette;
-
-	public VGA() {
-		this(MODE13H_WIDTH, MODE13H_HEIGHT);
-	}
-
-	public VGA(int width, int height) {
-		if (width <= 0 || height <= 0) {
-			throw new IllegalArgumentException("Width and height must be positive");
-		}
-
-		this.width = width;
-		this.height = height;
-		this.framebuffer = new byte[width * height];
-		this.palette = new int[256];
-
-		initializeDefaultPalette();
-	}
-
-	public int getWidth() {
-		return width;
-	}
-
-	public int getHeight() {
-		return height;
-	}
-
-	public void clear(int colorIndex) {
-		Arrays.fill(framebuffer, toPaletteIndex(colorIndex));
-	}
-
-	public void setPixel(int x, int y, int colorIndex) {
-		if (x < 0 || y < 0 || x >= width || y >= height) {
-			return;
-		}
-
-		framebuffer[y * width + x] = toPaletteIndex(colorIndex);
-	}
-
-	public int getPixel(int x, int y) {
-		if (x < 0 || y < 0 || x >= width || y >= height) {
-			return 0;
-		}
-
-		return framebuffer[y * width + x] & 0xFF;
-	}
-
-	public void drawChar(int x, int y, char character, int foregroundColor, int backgroundColor, boolean transparentBackground) {
-		int glyphIndex = character & 0x7F;
-		if (glyphIndex >= VGAFonts.LATIN8x8.length) {
-			glyphIndex = '?';
-		}
-
-		byte[] glyph = VGAFonts.LATIN8x8[glyphIndex];
-
-		for (int row = 0; row < 8; row++) {
-			int rowBits = glyph[row] & 0xFF;
-
-			for (int col = 0; col < 8; col++) {
-				boolean on = ((rowBits >> col) & 1) == 1;
-				if (on) {
-					setPixel(x + col, y + row, foregroundColor);
-				} else if (!transparentBackground) {
-					setPixel(x + col, y + row, backgroundColor);
-				}
-			}
-		}
-	}
-
-	public void drawText(int x, int y, String text, int foregroundColor, int backgroundColor, boolean transparentBackground) {
-		int cursorX = x;
-		int cursorY = y;
-
-		for (int i = 0; i < text.length(); i++) {
-			char character = text.charAt(i);
-
-			if (character == '\n') {
-				cursorX = x;
-				cursorY += 8;
-				continue;
-			}
-
-			drawChar(cursorX, cursorY, character, foregroundColor, backgroundColor, transparentBackground);
-			cursorX += 8;
-		}
-	}
-
-	public void setPaletteEntry(int index, int r, int g, int b) {
-		if (index < 0 || index > 255) {
-			return;
-		}
-
-		int red = clamp8(r);
-		int green = clamp8(g);
-		int blue = clamp8(b);
-		palette[index] = (red << 16) | (green << 8) | blue;
-	}
-
-	public int getPaletteEntry(int index) {
-		if (index < 0 || index > 255) {
-			return 0;
-		}
-
-		return palette[index];
-	}
-
-	public int[] toRgbBuffer() {
-		int[] rgb = new int[framebuffer.length];
-		for (int i = 0; i < framebuffer.length; i++) {
-			rgb[i] = palette[framebuffer[i] & 0xFF];
-		}
-		return rgb;
-	}
-
-	public BufferedImage toBufferedImage() {
-		BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
-		image.setRGB(0, 0, width, height, toRgbBuffer(), 0, width);
-		return image;
-	}
-
-	public void blitToGui(Gui gui, int scale) {
-		if (gui == null || scale <= 0) {
-			return;
-		}
-                try {
-            Thread.sleep(1);
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-        }
-		int[] rgb = toRgbBuffer();
-
-		for (int y = 0; y < height; y++) {
-			for (int x = 0; x < width; x++) {
-				int color = rgb[y * width + x];
-				int r = (color >> 16) & 0xFF;
-				int g = (color >> 8) & 0xFF;
-				int b = color & 0xFF;
-
-				int baseX = x * scale;
-				int baseY = y * scale;
-
-				for (int sy = 0; sy < scale; sy++) {
-					for (int sx = 0; sx < scale; sx++) {
-						gui.putPixel(baseX + sx, baseY + sy, r, g, b);
-					}
-				}
-			}
-		}
-	}
-
-	public void blitToGui(Gui gui) {
-		blitToGui(gui, 1);
-	}
-
-	private byte toPaletteIndex(int colorIndex) {
-		return (byte) (colorIndex & 0xFF);
-	}
-
-	private int clamp8(int value) {
-		return Math.max(0, Math.min(255, value));
-	}
-
-	private void initializeDefaultPalette() {
-		int[] ega16 = {
-				0x000000, 0x0000AA, 0x00AA00, 0x00AAAA,
-				0xAA0000, 0xAA00AA, 0xAA5500, 0xAAAAAA,
-				0x555555, 0x5555FF, 0x55FF55, 0x55FFFF,
-				0xFF5555, 0xFF55FF, 0xFFFF55, 0xFFFFFF
-		};
-
-		System.arraycopy(ega16, 0, palette, 0, ega16.length);
-
-		for (int i = 16; i < 256; i++) {
-			int gray = (int) Math.round(((i - 16) / 239.0) * 255.0);
-			palette[i] = (gray << 16) | (gray << 8) | gray;
-		}
-	}
-}
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/machines/Machine.java b/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/machines/Machine.java
deleted file mode 100644
index 7113e3a..0000000
--- a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/machines/Machine.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.github._0x4248.nova.BIOS.machines;
-
-public class Machine {
-
-    public final String id;
-    public final String biosLabel;
-    public final int screenScale;
-    public final boolean supportsSound;
-    public final boolean supportsKeyboard;
-
-    public Machine(String id, String biosLabel, int screenScale, boolean supportsSound, boolean supportsKeyboard) {
-        this.id = id;
-        this.biosLabel = biosLabel;
-        this.screenScale = screenScale;
-        this.supportsSound = supportsSound;
-        this.supportsKeyboard = supportsKeyboard;
-    }
-}
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/machines/StandardMachine.java b/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/machines/StandardMachine.java
deleted file mode 100644
index 3b327d1..0000000
--- a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/machines/StandardMachine.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.github._0x4248.nova.BIOS.machines;
-
-public class StandardMachine extends Machine {
-
-    public StandardMachine() {
-        super("STANDARD", "NOVA BIOS v0.1", 4, true, true);
-    }
-}
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Core/Gui.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Core/Gui.java
index 3a58aff..26543d9 100644
--- a/lab/nova/src/main/java/com/github/_0x4248/nova/Core/Gui.java
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Core/Gui.java
@@ -7,6 +7,7 @@ import java.awt.event.*;
 
 public class Gui extends JPanel implements KeyListener {
 
+    private final JFrame frame;
     private final BufferedImage framebuffer;
     private final int framebufferWidth;
     private final int framebufferHeight;
@@ -20,7 +21,7 @@ public class Gui extends JPanel implements KeyListener {
         keyboard = new Keyboard();
         sound = new Sound();
 
-        JFrame frame = new JFrame("NovaEngine");
+        frame = new JFrame("NovaEngine");
         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
         frame.setSize(width, height);
         frame.add(this);
@@ -68,6 +69,11 @@ public class Gui extends JPanel implements KeyListener {
         sound.biosBeep(lengthMs, pitchHz);
     }
 
+    public void close() {
+        frame.setVisible(false);
+        frame.dispose();
+    }
+
     @Override
     public void keyTyped(KeyEvent e) {
     }
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/audio/Speaker.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/audio/Speaker.java
new file mode 100644
index 0000000..bbeb90a
--- /dev/null
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/audio/Speaker.java
@@ -0,0 +1,22 @@
+package com.github._0x4248.nova.Machine.audio;
+
+import com.github._0x4248.nova.Core.Sound;
+import com.github._0x4248.nova.Machine.core.Hardware;
+
+public class Speaker implements Hardware {
+
+    private final Sound sound;
+
+    public Speaker() {
+        this.sound = new Sound();
+    }
+
+    @Override
+    public String id() {
+        return "speaker";
+    }
+
+    public void beep(int lengthMs, int pitchHz) {
+        sound.biosBeep(lengthMs, pitchHz);
+    }
+}
\ No newline at end of file
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Hardware.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Hardware.java
new file mode 100644
index 0000000..7ecfc22
--- /dev/null
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Hardware.java
@@ -0,0 +1,12 @@
+package com.github._0x4248.nova.Machine.core;
+
+public interface Hardware {
+
+    String id();
+
+    default void onAttach(Machine machine) {
+    }
+
+    default void onDetach(Machine machine) {
+    }
+}
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Keyboard.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Keyboard.java
new file mode 100644
index 0000000..0a1375d
--- /dev/null
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Keyboard.java
@@ -0,0 +1,23 @@
+package com.github._0x4248.nova.Machine.core;
+
+public class Keyboard implements Hardware {
+
+    private final Video video;
+
+    public Keyboard(Video video) {
+        this.video = video;
+    }
+
+    @Override
+    public String id() {
+        return "keyboard";
+    }
+
+    public boolean hasKeyPress() {
+        return video.gpu.hasKeyPress();
+    }
+
+    public Integer pollKeyCode() {
+        return video.gpu.pollKeyCode();
+    }
+}
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Machine.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Machine.java
new file mode 100644
index 0000000..18ea386
--- /dev/null
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Machine.java
@@ -0,0 +1,101 @@
+package com.github._0x4248.nova.Machine.core;
+
+import com.github._0x4248.nova.Machine.audio.Speaker;
+import com.github._0x4248.nova.Machine.floppy.Floppy;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class Machine {
+
+    private final Map<String, Hardware> hardware;
+
+    public Machine() {
+        this.hardware = new LinkedHashMap<>();
+        Video video = new Video();
+        attach(video);
+        attach(new Floppy());
+        attach(new Speaker());
+        attach(new Keyboard(video));
+    }
+
+    public static Machine basic() {
+        return new Machine();
+    }
+
+    public static Machine empty() {
+        return new Machine(false);
+    }
+
+    private Machine(boolean withDefaults) {
+        this.hardware = new LinkedHashMap<>();
+        if (withDefaults) {
+            Video video = new Video();
+            attach(video);
+            attach(new Floppy());
+            attach(new Speaker());
+            attach(new Keyboard(video));
+        }
+    }
+
+    public Machine attach(Hardware device) {
+        if (device == null) {
+            throw new IllegalArgumentException("device cannot be null");
+        }
+
+        Hardware previous = hardware.put(device.id(), device);
+        if (previous != null) {
+            previous.onDetach(this);
+        }
+        device.onAttach(this);
+        System.out.println("Attached hardware: " + device.id());
+        return this;
+    }
+
+    public <T extends Hardware> T get(String id, Class<T> type) {
+        Hardware device = hardware.get(id);
+        if (device == null) {
+            return null;
+        }
+        if (!type.isInstance(device)) {
+            throw new IllegalStateException("Hardware '" + id + "' is not a " + type.getSimpleName());
+        }
+        return type.cast(device);
+    }
+
+    public boolean has(String id) {
+        return hardware.containsKey(id);
+    }
+
+    public Machine detach(String id) {
+        Hardware removed = hardware.remove(id);
+        if (removed != null) {
+            removed.onDetach(this);
+        }
+        return this;
+    }
+
+    private <T extends Hardware> T require(String id, Class<T> type) {
+        T device = get(id, type);
+        if (device == null) {
+            throw new IllegalStateException("Missing required hardware: " + id);
+        }
+        return device;
+    }
+
+    public Video video() {
+        return require("video", Video.class);
+    }
+
+    public Floppy floppy() {
+        return require("floppy", Floppy.class);
+    }
+
+    public Speaker speaker() {
+        return require("speaker", Speaker.class);
+    }
+
+    public Keyboard keyboard() {
+        return require("keyboard", Keyboard.class);
+    }
+}
\ No newline at end of file
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/MachineProgram.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/MachineProgram.java
new file mode 100644
index 0000000..7c2fe91
--- /dev/null
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/MachineProgram.java
@@ -0,0 +1,6 @@
+package com.github._0x4248.nova.Machine.core;
+
+public interface MachineProgram {
+
+    void run(Machine machine, String[] args);
+}
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Video.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Video.java
new file mode 100644
index 0000000..df80962
--- /dev/null
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Video.java
@@ -0,0 +1,54 @@
+package com.github._0x4248.nova.Machine.core;
+
+import com.github._0x4248.nova.Machine.gpu.GPU;
+import com.github._0x4248.nova.Machine.gpu.GpuAdapter;
+import com.github._0x4248.nova.Machine.gpu.GpuOutputMode;
+import com.github._0x4248.nova.Machine.gpu.VideoMode;
+
+public class Video implements Hardware {
+
+    public final GPU gpu;
+    public final Modes modes;
+
+    public static final VideoMode ModesCGA = Modes.CGA_GRAPHICS_320x200;
+    public static final VideoMode ModesVGA = Modes.VGA_GRAPHICS_640x480_16;
+
+    public Video() {
+        this.modes = new Modes();
+        this.gpu = new GPU().init(ModesVGA);
+    }
+
+    @Override
+    public String id() {
+        return "video";
+    }
+
+    public static class Modes {
+        public static final VideoMode CGA_TEXT_40x25 = VideoMode.of(
+                "CGA_TEXT_40x25", GpuAdapter.CGA, GpuOutputMode.TEXT, 320, 200, 3, 16, false
+        );
+
+        public static final VideoMode CGA_GRAPHICS_320x200 = VideoMode.of(
+                "CGA_GRAPHICS_320x200", GpuAdapter.CGA, GpuOutputMode.VIDEO, 320, 200, 3, 4, false
+        );
+
+        public static final VideoMode VGA_TEXT_80x25 = VideoMode.of(
+                "VGA_TEXT_80x25", GpuAdapter.VGA, GpuOutputMode.TEXT, 640, 400, 2, 16, false
+        );
+
+        public static final VideoMode VGA_GRAPHICS_640x480_16 = VideoMode.of(
+                "VGA_GRAPHICS_640x480_16", GpuAdapter.VGA, GpuOutputMode.VIDEO, 640, 480, 2, 16, false
+        );
+
+        public static final VideoMode VGA_GRAPHICS_320x200_256 = VideoMode.of(
+                "VGA_GRAPHICS_320x200_256", GpuAdapter.VGA, GpuOutputMode.VIDEO, 320, 200, 4, 256, true
+        );
+
+        public final VideoMode CGA_TEXT = CGA_TEXT_40x25;
+        public final VideoMode CGA = CGA_GRAPHICS_320x200;
+
+        public final VideoMode VGA_TEXT = VGA_TEXT_80x25;
+        public final VideoMode VGA = VGA_GRAPHICS_640x480_16;
+        public final VideoMode VGA_256 = VGA_GRAPHICS_320x200_256;
+    }
+}
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/floppy/Floppy.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/floppy/Floppy.java
new file mode 100644
index 0000000..29be9ed
--- /dev/null
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/floppy/Floppy.java
@@ -0,0 +1,18 @@
+package com.github._0x4248.nova.Machine.floppy;
+
+import com.github._0x4248.nova.Machine.core.Hardware;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class Floppy implements Hardware {
+
+    @Override
+    public String id() {
+        return "floppy";
+    }
+
+    public boolean isInserted(Path mediumPath) {
+        return mediumPath != null && Files.exists(mediumPath) && Files.isRegularFile(mediumPath);
+    }
+}
\ No newline at end of file
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/GPU.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/GPU.java
new file mode 100644
index 0000000..db0def3
--- /dev/null
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/GPU.java
@@ -0,0 +1,146 @@
+package com.github._0x4248.nova.Machine.gpu;
+
+import com.github._0x4248.nova.Core.Gui;
+import com.github._0x4248.nova.Machine.gpu.drivers.CgaDriver;
+import com.github._0x4248.nova.Machine.gpu.drivers.VgaDriver;
+import com.github._0x4248.nova.Machine.gpu.drivers.VideoDriver;
+
+public class GPU {
+
+    private VideoDriver driver;
+    private Gui screen;
+    private int scale;
+    private VideoMode mode;
+
+    public GPU init(VideoMode mode) {
+        if (mode == null) {
+            throw new IllegalArgumentException("mode cannot be null");
+        }
+
+        closeOutput();
+
+        this.driver = createDriver(mode);
+        this.scale = mode.scale();
+        this.mode = mode;
+        this.screen = new Gui(driver.getWidth() * scale, driver.getHeight() * scale);
+        return this;
+    }
+
+    public VideoMode getMode() {
+        ensureInitialized();
+        return mode;
+    }
+
+    public String getModeName() {
+        return getMode().name();
+    }
+
+    public GpuOutputMode getOutputMode() {
+        ensureInitialized();
+        return mode.outputMode();
+    }
+
+    public int getColorCount() {
+        ensureInitialized();
+        return mode.colors();
+    }
+
+    public int getWidth() {
+        ensureInitialized();
+        return driver.getWidth();
+    }
+
+    public int getHeight() {
+        ensureInitialized();
+        return driver.getHeight();
+    }
+
+    public int getTextColumns() {
+        return getWidth() / 8;
+    }
+
+    public void clear(int colorIndex) {
+        ensureInitialized();
+        driver.clear(colorIndex);
+    }
+
+    public void drawText(int x, int y, String text, int foregroundColor, int backgroundColor, boolean transparentBackground) {
+        ensureInitialized();
+        driver.drawText(x, y, text, foregroundColor, backgroundColor, transparentBackground);
+    }
+
+    public void setPixel(int x, int y, int colorIndex) {
+        ensureInitialized();
+        driver.setPixel(x, y, colorIndex);
+    }
+
+    public void setPaletteEntry(int index, int r, int g, int b) {
+        ensureInitialized();
+        driver.setPaletteEntry(index, r, g, b);
+    }
+
+    public void present() {
+        ensureInitialized();
+
+        int[] rgb = driver.toRgbBuffer();
+        int width = driver.getWidth();
+        int height = driver.getHeight();
+
+        for (int y = 0; y < height; y++) {
+            for (int x = 0; x < width; x++) {
+                int color = rgb[y * width + x];
+                int r = (color >> 16) & 0xFF;
+                int g = (color >> 8) & 0xFF;
+                int b = color & 0xFF;
+
+                int baseX = x * scale;
+                int baseY = y * scale;
+
+                for (int sy = 0; sy < scale; sy++) {
+                    for (int sx = 0; sx < scale; sx++) {
+                        screen.putPixel(baseX + sx, baseY + sy, r, g, b);
+                    }
+                }
+            }
+        }
+    }
+
+    public boolean hasKeyPress() {
+        ensureInitialized();
+        return screen.hasKeyPress();
+    }
+
+    public Integer pollKeyCode() {
+        ensureInitialized();
+        return screen.pollKeyCode();
+    }
+
+    public void shutdown() {
+        closeOutput();
+        this.driver = null;
+        this.mode = null;
+    }
+
+    private VideoDriver createDriver(VideoMode mode) {
+        if (mode.adapter() == GpuAdapter.CGA) {
+            return new CgaDriver(mode);
+        }
+        if (mode.adapter() == GpuAdapter.VGA) {
+            return new VgaDriver(mode);
+        }
+        throw new IllegalStateException("Unsupported adapter: " + mode.adapter());
+    }
+
+    private void ensureInitialized() {
+        if (driver == null || screen == null || mode == null) {
+            throw new IllegalStateException("GPU not initialized. Call init(mode) first.");
+        }
+    }
+
+    private void closeOutput() {
+        if (screen != null) {
+            screen.close();
+            screen = null;
+        }
+    }
+}
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/GpuAdapter.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/GpuAdapter.java
new file mode 100644
index 0000000..652f673
--- /dev/null
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/GpuAdapter.java
@@ -0,0 +1,6 @@
+package com.github._0x4248.nova.Machine.gpu;
+
+public enum GpuAdapter {
+    CGA,
+    VGA
+}
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/GpuOutputMode.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/GpuOutputMode.java
new file mode 100644
index 0000000..4ec9928
--- /dev/null
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/GpuOutputMode.java
@@ -0,0 +1,6 @@
+package com.github._0x4248.nova.Machine.gpu;
+
+public enum GpuOutputMode {
+    TEXT,
+    VIDEO
+}
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/VideoMode.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/VideoMode.java
new file mode 100644
index 0000000..0f2041f
--- /dev/null
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/VideoMode.java
@@ -0,0 +1,106 @@
+package com.github._0x4248.nova.Machine.gpu;
+
+public final class VideoMode {
+
+    private final String name;
+    private final GpuAdapter adapter;
+    private final GpuOutputMode outputMode;
+    private final int width;
+    private final int height;
+    private final int scale;
+    private final int colors;
+    private final boolean paletteChangeSupported;
+
+    private VideoMode(
+            String name,
+            GpuAdapter adapter,
+            GpuOutputMode outputMode,
+            int width,
+            int height,
+            int scale,
+            int colors,
+            boolean paletteChangeSupported
+    ) {
+        if (name == null || name.isBlank()) {
+            throw new IllegalArgumentException("name cannot be blank");
+        }
+        if (adapter == null) {
+            throw new IllegalArgumentException("adapter cannot be null");
+        }
+        if (outputMode == null) {
+            throw new IllegalArgumentException("outputMode cannot be null");
+        }
+        if (width <= 0 || height <= 0) {
+            throw new IllegalArgumentException("width and height must be positive");
+        }
+        if (scale <= 0) {
+            throw new IllegalArgumentException("scale must be positive");
+        }
+        if (colors <= 0 || colors > 256) {
+            throw new IllegalArgumentException("colors must be between 1 and 256");
+        }
+
+        this.name = name;
+        this.adapter = adapter;
+        this.outputMode = outputMode;
+        this.width = width;
+        this.height = height;
+        this.scale = scale;
+        this.colors = colors;
+        this.paletteChangeSupported = paletteChangeSupported;
+    }
+
+    public static VideoMode of(
+            String name,
+            GpuAdapter adapter,
+            GpuOutputMode outputMode,
+            int width,
+            int height,
+            int scale,
+            int colors,
+            boolean paletteChangeSupported
+    ) {
+        return new VideoMode(name, adapter, outputMode, width, height, scale, colors, paletteChangeSupported);
+    }
+
+    public String name() {
+        return name;
+    }
+
+    public GpuAdapter adapter() {
+        return adapter;
+    }
+
+    public GpuOutputMode outputMode() {
+        return outputMode;
+    }
+
+    public int width() {
+        return width;
+    }
+
+    public int height() {
+        return height;
+    }
+
+    public int scale() {
+        return scale;
+    }
+
+    public int colors() {
+        return colors;
+    }
+
+    public boolean supportsPaletteChanges() {
+        return paletteChangeSupported;
+    }
+
+    public boolean supportsPixels() {
+        return outputMode == GpuOutputMode.VIDEO;
+    }
+
+    @Override
+    public String toString() {
+        return adapter + " " + outputMode + " " + width + "x" + height + " (" + colors + " colors)";
+    }
+}
\ No newline at end of file
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/drivers/CgaDriver.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/drivers/CgaDriver.java
new file mode 100644
index 0000000..9b8cf0f
--- /dev/null
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/drivers/CgaDriver.java
@@ -0,0 +1,138 @@
+package com.github._0x4248.nova.Machine.gpu.drivers;
+
+import com.github._0x4248.nova.Machine.gpu.GpuAdapter;
+import com.github._0x4248.nova.Machine.gpu.GpuOutputMode;
+import com.github._0x4248.nova.Machine.gpu.VideoMode;
+import com.github._0x4248.nova.Machine.gpu.fonts.VgaFonts;
+
+import java.util.Arrays;
+
+public class CgaDriver implements VideoDriver {
+
+    private final VideoMode mode;
+    private final byte[] framebuffer;
+    private final int[] palette;
+
+    public CgaDriver(VideoMode mode) {
+        if (mode.adapter() != GpuAdapter.CGA) {
+            throw new IllegalArgumentException("CgaDriver requires a CGA mode");
+        }
+        this.mode = mode;
+        this.framebuffer = new byte[mode.width() * mode.height()];
+        this.palette = new int[16];
+        initializePalette();
+    }
+
+    @Override
+    public int getWidth() {
+        return mode.width();
+    }
+
+    @Override
+    public int getHeight() {
+        return mode.height();
+    }
+
+    @Override
+    public int getColorCount() {
+        return mode.colors();
+    }
+
+    @Override
+    public GpuOutputMode getOutputMode() {
+        return mode.outputMode();
+    }
+
+    @Override
+    public void clear(int colorIndex) {
+        Arrays.fill(framebuffer, (byte) sanitizeColor(colorIndex));
+    }
+
+    @Override
+    public void drawText(int x, int y, String text, int foregroundColor, int backgroundColor, boolean transparentBackground) {
+        int cursorX = x;
+        int cursorY = y;
+
+        for (int i = 0; i < text.length(); i++) {
+            char character = text.charAt(i);
+            if (character == '\n') {
+                cursorX = x;
+                cursorY += 8;
+                continue;
+            }
+
+            drawChar(cursorX, cursorY, character, foregroundColor, backgroundColor, transparentBackground);
+            cursorX += 8;
+        }
+    }
+
+    @Override
+    public void setPixel(int x, int y, int colorIndex) {
+        if (mode.outputMode() != GpuOutputMode.VIDEO) {
+            throw new IllegalStateException("Current mode is TEXT. Switch to a VIDEO mode to use pixels.");
+        }
+        if (x < 0 || y < 0 || x >= mode.width() || y >= mode.height()) {
+            return;
+        }
+        framebuffer[y * mode.width() + x] = (byte) sanitizeColor(colorIndex);
+    }
+
+    @Override
+    public void setPaletteEntry(int index, int r, int g, int b) {
+        throw new IllegalStateException("CGA palette is fixed in this driver");
+    }
+
+    @Override
+    public int[] toRgbBuffer() {
+        int[] rgb = new int[framebuffer.length];
+        for (int i = 0; i < framebuffer.length; i++) {
+            rgb[i] = palette[framebuffer[i] & 0x0F];
+        }
+        return rgb;
+    }
+
+    private void drawChar(int x, int y, char character, int foregroundColor, int backgroundColor, boolean transparentBackground) {
+        int glyphIndex = character & 0x7F;
+        if (glyphIndex >= VgaFonts.LATIN8x8.length) {
+            glyphIndex = '?';
+        }
+
+        byte[] glyph = VgaFonts.LATIN8x8[glyphIndex];
+        int foreground = sanitizeColor(foregroundColor);
+        int background = sanitizeColor(backgroundColor);
+
+        for (int row = 0; row < 8; row++) {
+            int rowBits = glyph[row] & 0xFF;
+            for (int col = 0; col < 8; col++) {
+                boolean on = ((rowBits >> col) & 1) == 1;
+                if (on) {
+                    writePixel(x + col, y + row, foreground);
+                } else if (!transparentBackground) {
+                    writePixel(x + col, y + row, background);
+                }
+            }
+        }
+    }
+
+    private void writePixel(int x, int y, int colorIndex) {
+        if (x < 0 || y < 0 || x >= mode.width() || y >= mode.height()) {
+            return;
+        }
+        framebuffer[y * mode.width() + x] = (byte) sanitizeColor(colorIndex);
+    }
+
+    private int sanitizeColor(int colorIndex) {
+        return Math.floorMod(colorIndex, mode.colors());
+    }
+
+    private void initializePalette() {
+        int[] cga16 = {
+                0x000000, 0x0000AA, 0x00AA00, 0x00AAAA,
+                0xAA0000, 0xAA00AA, 0xAA5500, 0xAAAAAA,
+                0x555555, 0x5555FF, 0x55FF55, 0x55FFFF,
+                0xFF5555, 0xFF55FF, 0xFFFF55, 0xFFFFFF
+        };
+
+        System.arraycopy(cga16, 0, palette, 0, cga16.length);
+    }
+}
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/drivers/VgaDriver.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/drivers/VgaDriver.java
new file mode 100644
index 0000000..effc264
--- /dev/null
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/drivers/VgaDriver.java
@@ -0,0 +1,156 @@
+package com.github._0x4248.nova.Machine.gpu.drivers;
+
+import com.github._0x4248.nova.Machine.gpu.GpuAdapter;
+import com.github._0x4248.nova.Machine.gpu.GpuOutputMode;
+import com.github._0x4248.nova.Machine.gpu.VideoMode;
+import com.github._0x4248.nova.Machine.gpu.fonts.VgaFonts;
+
+import java.util.Arrays;
+
+public class VgaDriver implements VideoDriver {
+
+    private final VideoMode mode;
+    private final byte[] framebuffer;
+    private final int[] palette;
+
+    public VgaDriver(VideoMode mode) {
+        if (mode.adapter() != GpuAdapter.VGA) {
+            throw new IllegalArgumentException("VgaDriver requires a VGA mode");
+        }
+        this.mode = mode;
+        this.framebuffer = new byte[mode.width() * mode.height()];
+        this.palette = new int[256];
+        initializePalette();
+    }
+
+    @Override
+    public int getWidth() {
+        return mode.width();
+    }
+
+    @Override
+    public int getHeight() {
+        return mode.height();
+    }
+
+    @Override
+    public int getColorCount() {
+        return mode.colors();
+    }
+
+    @Override
+    public GpuOutputMode getOutputMode() {
+        return mode.outputMode();
+    }
+
+    @Override
+    public void clear(int colorIndex) {
+        Arrays.fill(framebuffer, (byte) sanitizeColor(colorIndex));
+    }
+
+    @Override
+    public void drawText(int x, int y, String text, int foregroundColor, int backgroundColor, boolean transparentBackground) {
+        int cursorX = x;
+        int cursorY = y;
+
+        for (int i = 0; i < text.length(); i++) {
+            char character = text.charAt(i);
+            if (character == '\n') {
+                cursorX = x;
+                cursorY += 8;
+                continue;
+            }
+
+            drawChar(cursorX, cursorY, character, foregroundColor, backgroundColor, transparentBackground);
+            cursorX += 8;
+        }
+    }
+
+    @Override
+    public void setPixel(int x, int y, int colorIndex) {
+        if (mode.outputMode() != GpuOutputMode.VIDEO) {
+            throw new IllegalStateException("Current mode is TEXT. Switch to a VIDEO mode to use pixels.");
+        }
+        if (x < 0 || y < 0 || x >= mode.width() || y >= mode.height()) {
+            return;
+        }
+        framebuffer[y * mode.width() + x] = (byte) sanitizeColor(colorIndex);
+    }
+
+    @Override
+    public void setPaletteEntry(int index, int r, int g, int b) {
+        if (!mode.supportsPaletteChanges()) {
+            throw new IllegalStateException("Palette changes are not supported in mode " + mode.name());
+        }
+        if (index < 0 || index >= mode.colors()) {
+            return;
+        }
+        int red = clamp8(r);
+        int green = clamp8(g);
+        int blue = clamp8(b);
+        palette[index] = (red << 16) | (green << 8) | blue;
+    }
+
+    @Override
+    public int[] toRgbBuffer() {
+        int[] rgb = new int[framebuffer.length];
+        for (int i = 0; i < framebuffer.length; i++) {
+            rgb[i] = palette[framebuffer[i] & 0xFF];
+        }
+        return rgb;
+    }
+
+    private void drawChar(int x, int y, char character, int foregroundColor, int backgroundColor, boolean transparentBackground) {
+        int glyphIndex = character & 0x7F;
+        if (glyphIndex >= VgaFonts.LATIN8x8.length) {
+            glyphIndex = '?';
+        }
+
+        byte[] glyph = VgaFonts.LATIN8x8[glyphIndex];
+        int foreground = sanitizeColor(foregroundColor);
+        int background = sanitizeColor(backgroundColor);
+
+        for (int row = 0; row < 8; row++) {
+            int rowBits = glyph[row] & 0xFF;
+            for (int col = 0; col < 8; col++) {
+                boolean on = ((rowBits >> col) & 1) == 1;
+                if (on) {
+                    writePixel(x + col, y + row, foreground);
+                } else if (!transparentBackground) {
+                    writePixel(x + col, y + row, background);
+                }
+            }
+        }
+    }
+
+    private void writePixel(int x, int y, int colorIndex) {
+        if (x < 0 || y < 0 || x >= mode.width() || y >= mode.height()) {
+            return;
+        }
+        framebuffer[y * mode.width() + x] = (byte) sanitizeColor(colorIndex);
+    }
+
+    private int sanitizeColor(int colorIndex) {
+        return Math.floorMod(colorIndex, mode.colors());
+    }
+
+    private int clamp8(int value) {
+        return Math.max(0, Math.min(255, value));
+    }
+
+    private void initializePalette() {
+        int[] ega16 = {
+                0x000000, 0x0000AA, 0x00AA00, 0x00AAAA,
+                0xAA0000, 0xAA00AA, 0xAA5500, 0xAAAAAA,
+                0x555555, 0x5555FF, 0x55FF55, 0x55FFFF,
+                0xFF5555, 0xFF55FF, 0xFFFF55, 0xFFFFFF
+        };
+
+        System.arraycopy(ega16, 0, palette, 0, ega16.length);
+
+        for (int i = 16; i < 256; i++) {
+            int gray = (int) Math.round(((i - 16) / 239.0) * 255.0);
+            palette[i] = (gray << 16) | (gray << 8) | gray;
+        }
+    }
+}
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/drivers/VideoDriver.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/drivers/VideoDriver.java
new file mode 100644
index 0000000..4983187
--- /dev/null
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/drivers/VideoDriver.java
@@ -0,0 +1,24 @@
+package com.github._0x4248.nova.Machine.gpu.drivers;
+
+import com.github._0x4248.nova.Machine.gpu.GpuOutputMode;
+
+public interface VideoDriver {
+
+    int getWidth();
+
+    int getHeight();
+
+    int getColorCount();
+
+    GpuOutputMode getOutputMode();
+
+    void clear(int colorIndex);
+
+    void drawText(int x, int y, String text, int foregroundColor, int backgroundColor, boolean transparentBackground);
+
+    void setPixel(int x, int y, int colorIndex);
+
+    void setPaletteEntry(int index, int r, int g, int b);
+
+    int[] toRgbBuffer();
+}
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/VGA/VGAFonts.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/fonts/VgaFonts.java
similarity index 99%
rename from lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/VGA/VGAFonts.java
rename to lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/fonts/VgaFonts.java
index de366a8..ad4890c 100644
--- a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/VGA/VGAFonts.java
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/fonts/VgaFonts.java
@@ -1,6 +1,6 @@
-package com.github._0x4248.nova.BIOS.VGA;
+package com.github._0x4248.nova.Machine.gpu.fonts;
 
-public class VGAFonts {
+public class VgaFonts {
     public static final byte[][] LATIN8x8 = {
     { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0000 (nul)
     { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0001
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Main.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Main.java
index 56f99e6..cbc403b 100644
--- a/lab/nova/src/main/java/com/github/_0x4248/nova/Main.java
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Main.java
@@ -1,27 +1,46 @@
 package com.github._0x4248.nova;
 
-import com.github._0x4248.nova.BIOS.BIOS;
-import com.github._0x4248.nova.BIOS.machines.StandardMachine;
+import com.github._0x4248.nova.Machine.core.Machine;
+import com.github._0x4248.nova.Machine.core.MachineProgram;
 
-import java.nio.file.Path;
-import java.nio.file.Paths;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
 
 public class Main {
-    private static final String DEFAULT_INTERNAL_APP = "com.github._0x4248.nova_examples.ExampleXYZ";
+    private static final String DEFAULT_INTERNAL_APP = "com.github._0x4248.nova_examples.NovaBasic";
 
     public static void main(String[] args) {
-        BIOS bios = new BIOS(new StandardMachine());
-        Path applicationJar = Paths.get("Application.jar");
-
-        boolean booted;
-        if (java.nio.file.Files.exists(applicationJar)) {
-            booted = bios.bootApplication(applicationJar, args);
-        } else {
-            booted = bios.bootInternalApplication(DEFAULT_INTERNAL_APP, args);
-        }
+        String targetProgram = args.length > 0 ? args[0] : DEFAULT_INTERNAL_APP;
+        String[] programArgs = args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0];
 
-        if (!booted) {
+        Machine machine = Machine.basic();
+        if (!runProgram(targetProgram, machine, programArgs)) {
             System.exit(1);
         }
     }
+
+    private static boolean runProgram(String className, Machine machine, String[] args) {
+        try {
+            Class<?> programClass = Class.forName(className);
+
+            if (MachineProgram.class.isAssignableFrom(programClass)) {
+                MachineProgram program = (MachineProgram) programClass.getDeclaredConstructor().newInstance();
+                program.run(machine, args == null ? new String[0] : args);
+                return true;
+            }
+
+            Method mainMethod = programClass.getMethod("main", String[].class);
+            int modifiers = mainMethod.getModifiers();
+            if (!Modifier.isPublic(modifiers) || !Modifier.isStatic(modifiers)) {
+                throw new IllegalStateException("main(String[]) must be public static");
+            }
+
+            mainMethod.invoke(null, (Object) (args == null ? new String[0] : args));
+            return true;
+        } catch (Exception e) {
+            System.err.println("Failed to run program '" + className + "': " + e.getMessage());
+            return false;
+        }
+    }
 }
\ No newline at end of file
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova_examples/ExampleXYZ.java b/lab/nova/src/main/java/com/github/_0x4248/nova_examples/ExampleXYZ.java
index b15add9..26ba9e9 100644
--- a/lab/nova/src/main/java/com/github/_0x4248/nova_examples/ExampleXYZ.java
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova_examples/ExampleXYZ.java
@@ -1,49 +1,71 @@
 package com.github._0x4248.nova_examples;
 
-import com.github._0x4248.nova.BIOS.BIOS;
-import com.github._0x4248.nova.BIOS.BiosRuntime;
-import com.github._0x4248.nova.BIOS.machines.StandardMachine;
+import com.github._0x4248.nova.Machine.core.Keyboard;
+import com.github._0x4248.nova.Machine.core.Machine;
+import com.github._0x4248.nova.Machine.core.MachineProgram;
+import com.github._0x4248.nova.Machine.core.Video;
+import com.github._0x4248.nova.Machine.gpu.GPU;
 
-public class ExampleXYZ {
-    public static final int BOOTFLAG = 1;
+import java.awt.event.KeyEvent;
+
+public class ExampleXYZ implements MachineProgram {
 
     public static void main(String[] args) {
-        System.out.println("Launching Example XYZ...");
-        BiosRuntime bios = BIOS.getRuntime();
-        if (bios == null) {
-            bios = new BiosRuntime(new StandardMachine());
-        }
+        new ExampleXYZ().run(Machine.basic(), args);
+    }
+
+    @Override
+    public void run(Machine machine, String[] args) {
+        GPU gpu = machine.video().gpu;
+        Keyboard keyboard = machine.keyboard();
 
-        bios.clear(1);
-        bios.drawText(24, 32, "HELLO WORLD", 15, 1, false);
-        bios.drawText(24, 48, "NOVA EXAMPLE XYZ", 14, 1, false);
+        gpu.init(Video.Modes.CGA_TEXT_40x25);
+        gpu.clear(0);
+        gpu.drawText(8, 8, "NOVA TEXT MODE", 15, 0, false);
+        gpu.drawText(8, 20, "Press ENTER for VGA graphics", 14, 0, false);
+        gpu.drawText(8, 32, "ESC exits", 10, 0, false);
+        gpu.present();
 
-        if (!bios.getMachine().supportsSound) {
-            bios.drawText(24, 64, "NO SOUND DEVICE", 12, 1, false);
+        while (true) {
+            Integer keyCode = keyboard.pollKeyCode();
+            if (keyCode == null) {
+                sleep(10);
+                continue;
+            }
+            if (keyCode == KeyEvent.VK_ESCAPE) {
+                break;
+            }
+            if (keyCode == KeyEvent.VK_ENTER) {
+                break;
+            }
         }
 
-        bios.present();
-        try{
-            Thread.sleep(2500);
-        } catch (InterruptedException e) {
-            e.printStackTrace();
+        gpu.init(Video.Modes.VGA_GRAPHICS_640x480_16);
+        gpu.clear(1);
+        gpu.drawText(8, 8, "VGA VIDEO MODE", 15, 1, false);
+
+        for (int x = 0; x < gpu.getWidth(); x++) {
+            int y = (int) (gpu.getHeight() * 0.5 + Math.sin(x / 20.0) * 50);
+            gpu.setPixel(x, y, 14);
         }
+        gpu.present();
+
         while (true) {
-                bios.clear(0);
-                bios.beep(200, 440);
-                bios.present();
-                try {
-                    Thread.sleep(1000);
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
-                bios.drawText(0, 0, "This is just an example", 15, 0, false);
-                bios.present();
-                try {
-                    Thread.sleep(1000);
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
+            Integer keyCode = keyboard.pollKeyCode();
+            if (keyCode != null && keyCode == KeyEvent.VK_ESCAPE) {
+                break;
+            }
+            sleep(10);
+        }
+
+        gpu.shutdown();
+    }
+
+    private void sleep(long millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
         }
     }
 }
diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova_examples/NovaBasic.java b/lab/nova/src/main/java/com/github/_0x4248/nova_examples/NovaBasic.java
new file mode 100644
index 0000000..249f6f6
--- /dev/null
+++ b/lab/nova/src/main/java/com/github/_0x4248/nova_examples/NovaBasic.java
@@ -0,0 +1,682 @@
+package com.github._0x4248.nova_examples;
+
+import com.github._0x4248.nova.Machine.core.Machine;
+import com.github._0x4248.nova.Machine.core.MachineProgram;
+import com.github._0x4248.nova.Machine.core.Keyboard;
+import com.github._0x4248.nova.Machine.core.Video;
+import com.github._0x4248.nova.Machine.gpu.GPU;
+
+import java.awt.event.KeyEvent;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.TreeMap;
+
+public class NovaBasic implements MachineProgram {
+
+    private static final int VIDEO_COLUMNS = 40;
+    private static final int VIDEO_ROWS = 25;
+
+    private final TreeMap<Integer, String> program = new TreeMap<>();
+    private final Map<String, Integer> variables = new HashMap<>();
+    private final List<String> screenLines = new ArrayList<>();
+    private GPU gpu;
+    private Keyboard keyboard;
+    private String currentInput = "";
+
+    public static void main(String[] args) {
+        new NovaBasic().run(Machine.basic(), args);
+    }
+
+    @Override
+    public void run(Machine machine, String[] args) {
+        initVideoOut(machine);
+        initKeyboardIn(machine);
+
+        outLine("NOVA BASIC V0.1");
+        outLine("READY.");
+        outLine("Type HELP for commands.");
+
+        if (gpu != null && keyboard != null) {
+            runKeyboardLoop();
+            if (gpu != null) {
+                gpu.shutdown();
+            }
+            return;
+        }
+
+        try (Scanner scanner = new Scanner(System.in)) {
+            while (true) {
+                System.out.print("> ");
+                if (!scanner.hasNextLine()) {
+                    break;
+                }
+
+                String line = scanner.nextLine().trim();
+                appendScreenLine("> " + line);
+                if (line.isEmpty()) {
+                    continue;
+                }
+
+                if (processInputLine(line)) {
+                    break;
+                }
+            }
+        }
+
+        if (gpu != null) {
+            gpu.shutdown();
+        }
+    }
+
+    private void runKeyboardLoop() {
+        while (true) {
+            String line = readLineFromKeyboard();
+            if (line == null) {
+                continue;
+            }
+
+            String trimmed = line.trim();
+            if (trimmed.isEmpty()) {
+                continue;
+            }
+
+            if (processInputLine(trimmed)) {
+                break;
+            }
+        }
+    }
+
+    private boolean processInputLine(String line) {
+        if (startsWithLineNumber(line)) {
+            saveProgramLine(line);
+            return false;
+        }
+
+        return handleDirectCommand(line);
+    }
+
+    private boolean handleDirectCommand(String line) {
+        String upper = line.toUpperCase();
+
+        if (upper.equals("RUN")) {
+            runProgram();
+            return false;
+        }
+        if (upper.equals("LIST")) {
+            listProgram();
+            return false;
+        }
+        if (upper.equals("NEW")) {
+            program.clear();
+            variables.clear();
+            outLine("OK");
+            return false;
+        }
+        if (upper.equals("HELP")) {
+            printHelp();
+            return false;
+        }
+        if (upper.equals("VARS")) {
+            printVars();
+            return false;
+        }
+        if (upper.equals("EXIT") || upper.equals("QUIT") || upper.equals("BYE")) {
+            outLine("BYE");
+            return true;
+        }
+
+        try {
+            executeStatement(line, null, null);
+        } catch (BasicRuntimeException e) {
+            outLine("? " + e.getMessage());
+        }
+        return false;
+    }
+
+    private boolean startsWithLineNumber(String line) {
+        int idx = 0;
+        while (idx < line.length() && Character.isDigit(line.charAt(idx))) {
+            idx++;
+        }
+        return idx > 0 && (idx == line.length() || Character.isWhitespace(line.charAt(idx)));
+    }
+
+    private void saveProgramLine(String line) {
+        int idx = 0;
+        while (idx < line.length() && Character.isDigit(line.charAt(idx))) {
+            idx++;
+        }
+
+        int lineNumber = Integer.parseInt(line.substring(0, idx));
+        String statement = line.substring(idx).trim();
+
+        if (statement.isEmpty()) {
+            program.remove(lineNumber);
+        } else {
+            program.put(lineNumber, statement);
+        }
+    }
+
+    private void listProgram() {
+        if (program.isEmpty()) {
+            outLine("(empty)");
+            return;
+        }
+        for (Map.Entry<Integer, String> entry : program.entrySet()) {
+            outLine(entry.getKey() + " " + entry.getValue());
+        }
+    }
+
+    private void printHelp() {
+        outLine("DIRECT: RUN, LIST, NEW, VARS, HELP, QUIT");
+        outLine("PROGRAM: PRINT, LET, GOTO, IF ... THEN <line>, END, REM");
+        outLine("EXAMPLE:");
+        outLine("10 LET X = 1");
+        outLine("20 PRINT X");
+        outLine("30 LET X = X + 1");
+        outLine("40 IF X < 5 THEN 20");
+        outLine("50 END");
+    }
+
+    private void printVars() {
+        if (variables.isEmpty()) {
+            outLine("(no vars)");
+            return;
+        }
+
+        for (Map.Entry<String, Integer> entry : variables.entrySet()) {
+            outLine(entry.getKey() + " = " + entry.getValue());
+        }
+    }
+
+    private void runProgram() {
+        if (program.isEmpty()) {
+            outLine("NO PROGRAM");
+            return;
+        }
+
+        List<Integer> lineNumbers = new ArrayList<>(program.keySet());
+        int pc = 0;
+        int stepCounter = 0;
+        final int maxSteps = 100_000;
+
+        try {
+            while (pc >= 0 && pc < lineNumbers.size()) {
+                if (++stepCounter > maxSteps) {
+                    throw new BasicRuntimeException("TOO MANY STEPS (possible infinite loop)");
+                }
+
+                int currentLine = lineNumbers.get(pc);
+                String statement = program.get(currentLine);
+
+                ExecutionResult result = executeStatement(statement, lineNumbers, pc);
+                if (result.endProgram) {
+                    break;
+                }
+                if (result.nextPc != null) {
+                    pc = result.nextPc;
+                } else {
+                    pc++;
+                }
+            }
+
+            outLine("READY.");
+        } catch (BasicRuntimeException e) {
+            outLine("? " + e.getMessage());
+        }
+    }
+
+    private ExecutionResult executeStatement(String statement, List<Integer> lineNumbers, Integer currentPc) {
+        String trimmed = statement.trim();
+        if (trimmed.isEmpty()) {
+            return ExecutionResult.continueNext();
+        }
+
+        String upper = trimmed.toUpperCase();
+
+        if (upper.startsWith("REM") || upper.startsWith("'")) {
+            return ExecutionResult.continueNext();
+        }
+
+        if (upper.startsWith("PRINT")) {
+            String arg = trimmed.length() > 5 ? trimmed.substring(5).trim() : "";
+            doPrint(arg);
+            return ExecutionResult.continueNext();
+        }
+
+        if (upper.startsWith("LET")) {
+            String assign = trimmed.substring(3).trim();
+            doLet(assign);
+            return ExecutionResult.continueNext();
+        }
+
+        if (upper.startsWith("IF")) {
+            return doIf(trimmed, lineNumbers);
+        }
+
+        if (upper.startsWith("GOTO")) {
+            String arg = trimmed.substring(4).trim();
+            int target = parseLineNumber(arg);
+            return ExecutionResult.jump(findLineIndex(lineNumbers, target));
+        }
+
+        if (upper.equals("END")) {
+            return ExecutionResult.end();
+        }
+
+        if (upper.equals("RUN")) {
+            runProgram();
+            return ExecutionResult.end();
+        }
+
+        if (trimmed.matches("^[A-Za-z][A-Za-z0-9_]*\\s*=.*$")) {
+            doLet(trimmed);
+            return ExecutionResult.continueNext();
+        }
+
+        throw new BasicRuntimeException("SYNTAX ERROR: " + statement);
+    }
+
+    private void doPrint(String arg) {
+        if (arg.isEmpty()) {
+            outLine("");
+            return;
+        }
+
+        if ((arg.startsWith("\"") && arg.endsWith("\"")) || (arg.startsWith("'") && arg.endsWith("'"))) {
+            outLine(arg.substring(1, arg.length() - 1));
+            return;
+        }
+
+        int value = evalExpr(arg);
+        outLine(String.valueOf(value));
+    }
+
+    private void initVideoOut(Machine machine) {
+        try {
+            gpu = machine.video().gpu;
+            gpu.init(Video.Modes.CGA_TEXT_40x25);
+            redrawVideo();
+        } catch (Exception ignored) {
+            gpu = null;
+        }
+    }
+
+    private void initKeyboardIn(Machine machine) {
+        try {
+            keyboard = machine.keyboard();
+        } catch (Exception ignored) {
+            keyboard = null;
+        }
+    }
+
+    private void outLine(String text) {
+        System.out.println(text);
+        appendScreenLine(text);
+    }
+
+    private void appendScreenLine(String text) {
+        String value = text == null ? "" : text;
+        for (String part : wrapLine(value)) {
+            screenLines.add(part);
+        }
+
+        while (screenLines.size() > VIDEO_ROWS - 2) {
+            screenLines.remove(0);
+        }
+
+        redrawVideo();
+    }
+
+    private List<String> wrapLine(String text) {
+        List<String> wrapped = new ArrayList<>();
+        if (text.isEmpty()) {
+            wrapped.add("");
+            return wrapped;
+        }
+
+        int start = 0;
+        while (start < text.length()) {
+            int end = Math.min(start + VIDEO_COLUMNS, text.length());
+            wrapped.add(text.substring(start, end));
+            start = end;
+        }
+        return wrapped;
+    }
+
+    private void redrawVideo() {
+        if (gpu == null) {
+            return;
+        }
+
+        gpu.clear(0);
+        gpu.drawText(0, 0, fitLine("NOVA BASIC"), 15, 0, false);
+
+        int row = 1;
+        for (String line : screenLines) {
+            if (row >= VIDEO_ROWS - 1) {
+                break;
+            }
+            gpu.drawText(0, row * 8, fitLine(line), 15, 0, false);
+            row++;
+        }
+
+        String prompt = "> " + currentInput + "_";
+        gpu.drawText(0, (VIDEO_ROWS - 1) * 8, fitLine(prompt), 14, 0, false);
+
+        gpu.present();
+    }
+
+    private String readLineFromKeyboard() {
+        StringBuilder builder = new StringBuilder();
+        currentInput = "";
+        redrawVideo();
+
+        while (true) {
+            Integer keyCode = keyboard.pollKeyCode();
+            if (keyCode == null) {
+                sleep(10);
+                continue;
+            }
+
+            if (keyCode == KeyEvent.VK_ENTER) {
+                String line = builder.toString();
+                appendScreenLine("> " + line);
+                System.out.println("> " + line);
+                currentInput = "";
+                redrawVideo();
+                return line;
+            }
+
+            if (keyCode == KeyEvent.VK_BACK_SPACE) {
+                if (builder.length() > 0) {
+                    builder.deleteCharAt(builder.length() - 1);
+                    currentInput = builder.toString();
+                    redrawVideo();
+                }
+                continue;
+            }
+
+            if (keyCode == KeyEvent.VK_ESCAPE) {
+                builder.setLength(0);
+                builder.append("QUIT");
+                currentInput = builder.toString();
+                redrawVideo();
+                continue;
+            }
+
+            Character mapped = mapKeyCodeToChar(keyCode);
+            if (mapped != null) {
+                builder.append(mapped);
+                currentInput = builder.toString();
+                redrawVideo();
+            }
+        }
+    }
+
+    private Character mapKeyCodeToChar(int keyCode) {
+        if (keyCode >= KeyEvent.VK_A && keyCode <= KeyEvent.VK_Z) {
+            return (char) ('A' + (keyCode - KeyEvent.VK_A));
+        }
+        if (keyCode >= KeyEvent.VK_0 && keyCode <= KeyEvent.VK_9) {
+            return (char) ('0' + (keyCode - KeyEvent.VK_0));
+        }
+        if (keyCode >= KeyEvent.VK_NUMPAD0 && keyCode <= KeyEvent.VK_NUMPAD9) {
+            return (char) ('0' + (keyCode - KeyEvent.VK_NUMPAD0));
+        }
+
+        return switch (keyCode) {
+            case KeyEvent.VK_SPACE -> ' ';
+            case KeyEvent.VK_PERIOD, KeyEvent.VK_DECIMAL -> '.';
+            case KeyEvent.VK_COMMA -> ',';
+            case KeyEvent.VK_MINUS, KeyEvent.VK_SUBTRACT -> '-';
+            case KeyEvent.VK_EQUALS -> '=';
+            case KeyEvent.VK_PLUS, KeyEvent.VK_ADD -> '+';
+            case KeyEvent.VK_SLASH, KeyEvent.VK_DIVIDE -> '/';
+            case KeyEvent.VK_ASTERISK, KeyEvent.VK_MULTIPLY -> '*';
+            case KeyEvent.VK_SEMICOLON -> ';';
+            case KeyEvent.VK_COLON -> ':';
+            case KeyEvent.VK_QUOTE -> '\'';
+            case KeyEvent.VK_OPEN_BRACKET -> '[';
+            case KeyEvent.VK_CLOSE_BRACKET -> ']';
+            case KeyEvent.VK_BACK_SLASH -> '\\';
+            default -> null;
+        };
+    }
+
+    private void sleep(long millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    private String fitLine(String text) {
+        String value = text == null ? "" : text;
+        if (value.length() > VIDEO_COLUMNS) {
+            return value.substring(0, VIDEO_COLUMNS);
+        }
+        return value + " ".repeat(VIDEO_COLUMNS - value.length());
+    }
+
+    private void doLet(String assign) {
+        int eq = assign.indexOf('=');
+        if (eq <= 0) {
+            throw new BasicRuntimeException("LET EXPECTS: NAME = EXPR");
+        }
+
+        String name = assign.substring(0, eq).trim().toUpperCase();
+        if (!name.matches("[A-Z][A-Z0-9_]*")) {
+            throw new BasicRuntimeException("BAD VARIABLE NAME: " + name);
+        }
+
+        String expr = assign.substring(eq + 1).trim();
+        int value = evalExpr(expr);
+        variables.put(name, value);
+    }
+
+    private ExecutionResult doIf(String statement, List<Integer> lineNumbers) {
+        String body = statement.substring(2).trim();
+        int thenPos = body.toUpperCase().indexOf("THEN");
+        if (thenPos < 0) {
+            throw new BasicRuntimeException("IF EXPECTS THEN");
+        }
+
+        String cond = body.substring(0, thenPos).trim();
+        String thenTarget = body.substring(thenPos + 4).trim();
+
+        if (evaluateCondition(cond)) {
+            int target = parseLineNumber(thenTarget);
+            return ExecutionResult.jump(findLineIndex(lineNumbers, target));
+        }
+        return ExecutionResult.continueNext();
+    }
+
+    private boolean evaluateCondition(String cond) {
+        String[] ops = {"<=", ">=", "<>", "=", "<", ">"};
+
+        for (String op : ops) {
+            int idx = cond.indexOf(op);
+            if (idx > 0) {
+                int left = evalExpr(cond.substring(0, idx).trim());
+                int right = evalExpr(cond.substring(idx + op.length()).trim());
+
+                return switch (op) {
+                    case "=" -> left == right;
+                    case "<>" -> left != right;
+                    case "<" -> left < right;
+                    case ">" -> left > right;
+                    case "<=" -> left <= right;
+                    case ">=" -> left >= right;
+                    default -> false;
+                };
+            }
+        }
+
+        throw new BasicRuntimeException("BAD IF CONDITION: " + cond);
+    }
+
+    private int parseLineNumber(String text) {
+        try {
+            return Integer.parseInt(text.trim());
+        } catch (NumberFormatException e) {
+            throw new BasicRuntimeException("BAD LINE NUMBER: " + text);
+        }
+    }
+
+    private int findLineIndex(List<Integer> lineNumbers, int line) {
+        if (lineNumbers == null) {
+            throw new BasicRuntimeException("GOTO/IF THEN needs a running program");
+        }
+
+        int idx = lineNumbers.indexOf(line);
+        if (idx < 0) {
+            throw new BasicRuntimeException("UNDEFINED LINE " + line);
+        }
+        return idx;
+    }
+
+    private int evalExpr(String expr) {
+        return new ExpressionParser(expr).parse();
+    }
+
+    private static final class ExecutionResult {
+        private final Integer nextPc;
+        private final boolean endProgram;
+
+        private ExecutionResult(Integer nextPc, boolean endProgram) {
+            this.nextPc = nextPc;
+            this.endProgram = endProgram;
+        }
+
+        private static ExecutionResult continueNext() {
+            return new ExecutionResult(null, false);
+        }
+
+        private static ExecutionResult jump(int nextPc) {
+            return new ExecutionResult(nextPc, false);
+        }
+
+        private static ExecutionResult end() {
+            return new ExecutionResult(null, true);
+        }
+    }
+
+    private static final class BasicRuntimeException extends RuntimeException {
+        private BasicRuntimeException(String message) {
+            super(message);
+        }
+    }
+
+    private final class ExpressionParser {
+        private final String input;
+        private int pos;
+
+        private ExpressionParser(String input) {
+            this.input = input;
+        }
+
+        private int parse() {
+            int value = parseExpression();
+            skipSpaces();
+            if (pos != input.length()) {
+                throw new BasicRuntimeException("BAD EXPR: " + input);
+            }
+            return value;
+        }
+
+        private int parseExpression() {
+            int value = parseTerm();
+            while (true) {
+                skipSpaces();
+                if (match('+')) {
+                    value += parseTerm();
+                } else if (match('-')) {
+                    value -= parseTerm();
+                } else {
+                    return value;
+                }
+            }
+        }
+
+        private int parseTerm() {
+            int value = parseFactor();
+            while (true) {
+                skipSpaces();
+                if (match('*')) {
+                    value *= parseFactor();
+                } else if (match('/')) {
+                    int divisor = parseFactor();
+                    if (divisor == 0) {
+                        throw new BasicRuntimeException("DIV BY ZERO");
+                    }
+                    value /= divisor;
+                } else {
+                    return value;
+                }
+            }
+        }
+
+        private int parseFactor() {
+            skipSpaces();
+
+            if (match('(')) {
+                int value = parseExpression();
+                skipSpaces();
+                if (!match(')')) {
+                    throw new BasicRuntimeException("MISSING )");
+                }
+                return value;
+            }
+
+            if (match('-')) {
+                return -parseFactor();
+            }
+
+            if (pos < input.length() && Character.isDigit(input.charAt(pos))) {
+                return parseNumber();
+            }
+
+            if (pos < input.length() && Character.isLetter(input.charAt(pos))) {
+                String name = parseIdentifier().toUpperCase();
+                return variables.getOrDefault(name, 0);
+            }
+
+            throw new BasicRuntimeException("BAD EXPR: " + input);
+        }
+
+        private int parseNumber() {
+            int start = pos;
+            while (pos < input.length() && Character.isDigit(input.charAt(pos))) {
+                pos++;
+            }
+            return Integer.parseInt(input.substring(start, pos));
+        }
+
+        private String parseIdentifier() {
+            int start = pos;
+            while (pos < input.length() && (Character.isLetterOrDigit(input.charAt(pos)) || input.charAt(pos) == '_')) {
+                pos++;
+            }
+            return input.substring(start, pos);
+        }
+
+        private void skipSpaces() {
+            while (pos < input.length() && Character.isWhitespace(input.charAt(pos))) {
+                pos++;
+            }
+        }
+
+        private boolean match(char expected) {
+            if (pos < input.length() && input.charAt(pos) == expected) {
+                pos++;
+                return true;
+            }
+            return false;
+        }
+    }
+}
[COMMIT END]
(C) 2025 0x4248 (C) 2025 4248 Media and 4248 Systems, All part of 0x4248 See LICENCE files for more information. Not all files are by 0x4248 always check Licencing.