diff --git a/src/quick/Action.java b/src/quick/Action.java new file mode 100644 index 0000000..e59c97f --- /dev/null +++ b/src/quick/Action.java @@ -0,0 +1,6 @@ +package quick; + +public abstract class Action { + protected Global global = Global.getInstance(); + public abstract Boolean execute(Interface base); +} diff --git a/src/quick/Color.java b/src/quick/Color.java new file mode 100644 index 0000000..d6fd3ac --- /dev/null +++ b/src/quick/Color.java @@ -0,0 +1,22 @@ +package quick; + +public class Color { + public static final String ANSI_RESET = "\u001B[0m"; + public static final String ANSI_BLACK = "\u001B[30m"; + public static final String ANSI_RED = "\u001B[31m"; + public static final String ANSI_GREEN = "\u001B[32m"; + public static final String ANSI_YELLOW = "\u001B[33m"; + public static final String ANSI_BLUE = "\u001B[34m"; + public static final String ANSI_PURPLE = "\u001B[35m"; + public static final String ANSI_CYAN = "\u001B[36m"; + public static final String ANSI_WHITE = "\u001B[37m"; + + public static final String ANSI_BLACK_BACKGROUND = "\u001B[40m"; + public static final String ANSI_RED_BACKGROUND = "\u001B[41m"; + public static final String ANSI_GREEN_BACKGROUND = "\u001B[42m"; + public static final String ANSI_YELLOW_BACKGROUND = "\u001B[43m"; + public static final String ANSI_BLUE_BACKGROUND = "\u001B[44m"; + public static final String ANSI_PURPLE_BACKGROUND = "\u001B[45m"; + public static final String ANSI_CYAN_BACKGROUND = "\u001B[46m"; + public static final String ANSI_WHITE_BACKGROUND = "\u001B[47m"; +} diff --git a/src/quick/Global.java b/src/quick/Global.java new file mode 100644 index 0000000..348e478 --- /dev/null +++ b/src/quick/Global.java @@ -0,0 +1,18 @@ +package quick; + +import java.util.ArrayList; +import java.util.List; + +public class Global { + private static Global instance; + public List> registeredViews = new ArrayList<>(); + + private Global() {} + + public static synchronized Global getInstance() { + if (instance == null) { + instance = new Global(); + } + return instance; + } +} diff --git a/src/quick/Helper.java b/src/quick/Helper.java new file mode 100644 index 0000000..60320b9 --- /dev/null +++ b/src/quick/Helper.java @@ -0,0 +1,7 @@ +package quick; + +public class Helper { + public static void clearScreen() { + System.out.print(""); + } +} diff --git a/src/quick/Interface.java b/src/quick/Interface.java new file mode 100644 index 0000000..b4492bd --- /dev/null +++ b/src/quick/Interface.java @@ -0,0 +1,99 @@ +package quick; + +import java.util.Scanner; + +import quick.exceptions.InvalidViewActionReturn; +import quick.guard.PrintDog; +import quick.intern.v_help; +import quick.intern.v_select; + +public class Interface { + public final Scanner scanner = new Scanner(System.in); + private Global global = Global.getInstance(); + private Boolean active = true; + private Class defaultView = v_select.class; + private View selectedView; + public int[] __terminalSize; + public int __renderCycleLines; + + public Interface() {} + + public Interface(Class defaultView) { + this.defaultView = defaultView; + } + + public void registerView(Class viewClass) { + global.registeredViews.add(viewClass); + } + + public Boolean selectView(View viewInstance) { + selectedView = viewInstance; + return viewInstance.init(); + } + + public Boolean selectView(Class viewClass) { + View new_view = View.instantiate(viewClass); + selectedView = new_view; + return new_view.init(); + } + + private Boolean cycle() { + selectedView.initBase(this); + + // Always start with a clean break + System.out.print("\r\n"); + System.out.flush(); + + this.__terminalSize = TerminalSize.getTerminalSize(); + this.__renderCycleLines = 0; + + Helper.clearScreen(); + + // Draw the view; it should update __renderCycleLines + selectedView.draw(); + System.out.flush(); + + // Always leave at least 2 lines: one for prompt, one for safety + int overheadLines = 2; + int linesToFill = Math.max(0, this.__terminalSize[0] - __renderCycleLines - overheadLines); + + for (int i = 0; i < linesToFill; i++) { + System.out.println(); + } + + String view_prompt = (selectedView.viewPrompt == null ? "" : selectedView.viewPrompt); + System.err.print(view_prompt); + System.err.flush(); + String userInputRaw = scanner.nextLine(); + System.out.println(); + + Action viewResponse = selectedView.onCommand(userInputRaw); + + if (viewResponse == null) { + return true; + } + + if (viewResponse instanceof Action) { + return viewResponse.execute(this); + } + + throw new InvalidViewActionReturn(); + } + + private void mainLoop() { + selectView(defaultView); + + while (active) { + active = cycle(); + } + } + + public void start() { + PrintDog.start(true); + View.scanner = scanner; + registerView(v_help.class); + registerView(v_select.class); + + mainLoop(); + } +} diff --git a/src/quick/TerminalSize.java b/src/quick/TerminalSize.java new file mode 100644 index 0000000..2132d15 --- /dev/null +++ b/src/quick/TerminalSize.java @@ -0,0 +1,25 @@ +package quick; + +import java.io.BufferedReader; +import java.io.InputStreamReader; + +public class TerminalSize { + public static int[] getTerminalSize() { + try { + Process process = Runtime.getRuntime().exec(new String[]{"sh", "-c", "stty size < /dev/tty"}); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + String line = reader.readLine(); + + if (line != null) { + String[] parts = line.trim().split(" "); + int rows = Integer.parseInt(parts[0]); + int cols = Integer.parseInt(parts[1]); + return new int[]{rows, cols}; + } + } catch (Exception e) { + System.err.println("[ERROR] Failed to get terminal size: " + e.getMessage()); + } + + return new int[]{69, 69}; // Soggy UwU fallback + } +} diff --git a/src/quick/View.java b/src/quick/View.java new file mode 100644 index 0000000..ba6b7f7 --- /dev/null +++ b/src/quick/View.java @@ -0,0 +1,53 @@ +package quick; + +import java.util.Scanner; + +import quick.exceptions.DuplicateViewMatchException; + +import java.util.ArrayList; + +public abstract class View { + protected static Global global = Global.getInstance(); + protected static ViewActionFactories actions = new ViewActionFactories(); + protected ViewTerminalAcces terminal; + public static Scanner scanner; + public static Interface __base; + public String viewPrompt; + public String viewSignature; + public String helperText; + + public abstract Boolean init(); + public abstract void draw(); + public abstract Action onCommand(String command); + public abstract Boolean onSelection(String userInputRaw); + + public void initBase(Interface base) { + View.__base = base; + this.terminal = new ViewTerminalAcces(base); + } + + public static View instantiate(Class viewClass) { + try { + return viewClass.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException("Could not instantiate view: " + viewClass.getName(), e); + } + } + + public static View signatureParse(String userInputRaw) { + ArrayList matched = new ArrayList(); + + for (Class ViewClass : global.registeredViews) { + View viewInstance = View.instantiate(ViewClass); + if (viewInstance.onSelection(userInputRaw)) { + matched.add(viewInstance); + } + } + + if (matched.size() > 1) { + throw new DuplicateViewMatchException(userInputRaw, matched.size()); + } + + return matched.isEmpty() ? null : matched.get(0); + } +} diff --git a/src/quick/ViewActionFactories.java b/src/quick/ViewActionFactories.java new file mode 100644 index 0000000..b05fe23 --- /dev/null +++ b/src/quick/ViewActionFactories.java @@ -0,0 +1,28 @@ +package quick; + +import quick.action.a_dd; +import quick.action.a_nop; +import quick.action.a_redirect; +import quick.action.a_stop; + +public class ViewActionFactories { + public Action redirect(Class target) { + return new a_redirect(target); + } + + public Action redirect(View target) { + return new a_redirect(target); + } + + public Action stop() { + return new a_stop(); + } + + public Action nop() { + return new a_nop(); + } + + public Action dd(Object... data) { + return new a_dd(data); + } +} diff --git a/src/quick/ViewTerminalAcces.java b/src/quick/ViewTerminalAcces.java new file mode 100644 index 0000000..799640b --- /dev/null +++ b/src/quick/ViewTerminalAcces.java @@ -0,0 +1,32 @@ +package quick; + +public class ViewTerminalAcces { + private Interface base; + + public ViewTerminalAcces(Interface base) { + this.base = base; + } + + public void print(String text) { + base.__renderCycleLines += 1; + System.out.print(text); + } + + public int getWidth() { + return base.__terminalSize[1]; + } + + public int getHeight() { + return base.__terminalSize[0]; + } + + public int getCurrentRclCount() { + return base.__renderCycleLines; + } + + public void fill(int size) { + for (int i = 0; i < size; i++) { + print("\n"); + } + } +} diff --git a/src/quick/action/a_dd.java b/src/quick/action/a_dd.java new file mode 100644 index 0000000..b2ebf64 --- /dev/null +++ b/src/quick/action/a_dd.java @@ -0,0 +1,109 @@ +package quick.action; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import quick.Action; +import quick.Color; +import quick.Interface; + +public class a_dd extends Action { + private final Object[] data; + + public a_dd(Object... data) { + this.data = data; + } + + @Override + public Boolean execute(Interface base) { + System.out.println(Color.ANSI_BLUE + "========== DUMP & DIE ==========" + Color.ANSI_RESET); + + for (int i = 0; i < data.length; i++) { + Object item = data[i]; + String type = (item == null) ? "null" : item.getClass().getSimpleName(); + String typeColor = getTypeColor(type); + + System.out.println( + Color.ANSI_WHITE + "[" + i + "] => " + + typeColor + "(" + type + ") " + formatValue(item) + Color.ANSI_RESET + ); + } + + System.out.println(Color.ANSI_BLUE + "============ END ============" + Color.ANSI_RESET); + return false; + } + + private String formatValue(Object obj) { + if (obj == null) return Color.ANSI_RED + "null" + Color.ANSI_RESET; + + if (obj instanceof Map) { + StringBuilder sb = new StringBuilder(Color.ANSI_PURPLE + "Map {\n"); + ((Map) obj).forEach((k, v) -> { + String type = (v == null) ? "null" : v.getClass().getSimpleName(); + String color = getTypeColor(type); + sb.append(Color.ANSI_WHITE + " " + k + " => " + color + "(" + type + ") " + formatValue(v) + "\n"); + }); + sb.append(Color.ANSI_PURPLE + "}"); + return sb.toString(); + } + + if (obj instanceof List) { + StringBuilder sb = new StringBuilder(Color.ANSI_BLUE + "List [\n"); + int i = 0; + for (Object val : (List) obj) { + String type = (val == null) ? "null" : val.getClass().getSimpleName(); + String color = getTypeColor(type); + sb.append(Color.ANSI_WHITE + " " + i++ + " => " + color + "(" + type + ") " + formatValue(val) + "\n"); + } + sb.append(Color.ANSI_BLUE + "]"); + return sb.toString(); + } + + if (obj instanceof Set) { + StringBuilder sb = new StringBuilder(Color.ANSI_RED + "Set {\n"); + for (Object val : (Set) obj) { + String type = (val == null) ? "null" : val.getClass().getSimpleName(); + String color = getTypeColor(type); + sb.append(Color.ANSI_WHITE + " => " + color + "(" + type + ") " + formatValue(val) + "\n"); + } + sb.append(Color.ANSI_RED + "}"); + return sb.toString(); + } + + if (obj.getClass().isArray()) { + StringBuilder sb = new StringBuilder(Color.ANSI_CYAN + "Array [\n"); + int len = java.lang.reflect.Array.getLength(obj); + for (int i = 0; i < len; i++) { + Object val = java.lang.reflect.Array.get(obj, i); + String type = (val == null) ? "null" : val.getClass().getSimpleName(); + String color = getTypeColor(type); + sb.append(Color.ANSI_WHITE + " " + i + " => " + color + "(" + type + ") " + formatValue(val) + "\n"); + } + sb.append(Color.ANSI_CYAN + "]"); + return sb.toString(); + } + + return getTypeColor(obj.getClass().getSimpleName()) + obj.toString(); + } + + private String getTypeColor(String type) { + if (type == null) return Color.ANSI_RED; + + switch (type) { + case "String": return Color.ANSI_GREEN; + case "Integer": + case "Long": + case "Double": + case "Float": + case "Short": + case "Byte": return Color.ANSI_YELLOW; + case "Boolean": return Color.ANSI_CYAN; + case "Map": return Color.ANSI_PURPLE; + case "List": return Color.ANSI_BLUE; + case "Set": return Color.ANSI_RED; + case "Object[]": return Color.ANSI_WHITE; + default: return Color.ANSI_WHITE; + } + } +} diff --git a/src/quick/action/a_nop.java b/src/quick/action/a_nop.java new file mode 100644 index 0000000..fbc6f40 --- /dev/null +++ b/src/quick/action/a_nop.java @@ -0,0 +1,10 @@ +package quick.action; + +import quick.Action; +import quick.Interface; + +public class a_nop extends Action { + public Boolean execute(Interface base) { + return true; + }; +} diff --git a/src/quick/action/a_redirect.java b/src/quick/action/a_redirect.java new file mode 100644 index 0000000..6291ec5 --- /dev/null +++ b/src/quick/action/a_redirect.java @@ -0,0 +1,49 @@ +package quick.action; + +import quick.Action; +import quick.Interface; +import quick.View; +import quick.exceptions.InvalidRedirect; + +public class a_redirect extends Action{ + public Class targetClass; + public View targetInstance; + private Interface base; + + public a_redirect(Class target) { + this.targetClass = target; + } + + public a_redirect(View target) { + this.targetInstance = target; + } + + public Boolean execute(Interface base) { + this.base = base; + + if (targetClass != null) { + return redirectByClass(); + } + if (targetInstance != null) { + return redirectByInstance(); + } + + throw new InvalidRedirect(null); + } + + private Boolean redirectByClass() { + if (!global.registeredViews.contains(targetClass)) { + throw new InvalidRedirect(targetClass); + } + + return base.selectView(targetClass); + } + + private Boolean redirectByInstance() { + if (!global.registeredViews.contains(targetInstance.getClass())) { + throw new InvalidRedirect(targetInstance.getClass()); + } + + return base.selectView(targetInstance); + } +} diff --git a/src/quick/action/a_stop.java b/src/quick/action/a_stop.java new file mode 100644 index 0000000..5d5c3a3 --- /dev/null +++ b/src/quick/action/a_stop.java @@ -0,0 +1,10 @@ +package quick.action; + +import quick.Action; +import quick.Interface; + +public class a_stop extends Action { + public Boolean execute(Interface base) { + return false; + }; +} diff --git a/src/quick/exceptions/DuplicateViewMatchException.java b/src/quick/exceptions/DuplicateViewMatchException.java new file mode 100644 index 0000000..7bc7512 --- /dev/null +++ b/src/quick/exceptions/DuplicateViewMatchException.java @@ -0,0 +1,7 @@ +package quick.exceptions; + +public class DuplicateViewMatchException extends RuntimeException { + public DuplicateViewMatchException(String input, int count) { + super("Input \"" + input + "\" matched " + count + " views. View signatures must be unique for this input."); + } +} diff --git a/src/quick/exceptions/IllegalTerminalPrintException.java b/src/quick/exceptions/IllegalTerminalPrintException.java new file mode 100644 index 0000000..3691d5b --- /dev/null +++ b/src/quick/exceptions/IllegalTerminalPrintException.java @@ -0,0 +1,7 @@ +package quick.exceptions; + +public class IllegalTerminalPrintException extends RuntimeException { + public IllegalTerminalPrintException(String message) { + super(message); + } +} diff --git a/src/quick/exceptions/InvalidRedirect.java b/src/quick/exceptions/InvalidRedirect.java new file mode 100644 index 0000000..dae845c --- /dev/null +++ b/src/quick/exceptions/InvalidRedirect.java @@ -0,0 +1,11 @@ +package quick.exceptions; + +import quick.View; + +public class InvalidRedirect extends RuntimeException { + public InvalidRedirect(Class target) { + super(target == null + ? "invalid use of null in redirect" + : "View" + target.getName() + " is not registered in base"); + } +} diff --git a/src/quick/exceptions/InvalidViewActionReturn.java b/src/quick/exceptions/InvalidViewActionReturn.java new file mode 100644 index 0000000..ad6423e --- /dev/null +++ b/src/quick/exceptions/InvalidViewActionReturn.java @@ -0,0 +1,7 @@ +package quick.exceptions; + +public class InvalidViewActionReturn extends RuntimeException { + public InvalidViewActionReturn() { + super("Please only return a Child of Action or null from a View"); + } +} diff --git a/src/quick/guard/PrintDog.java b/src/quick/guard/PrintDog.java new file mode 100644 index 0000000..e9f258e --- /dev/null +++ b/src/quick/guard/PrintDog.java @@ -0,0 +1,101 @@ +package quick.guard; + +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.ByteArrayOutputStream; +import java.util.Set; + +import quick.Color; + +import java.util.HashSet; + +public class PrintDog { + private static final Set trustedPackages = new HashSet<>(); + private static boolean strict = true; + private static PrintStream originalOut; + private static PrintStream mirroredOut; + + public static void start(boolean strictMode) { + strict = strictMode; + + originalOut = System.out; + + // Register your actual source packages only + trustedPackages.add("quick."); + trustedPackages.add("quick.action."); + trustedPackages.add("quick.exceptions."); + trustedPackages.add("quick.guard."); + trustedPackages.add("quick.intern."); + + // Setup global uncaught exception handler + Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { + originalOut.println(Color.ANSI_RED + "🔥 UNCAUGHT EXCEPTION IN THREAD: " + thread.getName() + Color.ANSI_RESET); + throwable.printStackTrace(originalOut); + // Optional kill: + // System.exit(1); + }); + + // Setup mirror output stream + mirroredOut = new PrintStream(new MirrorOutputStream(originalOut), true); + System.setOut(mirroredOut); + } + + private static class MirrorOutputStream extends OutputStream { + private final OutputStream original; + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + public MirrorOutputStream(OutputStream original) { + this.original = original; + } + + @Override + public void write(int b) { + try { + original.write(b); + buffer.write(b); + + if (b == '\n') { + flushBuffer(); + } + } catch (Exception e) { + e.printStackTrace(originalOut); + System.exit(69); + } + } + + private void flushBuffer() { + String content = buffer.toString(); + buffer.reset(); + + Throwable t = new Throwable(); + StackTraceElement[] stack = t.getStackTrace(); + + if (strict && !isFromTrustedCode(stack)) { + originalOut.println( + Color.ANSI_WHITE + Color.ANSI_RED_BACKGROUND + + "🐶 BARK! UNAUTHORIZED TERMINAL PRINT DETECTED!" + + Color.ANSI_RESET + "\n" + + Color.ANSI_YELLOW + "→ Use terminal.print() instead.\n" + + "→ Offending content:\n" + + Color.ANSI_WHITE + content.trim() + Color.ANSI_RESET + ); + + // 🚨 HARD CRASH + System.err.println(Color.ANSI_RED + "☠️ FATAL: System.out used in untrusted context!" + Color.ANSI_RESET); + System.exit(123); + } + } + + private boolean isFromTrustedCode(StackTraceElement[] stack) { + for (StackTraceElement frame : stack) { + String cls = frame.getClassName(); + for (String prefix : trustedPackages) { + if (cls.startsWith(prefix)) { + return true; + } + } + } + return false; + } + } +} diff --git a/src/quick/intern/v_help.java b/src/quick/intern/v_help.java new file mode 100644 index 0000000..fb871f6 --- /dev/null +++ b/src/quick/intern/v_help.java @@ -0,0 +1,30 @@ +package quick.intern; + +import quick.Action; +import quick.View; + +public class v_help extends View { + @Override + public Boolean init() { + viewPrompt = "HELP"; + viewSignature = "help"; + helperText = "help view most likely not implemented 😂 u noob"; + return true; + } + + @Override + public void draw() { + return; + } + + @Override + public Action onCommand(String command) { + return null; + + } + + @Override + public Boolean onSelection(String userInputRaw) { + return false; + } +} diff --git a/src/quick/intern/v_select.java b/src/quick/intern/v_select.java new file mode 100644 index 0000000..37e85c5 --- /dev/null +++ b/src/quick/intern/v_select.java @@ -0,0 +1,68 @@ +package quick.intern; + +import quick.Action; +import quick.Color; +import quick.View; +import quick.action.a_nop; +import quick.action.a_redirect; + +public class v_select extends View { + private String viewList; + private String lastSelectionWrong; + + @Override + public Boolean init() { + StringBuilder sb = new StringBuilder(); + for (Class viewClass : global.registeredViews) { + if (viewClass == v_select.class) continue; + View viewInstance = View.instantiate(viewClass); + viewInstance.init(); + if (viewInstance.viewSignature != null && !viewInstance.viewSignature.isEmpty()) { + sb.append(": " + viewInstance.viewSignature + " - " + viewInstance.helperText).append("\n"); + } else { + sb.append(": " + viewInstance.helperText).append("\n"); + } + } + if (sb.length() > 0 && sb.charAt(sb.length() - 1) == '\n') { + sb.deleteCharAt(sb.length() - 1); + } + viewList = sb.toString(); + return true; + } + + @Override + public void draw() { + terminal.print("Select a action:\n"); + for (String line : viewList.split("\n")) terminal.print(line+"\n"); + terminal.print("\n"); + + if (lastSelectionWrong != null) { + terminal.print(Color.ANSI_RED + "Something went wrong with: " + lastSelectionWrong + "\"" + Color.ANSI_RESET + "\n"); + terminal.print("\n"); + lastSelectionWrong = null; + } else { + terminal.print("\n"); + } + } + + @Override + public Action onCommand(String command) { + if (command.isEmpty()) { + return new a_nop(); + } + + View selectedView = View.signatureParse(command); + + if (selectedView == null) { + lastSelectionWrong = command; + return new a_nop(); + } + + return new a_redirect(selectedView); + } + + @Override + public Boolean onSelection(String userInputRaw) { + return false; + } +}