From d31fef5f8593756e8a9bfffd940c59328607a44f Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Mon, 17 Feb 2025 17:47:06 +0100 Subject: [PATCH 1/6] build: Made Java 11 the minimum version --- pom.xml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 9f59593..287c683 100644 --- a/pom.xml +++ b/pom.xml @@ -9,8 +9,8 @@ 1.0-SNAPSHOT - 8 - 8 + 11 + 11 UTF-8 3.30.3 @@ -34,6 +34,16 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + + --add-exports + java.xml/com.sun.org.apache.xerces.internal.dom=ALL-UNNAMED + + + com.diffplug.spotless spotless-maven-plugin From 4b7024dcd35ea675304c7300af7dce5d08c1060f Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Mon, 17 Feb 2025 17:48:40 +0100 Subject: [PATCH 2/6] feat: added support for events For now used to communicate resize events from the terminal. --- src/main/java/examples/FullPanel.java | 14 ++- .../org/codejive/context/events/Event.java | 4 +- .../codejive/context/events/ResizeEvent.java | 22 ++++ .../context/terminal/BufferedScreen.java | 114 ++++++++++++++++++ .../codejive/context/terminal/FlexRect.java | 4 + .../org/codejive/context/terminal/Rect.java | 4 + .../codejive/context/terminal/Resizeable.java | 5 + .../org/codejive/context/terminal/Screen.java | 83 +------------ .../org/codejive/context/terminal/Size.java | 14 +++ .../org/codejive/context/terminal/Term.java | 55 ++++----- 10 files changed, 199 insertions(+), 120 deletions(-) create mode 100644 src/main/java/org/codejive/context/events/ResizeEvent.java create mode 100644 src/main/java/org/codejive/context/terminal/BufferedScreen.java create mode 100644 src/main/java/org/codejive/context/terminal/Resizeable.java diff --git a/src/main/java/examples/FullPanel.java b/src/main/java/examples/FullPanel.java index f4db8c6..7b54536 100644 --- a/src/main/java/examples/FullPanel.java +++ b/src/main/java/examples/FullPanel.java @@ -27,6 +27,15 @@ public static void main(String... args) throws IOException { public int run() throws IOException { Screen screen = term.fullScreen(); + draw(screen); + + screen.update(); + term.input().readChar(); + + return 0; + } + + private void draw(Screen screen) throws IOException { int displayWidth = screen.rect().width(); int displayHeight = screen.rect().height(); @@ -38,11 +47,6 @@ public int run() throws IOException { cb.append(displayWidth + "x" + displayHeight); screen.printAt( displayWidth / 2 - cb.length() / 2, displayHeight / 2, cb.toAttributedString()); - - screen.update(); - term.input().readChar(); - - return 0; } private Box createBox(int w, int h) { diff --git a/src/main/java/org/codejive/context/events/Event.java b/src/main/java/org/codejive/context/events/Event.java index 6fd14dd..36cae0d 100644 --- a/src/main/java/org/codejive/context/events/Event.java +++ b/src/main/java/org/codejive/context/events/Event.java @@ -1,5 +1,5 @@ package org.codejive.context.events; -public interface Event { - EventTarget target(); +public interface Event { + T target(); } diff --git a/src/main/java/org/codejive/context/events/ResizeEvent.java b/src/main/java/org/codejive/context/events/ResizeEvent.java new file mode 100644 index 0000000..f138090 --- /dev/null +++ b/src/main/java/org/codejive/context/events/ResizeEvent.java @@ -0,0 +1,22 @@ +package org.codejive.context.events; + +import org.codejive.context.terminal.Size; + +public class ResizeEvent implements Event { + private final T target; + private final Size size; + + public ResizeEvent(Size size, T target) { + this.size = size; + this.target = target; + } + + public Size size() { + return size; + } + + @Override + public T target() { + return target; + } +} diff --git a/src/main/java/org/codejive/context/terminal/BufferedScreen.java b/src/main/java/org/codejive/context/terminal/BufferedScreen.java new file mode 100644 index 0000000..ff05966 --- /dev/null +++ b/src/main/java/org/codejive/context/terminal/BufferedScreen.java @@ -0,0 +1,114 @@ +package org.codejive.context.terminal; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.codejive.context.events.EventEmitter; +import org.codejive.context.events.ResizeEvent; +import org.jline.utils.AttributedCharSequence; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.Display; + +public class BufferedScreen implements Screen { + protected final Term term; + protected final FlexRect flexRect; + protected final Display display; + protected final EventEmitter> resizeEmitter = new EventEmitter<>(); + protected Size recordedSize; + protected AttributedStringBuilder[] lines; + + @Override + public Rect rect() { + return flexRect.actualRect(term.size()); + } + + protected BufferedScreen(Term term, int left, int top, int width, int height) { + this(term, new FlexRect(left, top, width, height)); + } + + protected BufferedScreen(Term term, FlexRect flexRect) { + this.term = term; + this.flexRect = flexRect; + this.display = new Display(term.terminal, false); + term.resizeEmitter.addListener(this::handleTermResizeEvent); + Rect r = rect(); + this.display.resize(r.height(), r.width()); + handleResize(r.size()); + clear(); + } + + @Override + public void clear() { + int width = rect().width(); + int height = rect().height(); + this.lines = new AttributedStringBuilder[height]; + for (int i = 0; i < height; i++) { + this.lines[i] = new AttributedStringBuilder(width); + } + } + + public void printAt(int x, int y, AttributedString str) { + if (y < rect().top() || y > rect().bottom()) { + return; + } + AttributedStringBuilder line = lines[y]; + if (x > rect().right() || (x + str.length() - 1) < rect().left()) { + return; + } + if (x < rect().left()) { + str = str.substring(rect().left() - x, str.length()); + x = rect().left(); + } + if ((x + str.length() - 1) > rect().right()) { + str = str.substring(0, rect().right() - x + 1); + } + if (line.length() < x) { + pad(line, ' ', x - line.length()); + line.append(str); + } else if (x + str.length() >= line.length()) { + line.setLength(x); + line.append(str); + } else { + AttributedStringBuilder ln = new AttributedStringBuilder(rect().width()); + ln.append(line.substring(0, x)); + ln.append(str); + ln.append(line.substring(x + str.length(), line.length())); + lines[y] = ln; + } + } + + public static void pad(AttributedStringBuilder str, char c, int n) { + for (int i = 0; i < n; i++) { + str.append(c); + } + } + + protected List lines() { + return Arrays.stream(lines) + .map(AttributedCharSequence::toAttributedString) + .collect(Collectors.toList()); + } + + public void update() { + display.update(lines(), 0); + } + + protected void handleTermResizeEvent(ResizeEvent event) { + handleResize(event.size()); + } + + protected void handleResize(Size newSize) { + display.resize(newSize.height(), newSize.width()); + if (recordedSize == null || !recordedSize.equals(newSize)) { + recordedSize = newSize; + onResize(newSize); + } + } + + @Override + public void onResize(Size newSize) { + System.out.println("RESIZE EVENT: " + newSize); + resizeEmitter.dispatch(new ResizeEvent<>(newSize, this)); + } +} diff --git a/src/main/java/org/codejive/context/terminal/FlexRect.java b/src/main/java/org/codejive/context/terminal/FlexRect.java index 858fec5..bb1c8bc 100644 --- a/src/main/java/org/codejive/context/terminal/FlexRect.java +++ b/src/main/java/org/codejive/context/terminal/FlexRect.java @@ -1,5 +1,9 @@ package org.codejive.context.terminal; +/** + * This class defines a rectangle (similar to Rect) but with the ability to have + * negative width and height which means that the width or height is relative to the available size. + */ public class FlexRect { private final int left, top, width, height; diff --git a/src/main/java/org/codejive/context/terminal/Rect.java b/src/main/java/org/codejive/context/terminal/Rect.java index 8ac8e3d..701cc95 100644 --- a/src/main/java/org/codejive/context/terminal/Rect.java +++ b/src/main/java/org/codejive/context/terminal/Rect.java @@ -25,6 +25,10 @@ public int bottom() { return top + height() - 1; } + public Size size() { + return new Size(width(), height()); + } + public boolean outside(Rect other) { return top() > other.bottom() || bottom() < other.top() diff --git a/src/main/java/org/codejive/context/terminal/Resizeable.java b/src/main/java/org/codejive/context/terminal/Resizeable.java new file mode 100644 index 0000000..8c899e1 --- /dev/null +++ b/src/main/java/org/codejive/context/terminal/Resizeable.java @@ -0,0 +1,5 @@ +package org.codejive.context.terminal; + +public interface Resizeable { + void onResize(Size newSize); +} diff --git a/src/main/java/org/codejive/context/terminal/Screen.java b/src/main/java/org/codejive/context/terminal/Screen.java index b1c7a54..a046412 100644 --- a/src/main/java/org/codejive/context/terminal/Screen.java +++ b/src/main/java/org/codejive/context/terminal/Screen.java @@ -1,91 +1,12 @@ package org.codejive.context.terminal; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; -import org.jline.utils.AttributedCharSequence; +import org.codejive.context.events.EventTarget; import org.jline.utils.AttributedString; -import org.jline.utils.AttributedStringBuilder; -import org.jline.utils.Display; -public interface Screen extends Rectangular { +public interface Screen extends Rectangular, EventTarget, Resizeable { void printAt(int x, int y, AttributedString str); void clear(); void update(); } - -class ScreenImpl implements Screen { - private final Rect rect; - private final Display display; - private AttributedStringBuilder[] lines; - - @Override - public Rect rect() { - return rect; - } - - protected ScreenImpl(Term term, int width, int height) { - this.rect = new Rect(0, 0, width, height); - this.display = new Display(term.terminal, false); - this.display.resize(height, width); - clear(); - } - - @Override - public void clear() { - int width = rect().width(); - int height = rect().height(); - this.lines = new AttributedStringBuilder[height]; - for (int i = 0; i < height; i++) { - this.lines[i] = new AttributedStringBuilder(width); - } - } - - public void printAt(int x, int y, AttributedString str) { - if (y < rect().top() || y > rect().bottom()) { - return; - } - AttributedStringBuilder line = lines[y]; - if (x > rect().right() || (x + str.length() - 1) < rect().left()) { - return; - } - if (x < rect().left()) { - str = str.substring(rect().left() - x, str.length()); - x = rect().left(); - } - if ((x + str.length() - 1) > rect().right()) { - str = str.substring(0, rect().right() - x + 1); - } - if (line.length() < x) { - pad(line, ' ', x - line.length()); - line.append(str); - } else if (x + str.length() >= line.length()) { - line.setLength(x); - line.append(str); - } else { - AttributedStringBuilder ln = new AttributedStringBuilder(rect.width()); - ln.append(line.substring(0, x)); - ln.append(str); - ln.append(line.substring(x + str.length(), line.length())); - lines[y] = ln; - } - } - - private static void pad(AttributedStringBuilder str, char c, int n) { - for (int i = 0; i < n; i++) { - str.append(c); - } - } - - private List lines() { - return Arrays.stream(lines) - .map(AttributedCharSequence::toAttributedString) - .collect(Collectors.toList()); - } - - public void update() { - display.update(lines(), 0); - } -} diff --git a/src/main/java/org/codejive/context/terminal/Size.java b/src/main/java/org/codejive/context/terminal/Size.java index aa51455..7ba7bd9 100644 --- a/src/main/java/org/codejive/context/terminal/Size.java +++ b/src/main/java/org/codejive/context/terminal/Size.java @@ -1,5 +1,7 @@ package org.codejive.context.terminal; +import java.util.Objects; + public class Size { private final int width; private final int height; @@ -19,6 +21,18 @@ public int height() { return height; } + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Size size = (Size) o; + return width == size.width && height == size.height; + } + + @Override + public int hashCode() { + return Objects.hash(width, height); + } + @Override public String toString() { return width + "x" + height; diff --git a/src/main/java/org/codejive/context/terminal/Term.java b/src/main/java/org/codejive/context/terminal/Term.java index 73bb191..48129d0 100644 --- a/src/main/java/org/codejive/context/terminal/Term.java +++ b/src/main/java/org/codejive/context/terminal/Term.java @@ -3,18 +3,18 @@ import java.io.Closeable; import java.io.Flushable; import java.io.IOException; -import org.codejive.context.events.Event; import org.codejive.context.events.EventEmitter; import org.codejive.context.events.EventTarget; +import org.codejive.context.events.ResizeEvent; import org.jline.terminal.Attributes; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; import org.jline.utils.InfoCmp; -public class Term implements Flushable, Closeable, EventTarget { - final Terminal terminal; - final EventEmitter onResize = new EventEmitter<>(); - private final Attributes savedAttributes; +public class Term implements Flushable, Closeable, Resizeable, EventTarget { + protected final Terminal terminal; + protected final EventEmitter> resizeEmitter = new EventEmitter<>(); + protected final Attributes savedAttributes; public static Term create() throws IOException { return new Term(); @@ -35,18 +35,21 @@ public Integer maxColors() { } public Screen fullScreen() { - Size size = size(); - return sizedScreen(size.width(), size.height()); + return sizedScreen(-1, -1); + } + + public Screen wideScreen(int height) { + return sizedScreen(-1, height); } public Screen sizedScreen(int width, int height) { - if (width == 0) { - width = 80; - } - if (height == 0) { - height = 40; - } - return new ScreenImpl(this, width, height); + return screen(0, 0, width, height); + } + + public Screen screen(int left, int top, int width, int height) { + assert left >= 0; + assert top >= 0; + return new BufferedScreen(this, 0, 0, width, height); } public Input input() { @@ -54,7 +57,7 @@ public Input input() { } @Override - public void flush() throws IOException { + public void flush() { terminal.flush(); } @@ -64,24 +67,12 @@ public void close() throws IOException { terminal.close(); } - public class TermResizeEvent implements Event { - private final Size size; - - public TermResizeEvent(Size size) { - this.size = size; - } - - public Size size() { - return size; - } - - @Override - public EventTarget target() { - return Term.this; - } + protected void handleResize(Terminal.Signal signal) { + onResize(size()); } - private void handleResize(Terminal.Signal signal) { - onResize.dispatch(new TermResizeEvent(size())); + @Override + public void onResize(Size newSize) { + resizeEmitter.dispatch(new ResizeEvent<>(newSize, this)); } } From c4b8c2d59ff49eac4c100515d9aa91295c85efba Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Tue, 18 Feb 2025 13:57:23 +0100 Subject: [PATCH 3/6] refactor: split out `Canvas` from `Screen` --- src/main/java/examples/FullPanel.java | 11 ++++---- .../context/render/BorderRenderer.java | 16 +++++------ .../codejive/context/render/BoxRenderer.java | 11 ++++---- .../org/codejive/context/terminal/Canvas.java | 9 +++++++ .../org/codejive/context/terminal/Screen.java | 7 +---- .../context/terminal/impl/InputImpl.java | 27 +++++++++++++++++++ 6 files changed, 57 insertions(+), 24 deletions(-) create mode 100644 src/main/java/org/codejive/context/terminal/Canvas.java create mode 100644 src/main/java/org/codejive/context/terminal/impl/InputImpl.java diff --git a/src/main/java/examples/FullPanel.java b/src/main/java/examples/FullPanel.java index 7b54536..d8b3848 100644 --- a/src/main/java/examples/FullPanel.java +++ b/src/main/java/examples/FullPanel.java @@ -8,6 +8,7 @@ import java.util.Collections; import org.codejive.context.render.BorderRenderer; import org.codejive.context.render.Box; +import org.codejive.context.terminal.Canvas; import org.codejive.context.terminal.Screen; import org.codejive.context.terminal.Term; import org.jline.utils.AttributedStringBuilder; @@ -35,17 +36,17 @@ public int run() throws IOException { return 0; } - private void draw(Screen screen) throws IOException { - int displayWidth = screen.rect().width(); - int displayHeight = screen.rect().height(); + private void draw(Canvas canvas) { + int displayWidth = canvas.rect().width(); + int displayHeight = canvas.rect().height(); Box b = createBox(displayWidth, displayHeight); - BorderRenderer br = new BorderRenderer(screen); + BorderRenderer br = new BorderRenderer(canvas); br.render(b); AttributedStringBuilder cb = new AttributedStringBuilder(); cb.append(displayWidth + "x" + displayHeight); - screen.printAt( + canvas.printAt( displayWidth / 2 - cb.length() / 2, displayHeight / 2, cb.toAttributedString()); } diff --git a/src/main/java/org/codejive/context/render/BorderRenderer.java b/src/main/java/org/codejive/context/render/BorderRenderer.java index ef9df6f..ae11422 100644 --- a/src/main/java/org/codejive/context/render/BorderRenderer.java +++ b/src/main/java/org/codejive/context/render/BorderRenderer.java @@ -1,15 +1,15 @@ package org.codejive.context.render; +import org.codejive.context.terminal.Canvas; import org.codejive.context.terminal.Rect; -import org.codejive.context.terminal.Screen; import org.codejive.context.util.Util; import org.jline.utils.AttributedString; public class BorderRenderer { - private final Screen screen; + private final Canvas canvas; - public BorderRenderer(Screen screen) { - this.screen = screen; + public BorderRenderer(Canvas canvas) { + this.canvas = canvas; } public void render(Box box) { @@ -23,7 +23,7 @@ public void render(Box box) { } // Calculate the rectangle to draw the border in Rect r = box.rect().grow(lw, tw, rw, bw); - if (r.outside(screen.rect())) { + if (r.outside(canvas.rect())) { return; } @@ -33,10 +33,10 @@ public void render(Box box) { int h = r.height(); AttributedString tbs = new AttributedString(String.format("+%s+", Util.repeat("-", w - 2))); AttributedString ins = new AttributedString(String.format("|%s|", Util.repeat(" ", w - 2))); - screen.printAt(x, y, tbs); + canvas.printAt(x, y, tbs); for (int i = 1; i < h - 1; i++) { - screen.printAt(x, y + i, ins); + canvas.printAt(x, y + i, ins); } - screen.printAt(x, y + h - 1, tbs); + canvas.printAt(x, y + h - 1, tbs); } } diff --git a/src/main/java/org/codejive/context/render/BoxRenderer.java b/src/main/java/org/codejive/context/render/BoxRenderer.java index 0a6098e..e36c84e 100644 --- a/src/main/java/org/codejive/context/render/BoxRenderer.java +++ b/src/main/java/org/codejive/context/render/BoxRenderer.java @@ -1,24 +1,25 @@ package org.codejive.context.render; +import org.codejive.context.terminal.Canvas; import org.codejive.context.terminal.Screen; import org.jline.utils.AttributedString; public class BoxRenderer { - private final Screen screen; + private final Canvas canvas; - public BoxRenderer(Screen screen) { - this.screen = screen; + public BoxRenderer(Canvas canvas) { + this.canvas = canvas; } public void render(Box box) { - if (box.rect().outside(screen.rect())) { + if (box.rect().outside(canvas.rect())) { return; } int x = box.left(); int y = box.top(); int i = 0; for (AttributedString str : box.content()) { - screen.printAt(x, y + i, str); + canvas.printAt(x, y + i, str); i++; } } diff --git a/src/main/java/org/codejive/context/terminal/Canvas.java b/src/main/java/org/codejive/context/terminal/Canvas.java new file mode 100644 index 0000000..48f3505 --- /dev/null +++ b/src/main/java/org/codejive/context/terminal/Canvas.java @@ -0,0 +1,9 @@ +package org.codejive.context.terminal; + +import org.jline.utils.AttributedString; + +public interface Canvas extends Rectangular { + void printAt(int x, int y, AttributedString str); + + void clear(); +} diff --git a/src/main/java/org/codejive/context/terminal/Screen.java b/src/main/java/org/codejive/context/terminal/Screen.java index a046412..09e99d0 100644 --- a/src/main/java/org/codejive/context/terminal/Screen.java +++ b/src/main/java/org/codejive/context/terminal/Screen.java @@ -1,12 +1,7 @@ package org.codejive.context.terminal; import org.codejive.context.events.EventTarget; -import org.jline.utils.AttributedString; - -public interface Screen extends Rectangular, EventTarget, Resizeable { - void printAt(int x, int y, AttributedString str); - - void clear(); +public interface Screen extends Canvas, EventTarget, Resizeable { void update(); } diff --git a/src/main/java/org/codejive/context/terminal/impl/InputImpl.java b/src/main/java/org/codejive/context/terminal/impl/InputImpl.java new file mode 100644 index 0000000..52a2a95 --- /dev/null +++ b/src/main/java/org/codejive/context/terminal/impl/InputImpl.java @@ -0,0 +1,27 @@ +package org.codejive.context.terminal.impl; + +import org.codejive.context.terminal.Input; +import org.codejive.context.terminal.Term; +import org.jline.utils.NonBlockingReader; + +import java.io.IOException; + +public class InputImpl implements Input { + private final Term term; + private final NonBlockingReader reader; + + public InputImpl(Term term) { + this.term = term; + this.reader = term.terminal.reader(); + } + + @Override + public int readChar() throws IOException { + return readChar(0); + } + + @Override + public int readChar(long timeout) throws IOException { + return reader.read(timeout); + } +} From 4cac303df9b884dd0643d84c68b5a6fe8eb6010c Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Tue, 18 Feb 2025 22:44:41 +0100 Subject: [PATCH 4/6] Split into interface and implementation --- .../org/codejive/context/events/Event.java | 2 +- .../codejive/context/events/ResizeEvent.java | 2 +- .../codejive/context/render/BoxRenderer.java | 1 - .../org/codejive/context/terminal/Input.java | 21 ------ .../org/codejive/context/terminal/Term.java | 69 +++++-------------- .../terminal/{ => impl}/BufferedScreen.java | 11 +-- .../context/terminal/impl/InputImpl.java | 8 +-- .../context/terminal/impl/JlineTerm.java | 66 ++++++++++++++++++ 8 files changed, 93 insertions(+), 87 deletions(-) rename src/main/java/org/codejive/context/terminal/{ => impl}/BufferedScreen.java (91%) create mode 100644 src/main/java/org/codejive/context/terminal/impl/JlineTerm.java diff --git a/src/main/java/org/codejive/context/events/Event.java b/src/main/java/org/codejive/context/events/Event.java index 36cae0d..f21b512 100644 --- a/src/main/java/org/codejive/context/events/Event.java +++ b/src/main/java/org/codejive/context/events/Event.java @@ -1,5 +1,5 @@ package org.codejive.context.events; -public interface Event { +public interface Event { T target(); } diff --git a/src/main/java/org/codejive/context/events/ResizeEvent.java b/src/main/java/org/codejive/context/events/ResizeEvent.java index f138090..5416c9c 100644 --- a/src/main/java/org/codejive/context/events/ResizeEvent.java +++ b/src/main/java/org/codejive/context/events/ResizeEvent.java @@ -2,7 +2,7 @@ import org.codejive.context.terminal.Size; -public class ResizeEvent implements Event { +public class ResizeEvent implements Event { private final T target; private final Size size; diff --git a/src/main/java/org/codejive/context/render/BoxRenderer.java b/src/main/java/org/codejive/context/render/BoxRenderer.java index e36c84e..e10547a 100644 --- a/src/main/java/org/codejive/context/render/BoxRenderer.java +++ b/src/main/java/org/codejive/context/render/BoxRenderer.java @@ -1,7 +1,6 @@ package org.codejive.context.render; import org.codejive.context.terminal.Canvas; -import org.codejive.context.terminal.Screen; import org.jline.utils.AttributedString; public class BoxRenderer { diff --git a/src/main/java/org/codejive/context/terminal/Input.java b/src/main/java/org/codejive/context/terminal/Input.java index 4ef3bd4..e76949b 100644 --- a/src/main/java/org/codejive/context/terminal/Input.java +++ b/src/main/java/org/codejive/context/terminal/Input.java @@ -1,30 +1,9 @@ package org.codejive.context.terminal; import java.io.IOException; -import org.jline.utils.NonBlockingReader; public interface Input { int readChar() throws IOException; int readChar(long timeout) throws IOException; } - -class InputImpl implements Input { - private final Term term; - private final NonBlockingReader reader; - - public InputImpl(Term term) { - this.term = term; - this.reader = term.terminal.reader(); - } - - @Override - public int readChar() throws IOException { - return readChar(0); - } - - @Override - public int readChar(long timeout) throws IOException { - return reader.read(timeout); - } -} diff --git a/src/main/java/org/codejive/context/terminal/Term.java b/src/main/java/org/codejive/context/terminal/Term.java index 48129d0..c505959 100644 --- a/src/main/java/org/codejive/context/terminal/Term.java +++ b/src/main/java/org/codejive/context/terminal/Term.java @@ -3,76 +3,39 @@ import java.io.Closeable; import java.io.Flushable; import java.io.IOException; -import org.codejive.context.events.EventEmitter; -import org.codejive.context.events.EventTarget; -import org.codejive.context.events.ResizeEvent; -import org.jline.terminal.Attributes; -import org.jline.terminal.Terminal; -import org.jline.terminal.TerminalBuilder; -import org.jline.utils.InfoCmp; +import org.codejive.context.terminal.impl.JlineTerm; -public class Term implements Flushable, Closeable, Resizeable, EventTarget { - protected final Terminal terminal; - protected final EventEmitter> resizeEmitter = new EventEmitter<>(); - protected final Attributes savedAttributes; - - public static Term create() throws IOException { - return new Term(); - } - - private Term() throws IOException { - terminal = TerminalBuilder.builder().build(); - savedAttributes = terminal.enterRawMode(); - terminal.handle(Terminal.Signal.WINCH, this::handleResize); - } - - public Size size() { - return new Size(terminal.getSize().getColumns(), terminal.getSize().getRows()); - } - - public Integer maxColors() { - return terminal.getNumericCapability(InfoCmp.Capability.max_colors); +public interface Term extends Flushable, Closeable, Resizeable { + static Term create() throws IOException { + return new JlineTerm(); } - public Screen fullScreen() { + default Screen fullScreen() { return sizedScreen(-1, -1); } - public Screen wideScreen(int height) { + default Screen wideScreen(int height) { return sizedScreen(-1, height); } - public Screen sizedScreen(int width, int height) { + default Screen sizedScreen(int width, int height) { return screen(0, 0, width, height); } - public Screen screen(int left, int top, int width, int height) { - assert left >= 0; - assert top >= 0; - return new BufferedScreen(this, 0, 0, width, height); - } + Screen screen(int left, int top, int width, int height); - public Input input() { - return new InputImpl(this); - } + Size size(); - @Override - public void flush() { - terminal.flush(); - } + Integer maxColors(); + + Input input(); @Override - public void close() throws IOException { - terminal.setAttributes(savedAttributes); - terminal.close(); - } + void flush(); - protected void handleResize(Terminal.Signal signal) { - onResize(size()); - } + @Override + void close() throws IOException; @Override - public void onResize(Size newSize) { - resizeEmitter.dispatch(new ResizeEvent<>(newSize, this)); - } + void onResize(Size newSize); } diff --git a/src/main/java/org/codejive/context/terminal/BufferedScreen.java b/src/main/java/org/codejive/context/terminal/impl/BufferedScreen.java similarity index 91% rename from src/main/java/org/codejive/context/terminal/BufferedScreen.java rename to src/main/java/org/codejive/context/terminal/impl/BufferedScreen.java index ff05966..9024cc9 100644 --- a/src/main/java/org/codejive/context/terminal/BufferedScreen.java +++ b/src/main/java/org/codejive/context/terminal/impl/BufferedScreen.java @@ -1,17 +1,18 @@ -package org.codejive.context.terminal; +package org.codejive.context.terminal.impl; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import org.codejive.context.events.EventEmitter; import org.codejive.context.events.ResizeEvent; +import org.codejive.context.terminal.*; import org.jline.utils.AttributedCharSequence; import org.jline.utils.AttributedString; import org.jline.utils.AttributedStringBuilder; import org.jline.utils.Display; public class BufferedScreen implements Screen { - protected final Term term; + protected final JlineTerm term; protected final FlexRect flexRect; protected final Display display; protected final EventEmitter> resizeEmitter = new EventEmitter<>(); @@ -23,11 +24,11 @@ public Rect rect() { return flexRect.actualRect(term.size()); } - protected BufferedScreen(Term term, int left, int top, int width, int height) { + BufferedScreen(JlineTerm term, int left, int top, int width, int height) { this(term, new FlexRect(left, top, width, height)); } - protected BufferedScreen(Term term, FlexRect flexRect) { + BufferedScreen(JlineTerm term, FlexRect flexRect) { this.term = term; this.flexRect = flexRect; this.display = new Display(term.terminal, false); @@ -94,7 +95,7 @@ public void update() { display.update(lines(), 0); } - protected void handleTermResizeEvent(ResizeEvent event) { + protected void handleTermResizeEvent(ResizeEvent event) { handleResize(event.size()); } diff --git a/src/main/java/org/codejive/context/terminal/impl/InputImpl.java b/src/main/java/org/codejive/context/terminal/impl/InputImpl.java index 52a2a95..379b126 100644 --- a/src/main/java/org/codejive/context/terminal/impl/InputImpl.java +++ b/src/main/java/org/codejive/context/terminal/impl/InputImpl.java @@ -1,16 +1,14 @@ package org.codejive.context.terminal.impl; +import java.io.IOException; import org.codejive.context.terminal.Input; -import org.codejive.context.terminal.Term; import org.jline.utils.NonBlockingReader; -import java.io.IOException; - public class InputImpl implements Input { - private final Term term; + private final JlineTerm term; private final NonBlockingReader reader; - public InputImpl(Term term) { + public InputImpl(JlineTerm term) { this.term = term; this.reader = term.terminal.reader(); } diff --git a/src/main/java/org/codejive/context/terminal/impl/JlineTerm.java b/src/main/java/org/codejive/context/terminal/impl/JlineTerm.java new file mode 100644 index 0000000..700dcf9 --- /dev/null +++ b/src/main/java/org/codejive/context/terminal/impl/JlineTerm.java @@ -0,0 +1,66 @@ +package org.codejive.context.terminal.impl; + +import java.io.IOException; +import org.codejive.context.events.EventEmitter; +import org.codejive.context.events.EventTarget; +import org.codejive.context.events.ResizeEvent; +import org.codejive.context.terminal.Input; +import org.codejive.context.terminal.Screen; +import org.codejive.context.terminal.Size; +import org.codejive.context.terminal.Term; +import org.jline.terminal.Attributes; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.jline.utils.InfoCmp; + +public class JlineTerm implements EventTarget, Term { + protected final Terminal terminal; + protected final EventEmitter> resizeEmitter = new EventEmitter<>(); + protected final Attributes savedAttributes; + + public JlineTerm() throws IOException { + terminal = TerminalBuilder.builder().build(); + savedAttributes = terminal.enterRawMode(); + terminal.handle(Terminal.Signal.WINCH, this::handleResize); + } + + @Override + public Screen screen(int left, int top, int width, int height) { + return new BufferedScreen(this, left, top, width, height); + } + + @Override + public Size size() { + return new Size(terminal.getSize().getColumns(), terminal.getSize().getRows()); + } + + @Override + public Integer maxColors() { + return terminal.getNumericCapability(InfoCmp.Capability.max_colors); + } + + @Override + public Input input() { + return new InputImpl(this); + } + + @Override + public void flush() { + terminal.flush(); + } + + @Override + public void close() throws IOException { + terminal.setAttributes(savedAttributes); + terminal.close(); + } + + protected void handleResize(Terminal.Signal signal) { + onResize(size()); + } + + @Override + public void onResize(Size newSize) { + resizeEmitter.dispatch(new ResizeEvent<>(newSize, this)); + } +} From 9eeaa3c59412a1c479106265ce4b76230c28cbb9 Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Thu, 17 Apr 2025 23:05:11 +0200 Subject: [PATCH 5/6] refactor: major refactor into modules Introduced modules for core, ansi, chart, and tui functionality. Project name change from 'context' to 'twinkle'. --- .gitignore | 2 +- README.md | 24 +- app.yml | 16 + jbang-catalog.json | 16 + jreleaser.yml | 69 +++ pom.xml | 97 ++-- .../codejive/context/events/EventTarget.java | 3 - .../org/codejive/context/terminal/Canvas.java | 9 - .../context/terminal/Rectangular.java | 5 - .../codejive/context/terminal/Resizeable.java | 5 - .../org/codejive/context/terminal/Screen.java | 7 - twinkle-ansi/pom.xml | 20 + .../java/org/codejive/twinkle/ansi/Ansi.java | 113 +++++ .../java/org/codejive/twinkle/ansi/Color.java | 372 +++++++++++++++ .../java/org/codejive/twinkle/ansi/Style.java | 436 ++++++++++++++++++ .../org/codejive/twinkle/ansi/TestColor.java | 276 +++++++++++ .../org/codejive/twinkle/ansi/TestStyle.java | 226 +++++++++ twinkle-chart/pom.xml | 29 ++ .../twinkle/components/graphs/bar/Bar.java | 31 ++ .../components/graphs/bar/BarConfig.java | 77 ++++ .../components/graphs/bar/FracBarConfig.java | 55 +++ .../graphs/bar/FracBarRenderer.java | 146 ++++++ .../components/graphs/plot/MathPlot.java | 271 +++++++++++ .../twinkle/components/graphs/plot/Plot.java | 175 +++++++ .../src/test/java/examples/BarDemo.java | 65 +++ .../src/test/java/examples/MathPlotDemo.java | 14 + twinkle-core/pom.xml | 29 ++ .../twinkle/core/component/Canvas.java | 41 ++ .../twinkle/core/component/Component.java | 5 + .../twinkle/core/component}/FlexRect.java | 6 +- .../twinkle/core/component/Panel.java | 40 ++ .../twinkle/core/component/PanelView.java | 7 + .../twinkle/core/component}/Rect.java | 35 +- .../twinkle/core/component/Rectangular.java | 7 + .../twinkle/core/component/Renderable.java | 5 + .../twinkle/core/component}/Size.java | 7 +- .../twinkle/core/component/Sized.java | 7 + .../core/component/StyledBufferPanel.java | 284 ++++++++++++ .../twinkle/core/components/Frame.java | 38 ++ .../twinkle/core/text/StyledBuffer.java | 82 ++++ .../twinkle/core/text/StyledCharSequence.java | 43 ++ .../core/text/StyledCodepointBuffer.java | 299 ++++++++++++ .../core/text/StyledStringBuilder.java | 170 +++++++ .../core/text/StyledBufferTimings.java | 93 ++++ .../codejive/twinkle/core/text/TestPanel.java | 206 +++++++++ .../twinkle/core/text/TestStyledBuffer.java | 105 +++++ twinkle-tui/pom.xml | 59 +++ .../src}/main/java/examples/Boxes.java | 14 +- .../src}/main/java/examples/FullPanel.java | 10 +- .../src}/main/java/examples/InlinePanel.java | 8 +- .../src}/main/java/examples/SimpleDom.java | 16 +- .../src}/main/java/examples/Util.java | 10 +- .../tui}/ciml/dom/ContextDocument.java | 4 +- .../twinkle/tui}/ciml/dom/ContextElement.java | 4 +- .../twinkle/tui}/ciml/dom/PanelElement.java | 2 +- .../twinkle/tui}/ciml/dom/ScreenElement.java | 2 +- .../twinkle/tui}/ciml/layout/DomLayouter.java | 6 +- .../codejive/twinkle/tui}/events/Event.java | 2 +- .../twinkle/tui}/events/EventEmitter.java | 2 +- .../twinkle/tui}/events/EventListener.java | 2 +- .../twinkle/tui/events/EventTarget.java | 3 + .../twinkle/tui}/events/ResizeEvent.java | 4 +- .../twinkle/tui}/render/BorderRenderer.java | 8 +- .../org/codejive/twinkle/tui}/render/Box.java | 10 +- .../twinkle/tui}/render/BoxRenderer.java | 4 +- .../twinkle/tui}/styles/CascadingStyle.java | 2 +- .../twinkle/tui}/styles/Property.java | 4 +- .../codejive/twinkle/tui}/styles/Style.java | 2 +- .../codejive/twinkle/tui}/styles/Type.java | 2 +- .../codejive/twinkle/tui}/styles/Unit.java | 2 +- .../codejive/twinkle/tui}/styles/Value.java | 12 +- .../codejive/twinkle/tui}/terminal/Input.java | 2 +- .../twinkle/tui/terminal/Resizeable.java | 7 + .../codejive/twinkle/tui/terminal/Screen.java | 8 + .../codejive/twinkle/tui}/terminal/Term.java | 8 +- .../tui}/terminal/impl/BufferedScreen.java | 13 +- .../twinkle/tui/terminal/impl/JlineInput.java | 8 +- .../twinkle/tui}/terminal/impl/JlineTerm.java | 18 +- .../twinkle/tui/util/EventEmittingReader.java | 228 +++++++++ .../twinkle/tui}/util/ScrollBuffer.java | 2 +- .../org/codejive/twinkle/tui}/util/Util.java | 2 +- 81 files changed, 4363 insertions(+), 195 deletions(-) create mode 100644 app.yml create mode 100644 jbang-catalog.json create mode 100644 jreleaser.yml delete mode 100644 src/main/java/org/codejive/context/events/EventTarget.java delete mode 100644 src/main/java/org/codejive/context/terminal/Canvas.java delete mode 100644 src/main/java/org/codejive/context/terminal/Rectangular.java delete mode 100644 src/main/java/org/codejive/context/terminal/Resizeable.java delete mode 100644 src/main/java/org/codejive/context/terminal/Screen.java create mode 100644 twinkle-ansi/pom.xml create mode 100644 twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Ansi.java create mode 100644 twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Color.java create mode 100644 twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java create mode 100644 twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestColor.java create mode 100644 twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestStyle.java create mode 100644 twinkle-chart/pom.xml create mode 100644 twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/bar/Bar.java create mode 100644 twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/bar/BarConfig.java create mode 100644 twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/bar/FracBarConfig.java create mode 100644 twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/bar/FracBarRenderer.java create mode 100644 twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/plot/MathPlot.java create mode 100644 twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/plot/Plot.java create mode 100644 twinkle-chart/src/test/java/examples/BarDemo.java create mode 100644 twinkle-chart/src/test/java/examples/MathPlotDemo.java create mode 100644 twinkle-core/pom.xml create mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/component/Canvas.java create mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/component/Component.java rename {src/main/java/org/codejive/context/terminal => twinkle-core/src/main/java/org/codejive/twinkle/core/component}/FlexRect.java (83%) create mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/component/Panel.java create mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/component/PanelView.java rename {src/main/java/org/codejive/context/terminal => twinkle-core/src/main/java/org/codejive/twinkle/core/component}/Rect.java (65%) create mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/component/Rectangular.java create mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/component/Renderable.java rename {src/main/java/org/codejive/context/terminal => twinkle-core/src/main/java/org/codejive/twinkle/core/component}/Size.java (80%) create mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/component/Sized.java create mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/component/StyledBufferPanel.java create mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/components/Frame.java create mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledBuffer.java create mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCharSequence.java create mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCodepointBuffer.java create mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledStringBuilder.java create mode 100644 twinkle-core/src/test/java/org/codejive/twinkle/core/text/StyledBufferTimings.java create mode 100644 twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestPanel.java create mode 100644 twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestStyledBuffer.java create mode 100644 twinkle-tui/pom.xml rename {src => twinkle-tui/src}/main/java/examples/Boxes.java (90%) rename {src => twinkle-tui/src}/main/java/examples/FullPanel.java (85%) rename {src => twinkle-tui/src}/main/java/examples/InlinePanel.java (87%) rename {src => twinkle-tui/src}/main/java/examples/SimpleDom.java (73%) rename {src => twinkle-tui/src}/main/java/examples/Util.java (85%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/ciml/dom/ContextDocument.java (89%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/ciml/dom/ContextElement.java (97%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/ciml/dom/PanelElement.java (78%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/ciml/dom/ScreenElement.java (78%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/ciml/layout/DomLayouter.java (50%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/events/Event.java (53%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/events/EventEmitter.java (92%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/events/EventListener.java (67%) create mode 100644 twinkle-tui/src/main/java/org/codejive/twinkle/tui/events/EventTarget.java rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/events/ResizeEvent.java (79%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/render/BorderRenderer.java (86%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/render/Box.java (93%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/render/BoxRenderer.java (84%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/styles/CascadingStyle.java (97%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/styles/Property.java (94%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/styles/Style.java (97%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/styles/Type.java (89%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/styles/Unit.java (59%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/styles/Value.java (94%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/terminal/Input.java (77%) create mode 100644 twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/Resizeable.java create mode 100644 twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/Screen.java rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/terminal/Term.java (75%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/terminal/impl/BufferedScreen.java (89%) rename src/main/java/org/codejive/context/terminal/impl/InputImpl.java => twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/impl/JlineInput.java (71%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/terminal/impl/JlineTerm.java (78%) create mode 100644 twinkle-tui/src/main/java/org/codejive/twinkle/tui/util/EventEmittingReader.java rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/util/ScrollBuffer.java (99%) rename {src/main/java/org/codejive/context => twinkle-tui/src/main/java/org/codejive/twinkle/tui}/util/Util.java (86%) diff --git a/.gitignore b/.gitignore index 2ff9ee8..a1dcf10 100644 --- a/.gitignore +++ b/.gitignore @@ -24,10 +24,10 @@ hs_err_pid* .vscode .idea +*.iml target build lib out dependency-reduced-pom.xml - diff --git a/README.md b/README.md index 965c084..39041aa 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# Context +# Twinkle -Context is a Java library for creating advanced user interfaces inside a console window +Twinkle is a Java library for creating advanced text-based user interfaces. This is a very early proof of concept, nothing to see here (yet) @@ -10,20 +10,24 @@ This is a very early proof of concept, nothing to see here (yet) To build the project, run the following command: ```bash -mvn clean package +./mvnw clean package ``` ## Running -To see a simple demo of panels being drawn randomly on the screen, run the following command: +To see a couple of very early demos, run the following commands: ```bash -java -cp target/context-1.0-SNAPSHOT.jar +java -cp twinkle-chart/target/twinkle-chart-1.0-SNAPSHOT.jar:twinkle-core/target/twinkle-core-1.0-SNAPSHOT.jar:twinkle-ansi/target/twinkle-ansi-1.0-SNAPSHOT.jar:twinkle-chart/target/test-classes examples.BarDemo +java -cp twinkle-chart/target/twinkle-chart-1.0-SNAPSHOT.jar:twinkle-core/target/twinkle-core-1.0-SNAPSHOT.jar:twinkle-ansi/target/twinkle-ansi-1.0-SNAPSHOT.jar:twinkle-chart/target/test-classes examples.MathPlotDemo ``` -Where `` is one of the following: +An easier way to run the demos is using [JBang](https://www.jbang.dev/): -- examples.Boxes -- examples.FullPanel -- examples.InlinePanel -- examples.SimpleDom +```bash +./mvnw install -DskipTests +jbang run examples/BarDemo.java +jbang run examples/MathPlotDemo.java +``` + +These demos only show Twinkle's Ansi output capabilities. There is no interactivity being shown. diff --git a/app.yml b/app.yml new file mode 100644 index 0000000..c9a8e4e --- /dev/null +++ b/app.yml @@ -0,0 +1,16 @@ +name: twinkle +description: Twinkle is a Java library for creating advanced text-based user interfaces. +authors: +- Tako Schotanus (tako@codejive.org) +links: + homepage: https://github.com/codejive/java-twinkle + repository: https://github.com/codejive/java-twinkle + documentation: https://github.com/codejive/java-twinkle/blob/main/README.md +java: 8 +dependencies: +actions: + clean: ./mvnw clean + build: ./mvnw spotless:apply package -DskipTests + test: ./mvnw test + runbar: java -cp twinkle-chart/target/twinkle-chart-1.0-SNAPSHOT.jar:twinkle-core/target/twinkle-core-1.0-SNAPSHOT.jar:twinkle-ansi/target/twinkle-ansi-1.0-SNAPSHOT.jar:twinkle-chart/target/test-classes examples.BarDemo + runplot: java -cp twinkle-chart/target/twinkle-chart-1.0-SNAPSHOT.jar:twinkle-core/target/twinkle-core-1.0-SNAPSHOT.jar:twinkle-ansi/target/twinkle-ansi-1.0-SNAPSHOT.jar:twinkle-chart/target/test-classes examples.PlotDemo diff --git a/jbang-catalog.json b/jbang-catalog.json new file mode 100644 index 0000000..8b74789 --- /dev/null +++ b/jbang-catalog.json @@ -0,0 +1,16 @@ +{ + "aliases": { + "BarDemo": { + "script-ref": "twinkle-chart\\src\\test\\java\\examples\\BarDemo.java", + "dependencies": [ + "org.codejive.twinkle:twinkle-chart:1.0-SNAPSHOT" + ] + }, + "MathPlotDemo": { + "script-ref": "twinkle-chart\\src\\test\\java\\examples\\MathPlotDemo.java", + "dependencies": [ + "org.codejive.twinkle:twinkle-chart:1.0-SNAPSHOT" + ] + } + } +} \ No newline at end of file diff --git a/jreleaser.yml b/jreleaser.yml new file mode 100644 index 0000000..3fab020 --- /dev/null +++ b/jreleaser.yml @@ -0,0 +1,69 @@ +project: + name: twinkle + description: Java TUI Library + longDescription: | + Twinkle is a Java library for creating advanced text-based user interfaces. + authors: + - Tako Schotanus + tags: + - java + - tui + - console + license: Apache-2.0 + links: + homepage: https://github.com/codejive/java-twinkle + languages: + java: + groupId: org.codejive.twinkle + version: '8' + inceptionYear: '2025' + stereotype: NONE + +assemble: + javaArchive: + twinkle: + active: ALWAYS + formats: + - ZIP + - TGZ + mainJar: + path: 'target/{{distributionName}}-{{projectVersion}}.jar' + jars: + - pattern: 'target/binary/lib/*.jar' + fileSets: + - input: '.' + includes: + - 'LICENSE' + +deploy: + maven: + mavenCentral: + twinkle: + active: RELEASE + url: https://central.sonatype.com/api/v1/publisher + stagingRepositories: + - target/staging-deploy + +release: + github: + owner: codejive + name: java-twinkle + overwrite: true + changelog: + formatted: ALWAYS + preset: conventional-commits + contributors: + format: '- {{contributorName}}{{#contributorUsernameAsLink}} ({{.}}){{/contributorUsernameAsLink}}' + +checksum: + individual: true + +signing: + active: ALWAYS + armored: true + +distributions: + twinkle: + artifacts: + - path: target/jreleaser/assemble/{{distributionName}}/java-archive/{{distributionName}}-{{projectVersion}}.zip + - path: target/jreleaser/assemble/{{distributionName}}/java-archive/{{distributionName}}-{{projectVersion}}.zip diff --git a/pom.xml b/pom.xml index 287c683..d42c262 100644 --- a/pom.xml +++ b/pom.xml @@ -4,46 +4,51 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - org.codejive - context + org.codejive.twinkle + twinkle 1.0-SNAPSHOT + pom - 11 - 11 UTF-8 - - 3.30.3 - 2.44.4 1.22.0 + 1.0.0 + 6.0.1 + 3.27.6 + + twinkle-core + twinkle-ansi + twinkle-chart + + + - org.jline - jline-terminal - ${version.jline} + org.jspecify + jspecify + ${version.jspecify} + + - org.jline - jline-terminal-jansi - ${version.jline} + org.junit.jupiter + junit-jupiter + ${version.junit} + test + + + org.assertj + assertj-core + ${version.assertj} + test - + - - org.apache.maven.plugins - maven-compiler-plugin - - - --add-exports - java.xml/com.sun.org.apache.xerces.internal.dom=ALL-UNNAMED - - - com.diffplug.spotless spotless-maven-plugin @@ -53,7 +58,6 @@ **/*.md - **/*.txt .gitignore @@ -83,50 +87,35 @@ org.apache.maven.plugins - maven-shade-plugin - - - package - - shade - - - - - - examples.Boxes - - - - - - + maven-compiler-plugin + + 8 + 8 + 11 + 11 - - maven-clean-plugin - 3.4.1 + 3.5.0 - maven-resources-plugin - 3.3.1 + 3.4.0 maven-compiler-plugin - 3.14.0 + 3.14.1 maven-surefire-plugin - 3.5.3 + 3.5.4 maven-jar-plugin - 3.4.2 + 3.5.0 maven-install-plugin @@ -136,7 +125,6 @@ maven-deploy-plugin 3.1.4 - maven-site-plugin 3.21.0 @@ -147,13 +135,14 @@ maven-dependency-plugin - 3.8.1 + 3.9.0 maven-shade-plugin - 3.6.0 + 3.6.1 + \ No newline at end of file diff --git a/src/main/java/org/codejive/context/events/EventTarget.java b/src/main/java/org/codejive/context/events/EventTarget.java deleted file mode 100644 index 26aa51d..0000000 --- a/src/main/java/org/codejive/context/events/EventTarget.java +++ /dev/null @@ -1,3 +0,0 @@ -package org.codejive.context.events; - -public interface EventTarget {} diff --git a/src/main/java/org/codejive/context/terminal/Canvas.java b/src/main/java/org/codejive/context/terminal/Canvas.java deleted file mode 100644 index 48f3505..0000000 --- a/src/main/java/org/codejive/context/terminal/Canvas.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.codejive.context.terminal; - -import org.jline.utils.AttributedString; - -public interface Canvas extends Rectangular { - void printAt(int x, int y, AttributedString str); - - void clear(); -} diff --git a/src/main/java/org/codejive/context/terminal/Rectangular.java b/src/main/java/org/codejive/context/terminal/Rectangular.java deleted file mode 100644 index d0a139c..0000000 --- a/src/main/java/org/codejive/context/terminal/Rectangular.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.codejive.context.terminal; - -public interface Rectangular { - Rect rect(); -} diff --git a/src/main/java/org/codejive/context/terminal/Resizeable.java b/src/main/java/org/codejive/context/terminal/Resizeable.java deleted file mode 100644 index 8c899e1..0000000 --- a/src/main/java/org/codejive/context/terminal/Resizeable.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.codejive.context.terminal; - -public interface Resizeable { - void onResize(Size newSize); -} diff --git a/src/main/java/org/codejive/context/terminal/Screen.java b/src/main/java/org/codejive/context/terminal/Screen.java deleted file mode 100644 index 09e99d0..0000000 --- a/src/main/java/org/codejive/context/terminal/Screen.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.codejive.context.terminal; - -import org.codejive.context.events.EventTarget; - -public interface Screen extends Canvas, EventTarget, Resizeable { - void update(); -} diff --git a/twinkle-ansi/pom.xml b/twinkle-ansi/pom.xml new file mode 100644 index 0000000..cef7341 --- /dev/null +++ b/twinkle-ansi/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + + org.codejive.twinkle + twinkle + 1.0-SNAPSHOT + ../pom.xml + + + twinkle-ansi + jar + + Generic ANSI support classes + + + diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Ansi.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Ansi.java new file mode 100644 index 0000000..49b6fd7 --- /dev/null +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Ansi.java @@ -0,0 +1,113 @@ +package org.codejive.twinkle.ansi; + +public class Ansi { + public static final char ESC = 27; + + public static final String CSI = ESC + "["; // Control Sequence Introducer + + // Style codes + public static final int RESET = 0; // Reset all attributes + public static final int BOLD = 1; // Bold text + public static final int FAINT = 2; // Faint/dim text + public static final int ITALICIZED = 3; // Italic text + public static final int UNDERLINED = 4; // Underlined text + public static final int BLINK = 5; // Blinking text + public static final int INVERSE = 7; // Reversed foreground + public static final int INVISIBLE = 8; // Invisible text + public static final int CROSSEDOUT = 9; // Strike-through + public static final int DOUBLEUNDERLINE = 21; // Double underline + public static final int NORMAL = 22; // Normal intensity + public static final int NOTITALICIZED = 23; // Not italic + public static final int NOTUNDERLINED = 24; // Not underlined + public static final int STEADY = 25; // Not blinking + public static final int POSITIVE = 27; // Positive image + public static final int VISIBLE = 28; // Visible text + public static final int NOTCROSSEDOUT = 29; // Not strike-through + public static final int DEFAULT_FOREGROUND = 39; + public static final int DEFAULT_BACKGROUND = 49; + + public static final String STYLE_RESET = style(RESET); // Reset all attributes + public static final String STYLE_DEFAULT_FOREGROUND = + style(DEFAULT_FOREGROUND); // Reset all attributes + public static final String STYLE_DEFAULT_BACKGROUND = + style(DEFAULT_BACKGROUND); // Reset all attributes + + public static final int BLACK = 0; + public static final int RED = 1; + public static final int GREEN = 2; + public static final int YELLOW = 3; + public static final int BLUE = 4; + public static final int MAGENTA = 5; + public static final int CYAN = 6; + public static final int WHITE = 7; + + public static final int FOREGROUND_BASE = 30; + public static final int FOREGROUND_DARK_BASE = 60; + public static final int FOREGROUND_BRIGHT_BASE = 90; + public static final int BACKGROUND_BASE = 40; + public static final int BACKGROUND_DARK_BASE = 70; + public static final int BACKGROUND_BRIGHT_BASE = 100; + + public static final int FOREGROUND_COLORS = 38; + public static final int BACKGROUND_COLORS = 48; + public static final int COLORS_RGB = 2; + public static final int COLORS_INDEXED = 5; + + public static String style(Object... styles) { + if (styles == null || styles.length == 0) { + return ""; + } + StringBuilder sb = new StringBuilder(); + sb.append(CSI); + for (int i = 0; i < styles.length; i++) { + sb.append(styles[i]); + if (i < styles.length - 1) { + sb.append(";"); + } + } + sb.append("m"); + return sb.toString(); + } + + public static String foreground(int index) { + return String.valueOf(FOREGROUND_BASE + index); + } + + public static String foregroundDark(int index) { + return String.valueOf(FOREGROUND_DARK_BASE + index); + } + + public static String foregroundBright(int index) { + return String.valueOf(FOREGROUND_BRIGHT_BASE + index); + } + + public static String background(int index) { + return String.valueOf(BACKGROUND_BASE + index); + } + + public static String backgroundDark(int index) { + return String.valueOf(BACKGROUND_DARK_BASE + index); + } + + public static String backgroundBright(int index) { + return String.valueOf(BACKGROUND_BRIGHT_BASE + index); + } + + public static String foregroundIndexed(int index) { + return FOREGROUND_COLORS + ":" + COLORS_INDEXED + ":" + index; + } + + public static String foregroundRgb(int r, int g, int b) { + return FOREGROUND_COLORS + ":" + COLORS_RGB + ":" + r + ":" + g + ":" + b; + } + + public static String backgroundIndexed(int index) { + return BACKGROUND_COLORS + ":" + COLORS_INDEXED + ":" + index; + } + + public static String backgroundRgb(int r, int g, int b) { + return BACKGROUND_COLORS + ":" + COLORS_RGB + ":" + r + ":" + g + ":" + b; + } + + private Ansi() {} +} diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Color.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Color.java new file mode 100644 index 0000000..be42200 --- /dev/null +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Color.java @@ -0,0 +1,372 @@ +package org.codejive.twinkle.ansi; + +import static org.codejive.twinkle.ansi.Ansi.*; + +import java.util.Objects; + +public interface Color { + + Color DEFAULT = DefaultColor.instance(); + + static BasicColor basic(int index) { + return BasicColor.byIndex(index); + } + + static BasicColor basic(int index, BasicColor.Intensity intensity) { + return BasicColor.byIndex(index, intensity); + } + + static IndexedColor indexed(int index) { + return IndexedColor.of(index); + } + + static RgbColor rgb(int r, int g, int b) { + return RgbColor.of(r, g, b); + } + + String toAnsiFg(); + + String toAnsiBg(); + + class DefaultColor implements Color { + private static final DefaultColor INSTANCE = new DefaultColor(); + + private DefaultColor() {} + + protected static Color instance() { + return INSTANCE; + } + + @Override + public String toAnsiFg() { + return STYLE_DEFAULT_FOREGROUND; + } + + @Override + public String toAnsiBg() { + return STYLE_DEFAULT_BACKGROUND; + } + } + + class BasicColor implements Color { + private final String name; + private final int index; + private final Intensity intensity; + private final String fgAnsi; + private final String bgAnsi; + + public enum Intensity { + normal, + dark, + bright; + } + + public static final BasicColor BLACK = BasicColor.of("black", Ansi.BLACK, Intensity.normal); + public static final BasicColor RED = BasicColor.of("red", Ansi.RED, Intensity.normal); + public static final BasicColor GREEN = BasicColor.of("green", Ansi.GREEN, Intensity.normal); + public static final BasicColor YELLOW = + BasicColor.of("yellow", Ansi.YELLOW, Intensity.normal); + public static final BasicColor BLUE = BasicColor.of("blue", Ansi.BLUE, Intensity.normal); + public static final BasicColor MAGENTA = + BasicColor.of("magenta", Ansi.MAGENTA, Intensity.normal); + public static final BasicColor CYAN = BasicColor.of("cyan", Ansi.CYAN, Intensity.normal); + public static final BasicColor WHITE = BasicColor.of("white", Ansi.WHITE, Intensity.normal); + + public static final BasicColor DARK_BLACK = + BasicColor.of("black", Ansi.BLACK, Intensity.dark); + public static final BasicColor DARK_RED = BasicColor.of("red", Ansi.RED, Intensity.dark); + public static final BasicColor DARK_GREEN = + BasicColor.of("green", Ansi.GREEN, Intensity.dark); + public static final BasicColor DARK_YELLOW = + BasicColor.of("yellow", Ansi.YELLOW, Intensity.dark); + public static final BasicColor DARK_BLUE = BasicColor.of("blue", Ansi.BLUE, Intensity.dark); + public static final BasicColor DARK_MAGENTA = + BasicColor.of("magenta", Ansi.MAGENTA, Intensity.dark); + public static final BasicColor DARK_CYAN = BasicColor.of("cyan", Ansi.CYAN, Intensity.dark); + public static final BasicColor DARK_WHITE = + BasicColor.of("white", Ansi.WHITE, Intensity.dark); + + public static final BasicColor BRIGHT_BLACK = + BasicColor.of("black", Ansi.BLACK, Intensity.bright); + public static final BasicColor BRIGHT_RED = + BasicColor.of("red", Ansi.RED, Intensity.bright); + public static final BasicColor BRIGHT_GREEN = + BasicColor.of("green", Ansi.GREEN, Intensity.bright); + public static final BasicColor BRIGHT_YELLOW = + BasicColor.of("yellow", Ansi.YELLOW, Intensity.bright); + public static final BasicColor BRIGHT_BLUE = + BasicColor.of("blue", Ansi.BLUE, Intensity.bright); + public static final BasicColor BRIGHT_MAGENTA = + BasicColor.of("magenta", Ansi.MAGENTA, Intensity.bright); + public static final BasicColor BRIGHT_CYAN = + BasicColor.of("cyan", Ansi.CYAN, Intensity.bright); + public static final BasicColor BRIGHT_WHITE = + BasicColor.of("white", Ansi.WHITE, Intensity.bright); + + private static final BasicColor[] normalColors = { + BLACK, RED, GREEN, YELLOW, + BLUE, MAGENTA, CYAN, WHITE + }; + + private static final BasicColor[] darkColors = { + DARK_BLACK, DARK_RED, DARK_GREEN, DARK_YELLOW, + DARK_BLUE, DARK_MAGENTA, DARK_CYAN, DARK_WHITE + }; + + private static final BasicColor[] brightColors = { + BRIGHT_BLACK, BRIGHT_RED, BRIGHT_GREEN, BRIGHT_YELLOW, + BRIGHT_BLUE, BRIGHT_MAGENTA, BRIGHT_CYAN, BRIGHT_WHITE + }; + + protected static BasicColor of(String name, int index, Intensity intensity) { + return new BasicColor(name, index, intensity); + } + + public static BasicColor byIndex(int index) { + return normalColors[index]; + } + + public static BasicColor byIndex(int index, Intensity intensity) { + switch (intensity) { + case dark: + return darkColors[index]; + case bright: + return brightColors[index]; + default: + return normalColors[index]; + } + } + + private BasicColor(String name, int index, Intensity intensity) { + if (index < 0 || index > 7) { + throw new IllegalArgumentException( + "Color index must be between 0 and 7, got: " + index); + } + this.name = name; + this.index = index; + this.intensity = intensity; + fgAnsi = fgAnsi(index, intensity); + bgAnsi = bgAnsi(index, intensity); + } + + public int index() { + return index; + } + + public Intensity intensity() { + return intensity; + } + + public BasicColor normal() { + if (intensity == Intensity.normal) { + return this; + } else { + return normalColors[index]; + } + } + + public BasicColor dark() { + if (intensity == Intensity.dark) { + return this; + } else { + return darkColors[index]; + } + } + + public BasicColor bright() { + if (intensity == Intensity.bright) { + return this; + } else { + return brightColors[index]; + } + } + + public String toAnsiFg() { + return fgAnsi; + } + + public String toAnsiBg() { + return bgAnsi; + } + + private static String fgAnsi(int index, Intensity intensity) { + switch (intensity) { + case normal: + return Ansi.style(Ansi.foreground(index)); + case dark: + return Ansi.style(Ansi.foregroundDark(index)); + case bright: + return Ansi.style(Ansi.foregroundBright(index)); + default: + throw new IllegalArgumentException("Unknown mode: " + intensity); + } + } + + private static String bgAnsi(int index, Intensity intensity) { + switch (intensity) { + case normal: + return Ansi.style(Ansi.background(index)); + case dark: + return Ansi.style(Ansi.backgroundDark(index)); + case bright: + return Ansi.style(Ansi.backgroundBright(index)); + default: + throw new IllegalArgumentException("Unknown mode: " + intensity); + } + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + BasicColor that = (BasicColor) o; + return index == that.index && intensity == that.intensity; + } + + @Override + public int hashCode() { + return Objects.hash(index, intensity); + } + + @Override + public String toString() { + switch (intensity) { + case normal: + return name; + case dark: + return "dark " + name; + case bright: + return "bright " + name; + default: + throw new IllegalArgumentException("Unknown mode: " + intensity); + } + } + } + + class IndexedColor implements Color { + private final int index; + private final String fgAnsi; + private final String bgAnsi; + + public static IndexedColor of(int index) { + return new IndexedColor(index); + } + + private IndexedColor(int index) { + if (index < 0 || index > 255) { + throw new IllegalArgumentException( + "Color index must be between 0 and 255, got: " + index); + } + this.index = index; + fgAnsi = fgAnsi(index); + bgAnsi = bgAnsi(index); + } + + public int index() { + return index; + } + + @Override + public String toAnsiFg() { + return fgAnsi; + } + + @Override + public String toAnsiBg() { + return bgAnsi; + } + + private static String fgAnsi(int index) { + return Ansi.style(Ansi.foregroundIndexed(index)); + } + + private static String bgAnsi(int index) { + return Ansi.style(Ansi.backgroundIndexed(index)); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + IndexedColor that = (IndexedColor) o; + return index == that.index; + } + + @Override + public int hashCode() { + return Objects.hashCode(index); + } + + @Override + public String toString() { + return "%" + index; + } + } + + class RgbColor implements Color { + private final int r, g, b; + private final String fgAnsi; + private final String bgAnsi; + + public static RgbColor of(int r, int g, int b) { + return new RgbColor(r, g, b); + } + + private RgbColor(int r, int g, int b) { + if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) { + throw new IllegalArgumentException( + "RGB values must be between 0 and 255, got: " + r + ", " + g + ", " + b); + } + this.r = r; + this.g = g; + this.b = b; + fgAnsi = fgAnsi(r, g, b); + bgAnsi = bgAnsi(r, g, b); + } + + public int r() { + return r; + } + + public int g() { + return g; + } + + public int b() { + return b; + } + + @Override + public String toAnsiFg() { + return fgAnsi; + } + + @Override + public String toAnsiBg() { + return bgAnsi; + } + + private static String fgAnsi(int r, int g, int b) { + return Ansi.style(Ansi.foregroundRgb(r, g, b)); + } + + private static String bgAnsi(int r, int g, int b) { + return Ansi.style(Ansi.backgroundRgb(r, g, b)); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + RgbColor rgbColor = (RgbColor) o; + return r == rgbColor.r && g == rgbColor.g && b == rgbColor.b; + } + + @Override + public int hashCode() { + return Objects.hash(r, g, b); + } + + @Override + public String toString() { + return String.format("#%02x%02x%02x", r, g, b); + } + } +} diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java new file mode 100644 index 0000000..1e3ee0d --- /dev/null +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java @@ -0,0 +1,436 @@ +package org.codejive.twinkle.ansi; + +import java.util.ArrayList; +import java.util.List; +import org.jspecify.annotations.NonNull; + +public class Style { + private final long state; + + private static final long IDX_BOLD = 0; + private static final long IDX_FAINT = 1; + private static final long IDX_ITALICIZED = 2; + private static final long IDX_UNDERLINED = 3; + private static final long IDX_BLINK = 4; + private static final long IDX_INVERSE = 5; + private static final long IDX_INVISIBLE = 6; + private static final long IDX_CROSSEDOUT = 7; + // private static final long DOUBLEUNDERLINE = 8; + + public static final long F_UNSTYLED = 0L; + public static final long F_BOLD = 1 << IDX_BOLD; + public static final long F_FAINT = 1 << IDX_FAINT; + public static final long F_ITALIC = 1 << IDX_ITALICIZED; + public static final long F_UNDERLINED = 1 << IDX_UNDERLINED; + public static final long F_BLINK = 1 << IDX_BLINK; + public static final long F_INVERSE = 1 << IDX_INVERSE; + public static final long F_HIDDEN = 1 << IDX_INVISIBLE; + public static final long F_STRIKETHROUGH = 1 << IDX_CROSSEDOUT; + + public static final Style UNSTYLED = new Style(0); + public static final Style BOLD = UNSTYLED.bold(); + public static final Style FAINT = UNSTYLED.faint(); + public static final Style ITALIC = UNSTYLED.italic(); + public static final Style UNDERLINED = UNSTYLED.underlined(); + public static final Style BLINK = UNSTYLED.blink(); + public static final Style INVERSE = UNSTYLED.inverse(); + public static final Style HIDDEN = UNSTYLED.hidden(); + public static final Style STRIKETHROUGH = UNSTYLED.strikethrough(); + + public static final String PROP_TOANSI = "twinkle.styledbuffer.toAnsi"; + + public static @NonNull Style ofFgColor(@NonNull Color color) { + return UNSTYLED.fgColor(color); + } + + public static @NonNull Style ofBgColor(@NonNull Color color) { + return UNSTYLED.bgColor(color); + } + + /* + * State bit encoding is as follows (64 bits): + * Bit 0: Bold + * Bit 1: Faint + * Bit 2: Italicized + * Bit 3: Underlined + * Bit 4: Blink + * Bit 5: Inverse + * Bit 6: Invisible + * Bit 7: Crossed-out + * Bits 8-13: Reserved + * Bits 14-38: Foreground color (see below) + * Bits 39-63: Background color (see below) + * + * Foreground/Background color encoding (25 bits): + * Bit 0: Color mode (0 = basic/indexed, 1 = RGB) + * + * If color mode is basic/indexed: + * Bit 1: Palette type (0 = basic, 1 = indexed) + * + * If palette type is basic: + * Bits 0-1: Intensity (0 = default, 1 = normal, 2 = dark, 3 = bright) + * Bits 2-4: Color index + * + * If palette type is indexed: + * Bits 1-8: Color index + * + * If color mode is RGB: + * Bits 1-8: Red component + * Bits 9-16: Green component + * Bits 17-24: Blue component + */ + + private static final int SHIFT_FG_COLOR = 14; + private static final int SHIFT_BG_COLOR = 39; + private static final int SHIFT_PALETTE_TYPE = 1; + private static final int SHIFT_COLOR_BASIC_INTENSITY = 2; + private static final int SHIFT_COLOR_BASIC_INDEX = 5; + private static final int SHIFT_COLOR_INDEXED_INDEX = 2; + private static final int SHIFT_COLOR_R = 1; + private static final int SHIFT_COLOR_G = 9; + private static final int SHIFT_COLOR_B = 17; + + private static final long MASK_COLOR = 0x01ffffffL; + private static final long MASK_FG_COLOR = MASK_COLOR << SHIFT_FG_COLOR; + private static final long MASK_BG_COLOR = MASK_COLOR << SHIFT_BG_COLOR; + private static final long MASK_COLOR_MODE = 0x01L; + private static final long MASK_PALETTE_TYPE = 0x01L; + private static final long MASK_COLOR_BASIC_INTENSITY = 0x03L; + private static final long MASK_COLOR_BASIC_INDEX = 0x07L; + private static final long MASK_COLOR_PART = 0xffL; + + private static final long CM_INDEXED = 0; + private static final long CM_RGB = 1; + + private static final long PALETTE_BASIC = 0; + private static final long PALETTE_INDEXED = 1; + + // Not really an intensity, but a flag to indicate default color, + // but we're (ab)using the intensity bits to store it + private static final long INTENSITY_DEFAULT = 0; + + private static final long INTENSITY_NORMAL = 1; + private static final long INTENSITY_DARK = 2; + private static final long INTENSITY_BRIGHT = 3; + + public static @NonNull Style of(long state) { + if (state == 0) { + return UNSTYLED; + } + return new Style(state); + } + + private Style(long state) { + this.state = state; + } + + public long state() { + return state; + } + + public @NonNull Style unstyled() { + return UNSTYLED; + } + + public @NonNull Style normal() { + return of(state & ~(F_BOLD | F_FAINT)); + } + + public boolean isBold() { + return (state & F_BOLD) != 0; + } + + public @NonNull Style bold() { + return of(state | F_BOLD); + } + + public boolean isFaint() { + return (state & F_FAINT) != 0; + } + + public @NonNull Style faint() { + return of(state | F_FAINT); + } + + public boolean isItalic() { + return (state & F_ITALIC) != 0; + } + + public @NonNull Style italic() { + return of(state | F_ITALIC); + } + + public @NonNull Style italicOff() { + return of(state & ~F_ITALIC); + } + + public boolean isUnderlined() { + return (state & F_UNDERLINED) != 0; + } + + public @NonNull Style underlined() { + return of(state | F_UNDERLINED); + } + + public @NonNull Style underlinedOff() { + return of(state & ~F_UNDERLINED); + } + + public boolean isBlink() { + return (state & F_BLINK) != 0; + } + + public @NonNull Style blink() { + return of(state | F_BLINK); + } + + public @NonNull Style blinkOff() { + return of(state & ~F_BLINK); + } + + public boolean isInverse() { + return (state & F_INVERSE) != 0; + } + + public @NonNull Style inverse() { + return of(state | F_INVERSE); + } + + public @NonNull Style inverseOff() { + return of(state & ~F_INVERSE); + } + + public boolean isHidden() { + return (state & F_HIDDEN) != 0; + } + + public @NonNull Style hidden() { + return of(state | F_HIDDEN); + } + + public @NonNull Style hiddenOff() { + return of(state & ~F_HIDDEN); + } + + public boolean isStrikethrough() { + return (state & F_STRIKETHROUGH) != 0; + } + + public @NonNull Style strikethrough() { + return of(state | F_STRIKETHROUGH); + } + + public @NonNull Style strikethroughOff() { + return of(state & ~F_STRIKETHROUGH); + } + + public @NonNull Color fgColor() { + long fgc = ((state & MASK_FG_COLOR) >> SHIFT_FG_COLOR); + return decodeColor(fgc); + } + + public @NonNull Style fgColor(@NonNull Color color) { + long newState = (state & ~MASK_FG_COLOR) | (encodeColor(color) << SHIFT_FG_COLOR); + return of(newState); + } + + public @NonNull Color bgColor() { + long bgc = ((state & MASK_BG_COLOR) >> SHIFT_BG_COLOR); + return decodeColor(bgc); + } + + public @NonNull Style bgColor(@NonNull Color color) { + long newState = (state & ~MASK_BG_COLOR) | (encodeColor(color) << SHIFT_BG_COLOR); + return of(newState); + } + + private static long encodeColor(@NonNull Color color) { + long result = 0; + if (color instanceof Color.RgbColor) { + Color.RgbColor rgbColor = (Color.RgbColor) color; + result |= CM_RGB; + result |= ((long) rgbColor.r() & MASK_COLOR_PART) << SHIFT_COLOR_R; + result |= ((long) rgbColor.g() & MASK_COLOR_PART) << SHIFT_COLOR_G; + result |= ((long) rgbColor.b() & MASK_COLOR_PART) << SHIFT_COLOR_B; + } else if (color instanceof Color.IndexedColor) { + Color.IndexedColor idxColor = (Color.IndexedColor) color; + result |= PALETTE_INDEXED << SHIFT_PALETTE_TYPE; + result |= ((long) idxColor.index() & MASK_COLOR_PART) << SHIFT_COLOR_INDEXED_INDEX; + } else if (color instanceof Color.BasicColor) { + Color.BasicColor basicColor = (Color.BasicColor) color; + int intensity; + switch (basicColor.intensity()) { + case normal: + intensity = 1; + break; + case dark: + intensity = 2; + break; + case bright: + intensity = 3; + break; + default: + intensity = 0; + break; + } + ; + result |= + ((long) intensity & MASK_COLOR_BASIC_INTENSITY) << SHIFT_COLOR_BASIC_INTENSITY; + result |= + ((long) basicColor.index() & MASK_COLOR_BASIC_INDEX) << SHIFT_COLOR_BASIC_INDEX; + } + return result; + } + + private static @NonNull Color decodeColor(long color) { + Color result = Color.DEFAULT; + long mode = color & MASK_COLOR_MODE; + if (mode == CM_INDEXED) { + long paletteType = (color >> SHIFT_PALETTE_TYPE) & MASK_PALETTE_TYPE; + if (paletteType == PALETTE_BASIC) { + int intensity = + (int) ((color >> SHIFT_COLOR_BASIC_INTENSITY) & MASK_COLOR_BASIC_INTENSITY); + int colorIndex = + (int) ((color >> SHIFT_COLOR_BASIC_INDEX) & MASK_COLOR_BASIC_INDEX); + switch (intensity) { + case 1: + result = Color.basic(colorIndex, Color.BasicColor.Intensity.normal); + break; + case 2: + result = Color.basic(colorIndex, Color.BasicColor.Intensity.dark); + break; + case 3: + result = Color.basic(colorIndex, Color.BasicColor.Intensity.bright); + break; + } + } else { // paletteType == F_PALETTE_INDEXED + int colorIndex = (int) ((color >> SHIFT_COLOR_INDEXED_INDEX) & MASK_COLOR_PART); + result = Color.indexed(colorIndex); + } + } else { // mode == F_CM_RGB + int r = (int) ((color >> SHIFT_COLOR_R) & MASK_COLOR_PART); + int g = (int) ((color >> SHIFT_COLOR_G) & MASK_COLOR_PART); + int b = (int) ((color >> SHIFT_COLOR_B) & MASK_COLOR_PART); + result = Color.rgb(r, g, b); + } + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Style)) return false; + Style other = (Style) o; + return this.state == other.state; + } + + @Override + public int hashCode() { + return Long.hashCode(state); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("StyleImpl{"); + if (isBold()) sb.append("bold, "); + if (isFaint()) sb.append("faint, "); + if (isItalic()) sb.append("italic, "); + if (isUnderlined()) sb.append("underlined, "); + if (isBlink()) sb.append("blink, "); + if (isInverse()) sb.append("inverse, "); + if (isHidden()) sb.append("hidden, "); + if (isStrikethrough()) sb.append("strikethrough, "); + if (fgColor() != Color.DEFAULT) sb.append("fgColor=").append(fgColor()).append(", "); + if (bgColor() != Color.DEFAULT) sb.append("bgColor=").append(bgColor()); + if (sb.charAt(sb.length() - 2) == ',') + sb.setLength(sb.length() - 2); // Remove trailing comma + sb.append('}'); + return sb.toString(); + } + + static boolean fastOverShort = System.getProperty(PROP_TOANSI, "fast").equals("fast"); + + public String toAnsiString() { + return toAnsiString(UNSTYLED); + } + + public String toAnsiString(Style currentStyle) { + return toAnsiString(currentStyle.state()); + } + + public String toAnsiString(long currentStyleState) { + if (fastOverShort) { + List styles = new ArrayList<>(); + return toAnsiString(styles, currentStyleState); + } else { + List styles1 = new ArrayList<>(); + List styles2 = new ArrayList<>(); + styles2.add(Ansi.RESET); + String ansi1 = toAnsiString(styles1, currentStyleState); + String ansi2 = toAnsiString(styles2, F_UNSTYLED); + return (ansi1.length() <= ansi2.length()) ? ansi1 : ansi2; + } + } + + private String toAnsiString(List styles, long currentStyleState) { + if ((currentStyleState & (F_BOLD | F_FAINT)) != (state & (F_BOLD | F_FAINT))) { + // First we switch to NORMAL to clear both BOLD and FAINT + if ((currentStyleState & (F_BOLD | F_FAINT)) != 0) { + styles.add(Ansi.NORMAL); + } + // Now we set the needed styles + if (isBold()) styles.add(Ansi.BOLD); + if (isFaint()) styles.add(Ansi.FAINT); + } + if ((currentStyleState & F_ITALIC) != (state & F_ITALIC)) { + if (isItalic()) { + styles.add(Ansi.ITALICIZED); + } else { + styles.add(Ansi.NOTITALICIZED); + } + } + if ((currentStyleState & F_UNDERLINED) != (state & F_UNDERLINED)) { + if (isUnderlined()) { + styles.add(Ansi.UNDERLINED); + } else { + styles.add(Ansi.NOTUNDERLINED); + } + } + if ((currentStyleState & F_BLINK) != (state & F_BLINK)) { + if (isBlink()) { + styles.add(Ansi.BLINK); + } else { + styles.add(Ansi.STEADY); + } + } + if ((currentStyleState & F_INVERSE) != (state & F_INVERSE)) { + if (isInverse()) { + styles.add(Ansi.INVERSE); + } else { + styles.add(Ansi.POSITIVE); + } + } + if ((currentStyleState & F_HIDDEN) != (state & F_HIDDEN)) { + if (isHidden()) { + styles.add(Ansi.INVISIBLE); + } else { + styles.add(Ansi.VISIBLE); + } + } + if ((currentStyleState & F_STRIKETHROUGH) != (state & F_STRIKETHROUGH)) { + if (isStrikethrough()) { + styles.add(Ansi.CROSSEDOUT); + } else { + styles.add(Ansi.NOTCROSSEDOUT); + } + } + if ((currentStyleState & MASK_FG_COLOR) != (state & MASK_FG_COLOR)) { + styles.add(fgColor().toAnsiFg()); + } + if ((currentStyleState & MASK_BG_COLOR) != (state & MASK_BG_COLOR)) { + styles.add(bgColor().toAnsiBg()); + } + return Ansi.style(styles.toArray()); + } +} diff --git a/twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestColor.java b/twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestColor.java new file mode 100644 index 0000000..f80cb86 --- /dev/null +++ b/twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestColor.java @@ -0,0 +1,276 @@ +package org.codejive.twinkle.ansi; + +import static org.assertj.core.api.Assertions.*; +import static org.codejive.twinkle.ansi.Ansi.*; + +import org.codejive.twinkle.ansi.Color.*; +import org.junit.jupiter.api.Test; + +public class TestColor { + + @Test + public void testDefaultColorCodes() { + assertThat(Color.DEFAULT.toAnsiFg()).isEqualTo(Ansi.style(Ansi.DEFAULT_FOREGROUND)); + assertThat(Color.DEFAULT.toAnsiBg()).isEqualTo(Ansi.style(Ansi.DEFAULT_BACKGROUND)); + } + + @Test + public void testBasicColorCodes() { + // Basic foreground colors + assertThat(BasicColor.BLACK.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foreground(Ansi.BLACK))); + assertThat(BasicColor.RED.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foreground(Ansi.RED))); + assertThat(BasicColor.GREEN.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foreground(Ansi.GREEN))); + assertThat(BasicColor.YELLOW.toAnsiFg()) + .isEqualTo(Ansi.style(Ansi.foreground(Ansi.YELLOW))); + assertThat(BasicColor.BLUE.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foreground(Ansi.BLUE))); + assertThat(BasicColor.MAGENTA.toAnsiFg()) + .isEqualTo(Ansi.style(Ansi.foreground(Ansi.MAGENTA))); + assertThat(BasicColor.CYAN.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foreground(Ansi.CYAN))); + assertThat(BasicColor.WHITE.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foreground(Ansi.WHITE))); + + // Basic foreground colors - dark variants + assertThat(BasicColor.BLACK.dark().toAnsiFg()) + .isEqualTo(Ansi.style(Ansi.foregroundDark(BLACK))); + assertThat(BasicColor.RED.dark().toAnsiFg()) + .isEqualTo(Ansi.style(Ansi.foregroundDark(Ansi.RED))); + assertThat(BasicColor.GREEN.dark().toAnsiFg()) + .isEqualTo(Ansi.style(Ansi.foregroundDark(Ansi.GREEN))); + assertThat(BasicColor.YELLOW.dark().toAnsiFg()) + .isEqualTo(Ansi.style(Ansi.foregroundDark(Ansi.YELLOW))); + assertThat(BasicColor.BLUE.dark().toAnsiFg()) + .isEqualTo(Ansi.style(Ansi.foregroundDark(Ansi.BLUE))); + assertThat(BasicColor.MAGENTA.dark().toAnsiFg()) + .isEqualTo(Ansi.style(Ansi.foregroundDark(Ansi.MAGENTA))); + assertThat(BasicColor.CYAN.dark().toAnsiFg()) + .isEqualTo(Ansi.style(Ansi.foregroundDark(Ansi.CYAN))); + assertThat(BasicColor.WHITE.dark().toAnsiFg()) + .isEqualTo(Ansi.style(Ansi.foregroundDark(Ansi.WHITE))); + + // Basic foreground colors - bright variants + assertThat(BasicColor.BLACK.bright().toAnsiFg()) + .isEqualTo(Ansi.style(Ansi.foregroundBright(BLACK))); + assertThat(BasicColor.RED.bright().toAnsiFg()) + .isEqualTo(Ansi.style(Ansi.foregroundBright(Ansi.RED))); + assertThat(BasicColor.GREEN.bright().toAnsiFg()) + .isEqualTo(Ansi.style(Ansi.foregroundBright(Ansi.GREEN))); + assertThat(BasicColor.YELLOW.bright().toAnsiFg()) + .isEqualTo(Ansi.style(Ansi.foregroundBright(Ansi.YELLOW))); + assertThat(BasicColor.BLUE.bright().toAnsiFg()) + .isEqualTo(Ansi.style(Ansi.foregroundBright(Ansi.BLUE))); + assertThat(BasicColor.MAGENTA.bright().toAnsiFg()) + .isEqualTo(Ansi.style(Ansi.foregroundBright(Ansi.MAGENTA))); + assertThat(BasicColor.CYAN.bright().toAnsiFg()) + .isEqualTo(Ansi.style(Ansi.foregroundBright(Ansi.CYAN))); + assertThat(BasicColor.WHITE.bright().toAnsiFg()) + .isEqualTo(Ansi.style(Ansi.foregroundBright(Ansi.WHITE))); + + // Basic background colors + assertThat(BasicColor.BLACK.toAnsiBg()).isEqualTo(Ansi.style(Ansi.background(Ansi.BLACK))); + assertThat(BasicColor.RED.toAnsiBg()).isEqualTo(Ansi.style(Ansi.background(Ansi.RED))); + assertThat(BasicColor.GREEN.toAnsiBg()).isEqualTo(Ansi.style(Ansi.background(Ansi.GREEN))); + assertThat(BasicColor.YELLOW.toAnsiBg()) + .isEqualTo(Ansi.style(Ansi.background(Ansi.YELLOW))); + assertThat(BasicColor.BLUE.toAnsiBg()).isEqualTo(Ansi.style(Ansi.background(Ansi.BLUE))); + assertThat(BasicColor.MAGENTA.toAnsiBg()) + .isEqualTo(Ansi.style(Ansi.background(Ansi.MAGENTA))); + assertThat(BasicColor.CYAN.toAnsiBg()).isEqualTo(Ansi.style(Ansi.background(Ansi.CYAN))); + assertThat(BasicColor.WHITE.toAnsiBg()).isEqualTo(Ansi.style(Ansi.background(Ansi.WHITE))); + + // Basic background colors - dark variants + assertThat(BasicColor.BLACK.dark().toAnsiBg()) + .isEqualTo(Ansi.style(Ansi.backgroundDark(BLACK))); + assertThat(BasicColor.RED.dark().toAnsiBg()) + .isEqualTo(Ansi.style(Ansi.backgroundDark(Ansi.RED))); + assertThat(BasicColor.GREEN.dark().toAnsiBg()) + .isEqualTo(Ansi.style(Ansi.backgroundDark(Ansi.GREEN))); + assertThat(BasicColor.YELLOW.dark().toAnsiBg()) + .isEqualTo(Ansi.style(Ansi.backgroundDark(Ansi.YELLOW))); + assertThat(BasicColor.BLUE.dark().toAnsiBg()) + .isEqualTo(Ansi.style(Ansi.backgroundDark(Ansi.BLUE))); + assertThat(BasicColor.MAGENTA.dark().toAnsiBg()) + .isEqualTo(Ansi.style(Ansi.backgroundDark(Ansi.MAGENTA))); + assertThat(BasicColor.CYAN.dark().toAnsiBg()) + .isEqualTo(Ansi.style(Ansi.backgroundDark(Ansi.CYAN))); + assertThat(BasicColor.WHITE.dark().toAnsiBg()) + .isEqualTo(Ansi.style(Ansi.backgroundDark(Ansi.WHITE))); + + // Basic background colors - bright variants + assertThat(BasicColor.BLACK.bright().toAnsiBg()) + .isEqualTo(Ansi.style(Ansi.backgroundBright(BLACK))); + assertThat(BasicColor.RED.bright().toAnsiBg()) + .isEqualTo(Ansi.style(Ansi.backgroundBright(Ansi.RED))); + assertThat(BasicColor.GREEN.bright().toAnsiBg()) + .isEqualTo(Ansi.style(Ansi.backgroundBright(Ansi.GREEN))); + assertThat(BasicColor.YELLOW.bright().toAnsiBg()) + .isEqualTo(Ansi.style(Ansi.backgroundBright(Ansi.YELLOW))); + assertThat(BasicColor.BLUE.bright().toAnsiBg()) + .isEqualTo(Ansi.style(Ansi.backgroundBright(Ansi.BLUE))); + assertThat(BasicColor.MAGENTA.bright().toAnsiBg()) + .isEqualTo(Ansi.style(Ansi.backgroundBright(Ansi.MAGENTA))); + assertThat(BasicColor.CYAN.bright().toAnsiBg()) + .isEqualTo(Ansi.style(Ansi.backgroundBright(Ansi.CYAN))); + assertThat(BasicColor.WHITE.bright().toAnsiBg()) + .isEqualTo(Ansi.style(Ansi.backgroundBright(Ansi.WHITE))); + } + + @Test + public void testIndexedColorCodes() { + IndexedColor color = IndexedColor.of(0); + assertThat(color.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foregroundIndexed(0))); + assertThat(color.toAnsiBg()).isEqualTo(Ansi.style(Ansi.backgroundIndexed(0))); + color = IndexedColor.of(128); + assertThat(color.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foregroundIndexed(128))); + assertThat(color.toAnsiBg()).isEqualTo(Ansi.style(Ansi.backgroundIndexed(128))); + color = IndexedColor.of(255); + assertThat(color.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foregroundIndexed(255))); + assertThat(color.toAnsiBg()).isEqualTo(Ansi.style(Ansi.backgroundIndexed(255))); + } + + @Test + public void testIndexedColorCodesUnderflow() { + assertThatThrownBy( + () -> { + IndexedColor.of(-1); + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Color index must be between 0 and 255, got: -1"); + } + + @Test + public void testIndexedColorCodesOverflow() { + assertThatThrownBy( + () -> { + IndexedColor.of(256); + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Color index must be between 0 and 255, got: 256"); + } + + @Test + public void testRgbColorCodes() { + RgbColor color = RgbColor.of(0, 0, 0); + assertThat(color.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foregroundRgb(0, 0, 0))); + assertThat(color.toAnsiBg()).isEqualTo(Ansi.style(Ansi.backgroundRgb(0, 0, 0))); + color = RgbColor.of(128, 64, 32); + assertThat(color.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foregroundRgb(128, 64, 32))); + assertThat(color.toAnsiBg()).isEqualTo(Ansi.style(Ansi.backgroundRgb(128, 64, 32))); + color = RgbColor.of(255, 255, 255); + assertThat(color.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foregroundRgb(255, 255, 255))); + assertThat(color.toAnsiBg()).isEqualTo(Ansi.style(Ansi.backgroundRgb(255, 255, 255))); + } + + @Test + public void testRgbColorCodesUnderflow() { + assertThatThrownBy( + () -> { + RgbColor.of(-1, 0, 0); + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("RGB values must be between 0 and 255, got: -1, 0, 0"); + assertThatThrownBy( + () -> { + RgbColor.of(0, -1, 0); + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("RGB values must be between 0 and 255, got: 0, -1, 0"); + assertThatThrownBy( + () -> { + RgbColor.of(0, 0, -1); + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("RGB values must be between 0 and 255, got: 0, 0, -1"); + } + + @Test + public void testRgbColorCodesOverflow() { + assertThatThrownBy( + () -> { + RgbColor.of(256, 0, 0); + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("RGB values must be between 0 and 255, got: 256, 0, 0"); + assertThatThrownBy( + () -> { + RgbColor.of(0, 256, 0); + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("RGB values must be between 0 and 255, got: 0, 256, 0"); + assertThatThrownBy( + () -> { + RgbColor.of(0, 0, 256); + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("RGB values must be between 0 and 255, got: 0, 0, 256"); + } + + @Test + public void testBasicColorCodesByIndex() { + assertThat(BasicColor.byIndex(BLACK)).isEqualTo(BasicColor.BLACK); + assertThat(BasicColor.byIndex(RED)).isEqualTo(BasicColor.RED); + assertThat(BasicColor.byIndex(GREEN)).isEqualTo(BasicColor.GREEN); + assertThat(BasicColor.byIndex(YELLOW)).isEqualTo(BasicColor.YELLOW); + assertThat(BasicColor.byIndex(BLUE)).isEqualTo(BasicColor.BLUE); + assertThat(BasicColor.byIndex(MAGENTA)).isEqualTo(BasicColor.MAGENTA); + assertThat(BasicColor.byIndex(CYAN)).isEqualTo(BasicColor.CYAN); + assertThat(BasicColor.byIndex(WHITE)).isEqualTo(BasicColor.WHITE); + } + + @Test + public void testBasicColorCodesByIndexNormal() { + assertThat(BasicColor.byIndex(BLACK, BasicColor.Intensity.normal)) + .isEqualTo(BasicColor.BLACK); + assertThat(BasicColor.byIndex(RED, BasicColor.Intensity.normal)).isEqualTo(BasicColor.RED); + assertThat(BasicColor.byIndex(GREEN, BasicColor.Intensity.normal)) + .isEqualTo(BasicColor.GREEN); + assertThat(BasicColor.byIndex(YELLOW, BasicColor.Intensity.normal)) + .isEqualTo(BasicColor.YELLOW); + assertThat(BasicColor.byIndex(BLUE, BasicColor.Intensity.normal)) + .isEqualTo(BasicColor.BLUE); + assertThat(BasicColor.byIndex(MAGENTA, BasicColor.Intensity.normal)) + .isEqualTo(BasicColor.MAGENTA); + assertThat(BasicColor.byIndex(CYAN, BasicColor.Intensity.normal)) + .isEqualTo(BasicColor.CYAN); + assertThat(BasicColor.byIndex(WHITE, BasicColor.Intensity.normal)) + .isEqualTo(BasicColor.WHITE); + } + + @Test + public void testBasicColorCodesByIndexDark() { + assertThat(BasicColor.byIndex(BLACK, BasicColor.Intensity.dark)) + .isEqualTo(BasicColor.DARK_BLACK); + assertThat(BasicColor.byIndex(RED, BasicColor.Intensity.dark)) + .isEqualTo(BasicColor.DARK_RED); + assertThat(BasicColor.byIndex(GREEN, BasicColor.Intensity.dark)) + .isEqualTo(BasicColor.DARK_GREEN); + assertThat(BasicColor.byIndex(YELLOW, BasicColor.Intensity.dark)) + .isEqualTo(BasicColor.DARK_YELLOW); + assertThat(BasicColor.byIndex(BLUE, BasicColor.Intensity.dark)) + .isEqualTo(BasicColor.DARK_BLUE); + assertThat(BasicColor.byIndex(MAGENTA, BasicColor.Intensity.dark)) + .isEqualTo(BasicColor.DARK_MAGENTA); + assertThat(BasicColor.byIndex(CYAN, BasicColor.Intensity.dark)) + .isEqualTo(BasicColor.DARK_CYAN); + assertThat(BasicColor.byIndex(WHITE, BasicColor.Intensity.dark)) + .isEqualTo(BasicColor.DARK_WHITE); + } + + @Test + public void testBasicColorCodesByIndexBright() { + assertThat(BasicColor.byIndex(BLACK, BasicColor.Intensity.bright)) + .isEqualTo(BasicColor.BRIGHT_BLACK); + assertThat(BasicColor.byIndex(RED, BasicColor.Intensity.bright)) + .isEqualTo(BasicColor.BRIGHT_RED); + assertThat(BasicColor.byIndex(GREEN, BasicColor.Intensity.bright)) + .isEqualTo(BasicColor.BRIGHT_GREEN); + assertThat(BasicColor.byIndex(YELLOW, BasicColor.Intensity.bright)) + .isEqualTo(BasicColor.BRIGHT_YELLOW); + assertThat(BasicColor.byIndex(BLUE, BasicColor.Intensity.bright)) + .isEqualTo(BasicColor.BRIGHT_BLUE); + assertThat(BasicColor.byIndex(MAGENTA, BasicColor.Intensity.bright)) + .isEqualTo(BasicColor.BRIGHT_MAGENTA); + assertThat(BasicColor.byIndex(CYAN, BasicColor.Intensity.bright)) + .isEqualTo(BasicColor.BRIGHT_CYAN); + assertThat(BasicColor.byIndex(WHITE, BasicColor.Intensity.bright)) + .isEqualTo(BasicColor.BRIGHT_WHITE); + } +} diff --git a/twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestStyle.java b/twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestStyle.java new file mode 100644 index 0000000..534f064 --- /dev/null +++ b/twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestStyle.java @@ -0,0 +1,226 @@ +package org.codejive.twinkle.ansi; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class TestStyle { + @BeforeEach + public void init() { + Style.fastOverShort = false; + } + + @Test + public void testStyleCreation() { + Style style1 = Style.of(Style.F_BOLD); + Style style2 = Style.BOLD; + + assertThat(style1).isEqualTo(style2); + assertThat(style1.state()).isEqualTo(style2.state()); + + Style style3 = Style.FAINT; + + assertThat(style1).isNotEqualTo(style3); + } + + @Test + public void testStyleCombination() { + Style style = + Style.UNSTYLED + .bold() + .faint() + .italic() + .underlined() + .blink() + .inverse() + .hidden() + .strikethrough(); + assertThat(style.isBold()).isTrue(); + assertThat(style.isFaint()).isTrue(); + assertThat(style.isItalic()).isTrue(); + assertThat(style.isUnderlined()).isTrue(); + assertThat(style.isBlink()).isTrue(); + assertThat(style.isInverse()).isTrue(); + assertThat(style.isHidden()).isTrue(); + assertThat(style.isStrikethrough()).isTrue(); + + style = + style.normal() + .italicOff() + .underlinedOff() + .blinkOff() + .inverseOff() + .hiddenOff() + .strikethroughOff(); + assertThat(style).isEqualTo(Style.UNSTYLED); + } + + @Test + public void testBasicColorStyles() { + Style style = Style.UNSTYLED; + assertThat(style.fgColor()).isEqualTo(Color.DEFAULT); + assertThat(style.bgColor()).isEqualTo(Color.DEFAULT); + + style = style.fgColor(Color.BasicColor.BLUE); + assertThat(style.fgColor()).isEqualTo(Color.BasicColor.BLUE); + + style = style.bgColor(Color.BasicColor.RED); + assertThat(style.bgColor()).isEqualTo(Color.BasicColor.RED); + } + + @Test + public void testIndexedColorStyles() { + for (int i = 0; i < 256; i++) { + Style style = Style.ofFgColor(Color.indexed(i)).bgColor(Color.indexed(255 - i)); + assertThat(style.fgColor()).isEqualTo(Color.indexed(i)); + assertThat(style.bgColor()).isEqualTo(Color.indexed(255 - i)); + } + } + + @Test + public void testRgbColorStyles() { + Style style = + Style.UNSTYLED.fgColor(Color.rgb(100, 150, 200)).bgColor(Color.rgb(50, 75, 125)); + assertThat(style.fgColor()).isEqualTo(Color.rgb(100, 150, 200)); + assertThat(style.bgColor()).isEqualTo(Color.rgb(50, 75, 125)); + } + + @Test + public void testMixedStyles() { + Style style = + Style.UNSTYLED + .bold() + .faint() + .italic() + .underlined() + .blink() + .inverse() + .hidden() + .strikethrough(); + style = style.fgColor(Color.BasicColor.BLUE); + style = style.bgColor(Color.indexed(128)); + style = + style.normal() + .italicOff() + .underlinedOff() + .blinkOff() + .inverseOff() + .hiddenOff() + .strikethroughOff(); + style = style.fgColor(Color.DEFAULT); + style = style.bgColor(Color.DEFAULT); + assertThat(style).isEqualTo(Style.UNSTYLED); + } + + @Test + public void testToAnsiStringUnstyled() { + Style style = Style.UNSTYLED; + String ansiCode = style.toAnsiString(); + assertThat(ansiCode).isEqualTo(""); + } + + @Test + public void testToAnsiStringAllStyles() { + Style style = + Style.UNSTYLED + .bold() + .faint() + .italic() + .underlined() + .blink() + .inverse() + .hidden() + .strikethrough(); + String ansiCode = style.toAnsiString(); + assertThat(ansiCode) + .isEqualTo( + Ansi.style( + Ansi.BOLD, + Ansi.FAINT, + Ansi.ITALICIZED, + Ansi.UNDERLINED, + Ansi.BLINK, + Ansi.INVERSE, + Ansi.INVISIBLE, + Ansi.CROSSEDOUT)); + } + + @Test + public void testToAnsiStringAllStylesWithCurrent() { + Style style = + Style.UNSTYLED + .bold() + .faint() + .italic() + .underlined() + .blink() + .inverse() + .hidden() + .strikethrough(); + String ansiCode = style.toAnsiString(Style.F_BOLD | Style.F_UNDERLINED); + assertThat(ansiCode) + .isEqualTo( + Ansi.style( + Ansi.NORMAL, + Ansi.BOLD, + Ansi.FAINT, + Ansi.ITALICIZED, + Ansi.BLINK, + Ansi.INVERSE, + Ansi.INVISIBLE, + Ansi.CROSSEDOUT)); + } + + @Test + public void testToAnsiStringAllStylesWithCurrent2() { + Style style = + Style.UNSTYLED + .bold() + .faint() + .italic() + .underlined() + .blink() + .inverse() + .hidden() + .strikethrough(); + String ansiCode = style.toAnsiString(Style.F_BOLD | Style.F_FAINT | Style.F_UNDERLINED); + assertThat(ansiCode) + .isEqualTo( + Ansi.style( + Ansi.ITALICIZED, + Ansi.BLINK, + Ansi.INVERSE, + Ansi.INVISIBLE, + Ansi.CROSSEDOUT)); + } + + @Test + public void testToAnsiFastVsShort() { + long currentStyleState = + Style.F_BOLD + | Style.F_FAINT + | Style.F_ITALIC + | Style.F_UNDERLINED + | Style.F_BLINK + | Style.F_INVERSE + | Style.F_HIDDEN + | Style.F_STRIKETHROUGH; + String ansiCode = Style.UNSTYLED.bold().toAnsiString(currentStyleState); + assertThat(ansiCode).isEqualTo(Ansi.style(Ansi.RESET, Ansi.BOLD)); + Style.fastOverShort = true; + ansiCode = Style.UNSTYLED.bold().toAnsiString(currentStyleState); + assertThat(ansiCode) + .isEqualTo( + Ansi.style( + Ansi.NORMAL, + Ansi.BOLD, + Ansi.NOTITALICIZED, + Ansi.NOTUNDERLINED, + Ansi.STEADY, + Ansi.POSITIVE, + Ansi.VISIBLE, + Ansi.NOTCROSSEDOUT)); + Style.fastOverShort = false; + } +} diff --git a/twinkle-chart/pom.xml b/twinkle-chart/pom.xml new file mode 100644 index 0000000..5a23f94 --- /dev/null +++ b/twinkle-chart/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + + org.codejive.twinkle + twinkle + 1.0-SNAPSHOT + ../pom.xml + + + twinkle-chart + jar + + Text-mode charting module for Twinkle TUI library + + + + org.codejive.twinkle + twinkle-core + 1.0-SNAPSHOT + compile + + + + + diff --git a/twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/bar/Bar.java b/twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/bar/Bar.java new file mode 100644 index 0000000..df2124e --- /dev/null +++ b/twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/bar/Bar.java @@ -0,0 +1,31 @@ +package org.codejive.twinkle.components.graphs.bar; + +import org.codejive.twinkle.core.component.Canvas; +import org.codejive.twinkle.core.component.Renderable; + +public class Bar implements Renderable { + private final FracBarRenderer renderer; + private Number value = 0.0d; + + /** + * Returns a fractional horizontal Bar representing values between 0 and 100. + * + * @return A Bar instance + */ + public static Bar bar() { + return new Bar(FracBarConfig.create()); + } + + public Bar(FracBarConfig config) { + this.renderer = new FracBarRenderer(config); + } + + public Bar setValue(Number value) { + this.value = value; + return this; + } + + public void render(Canvas canvas) { + renderer.render(canvas, value); + } +} diff --git a/twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/bar/BarConfig.java b/twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/bar/BarConfig.java new file mode 100644 index 0000000..5d62d7e --- /dev/null +++ b/twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/bar/BarConfig.java @@ -0,0 +1,77 @@ +package org.codejive.twinkle.components.graphs.bar; + +public class BarConfig { + private Number minValue; + private Number maxValue; + private Direction direction; + + public enum Orientation { + HORIZONTAL, + VERTICAL; + } + + public enum Direction { + L2R(Orientation.HORIZONTAL, 1, 0), + R2L(Orientation.HORIZONTAL, -1, 0), + B2T(Orientation.VERTICAL, 0, -1), + T2B(Orientation.VERTICAL, 0, 1); + + public final Orientation orientation; + public final int dx; + public final int dy; + + Direction(Orientation orientation, int dx, int dy) { + this.orientation = orientation; + this.dx = dx; + this.dy = dy; + } + } + + public BarConfig() { + this.minValue = 0; + this.maxValue = 100; + this.direction = Direction.L2R; + } + + public Number minValue() { + return minValue; + } + + public BarConfig minValue(Number minValue) { + this.minValue = minValue; + return this; + } + + public Number maxValue() { + return maxValue; + } + + public BarConfig maxValue(Number maxValue) { + this.maxValue = maxValue; + return this; + } + + public Direction direction() { + return direction; + } + + public BarConfig direction(Direction direction) { + this.direction = direction; + return this; + } + + public BarConfig copy() { + return copy_(new BarConfig()); + } + + protected BarConfig copy_(BarConfig b) { + b.minValue = this.minValue; + b.maxValue = this.maxValue; + b.direction = this.direction; + return b; + } + + public static BarConfig create() { + return new BarConfig(); + } +} diff --git a/twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/bar/FracBarConfig.java b/twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/bar/FracBarConfig.java new file mode 100644 index 0000000..e3d6bf1 --- /dev/null +++ b/twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/bar/FracBarConfig.java @@ -0,0 +1,55 @@ +package org.codejive.twinkle.components.graphs.bar; + +public class FracBarConfig extends BarConfig { + private Design design; + + public enum Design { + TEXT_BLOCK, + COLOR_BLOCK, + FULL_BLOCK, + HALF_BLOCK, + FRACTIONAL_BLOCK; + } + + public FracBarConfig() { + this.design = Design.FRACTIONAL_BLOCK; + } + + @Override + public FracBarConfig minValue(Number minValue) { + return (FracBarConfig) super.minValue(minValue); + } + + @Override + public FracBarConfig maxValue(Number maxValue) { + return (FracBarConfig) super.maxValue(maxValue); + } + + @Override + public FracBarConfig direction(Direction direction) { + return (FracBarConfig) super.direction(direction); + } + + public Design design() { + return design; + } + + public FracBarConfig design(Design design) { + this.design = design; + return this; + } + + public FracBarConfig copy() { + return copy_(new FracBarConfig()); + } + + protected FracBarConfig copy_(FracBarConfig b) { + super.copy_(b); + b.design = this.design; + return b; + } + + public static FracBarConfig create() { + return new FracBarConfig(); + } +} diff --git a/twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/bar/FracBarRenderer.java b/twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/bar/FracBarRenderer.java new file mode 100644 index 0000000..3c64a2f --- /dev/null +++ b/twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/bar/FracBarRenderer.java @@ -0,0 +1,146 @@ +package org.codejive.twinkle.components.graphs.bar; + +import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.components.graphs.bar.BarConfig.*; +import org.codejive.twinkle.components.graphs.bar.FracBarConfig.*; +import org.codejive.twinkle.core.component.Canvas; +import org.codejive.twinkle.core.component.Size; + +public class FracBarRenderer { + private final FracBarConfig config; + + private static final char[] TEXT_BLOCK = {'#'}; + private static final char[] COLOR_BLOCK = {' '}; + private static final char[] BLOCK_FULL = {'\u2588'}; + private static final char[] BLOCK_HALF_L2R = {'\u2588', '\u258c'}; // full, left half + private static final char[] BLOCK_HALF_R2L = {'\u2588', '\u2590'}; // full, right half + private static final char[] BLOCK_HALF_T2B = {'\u2588', '\u2580'}; // full, top half + private static final char[] BLOCK_HALF_B2T = {'\u2588', '\u2584'}; // full, bottom half + private static final char[] BLOCK_FRAC_L2R = { + '\u2588', '\u258f', '\u258e', '\u258d', '\u258c', '\u258b', '\u258a', '\u2589' + }; // full, left 1/8 .. 7/8 + private static final char[] BLOCK_FRAC_B2T = { + '\u2588', '\u2581', '\u2582', '\u2583', '\u2584', '\u2585', '\u2586', '\u2587' + }; // full, lower 1/8 .. 7/8 + private static final char BLOCK_OVERFLOW = '\u2593'; // dark shade block + + public FracBarRenderer() { + this(FracBarConfig.create()); + } + + public FracBarRenderer(FracBarConfig config) { + this.config = config; + } + + public void render(Canvas canvas, Number value) { + FracBarConfig.Design activeDesign = config.design(); + if (activeDesign == Design.FRACTIONAL_BLOCK + && (config.direction() == Direction.R2L || config.direction() == Direction.T2B)) { + // Fallback to half block design for unsupported directions + activeDesign = Design.HALF_BLOCK; + } + + double dx = value.doubleValue(); + double dmin = config.minValue().doubleValue(); + double dmax = config.maxValue().doubleValue(); + + if (dmin > dmax) { + throw new IllegalArgumentException("Minimum value greater than maximum value"); + } + // Clamp value to minimum + dx = Math.max(dmin, dx); + + Direction direction = config.direction(); + + char[] blocks; + switch (activeDesign) { + case TEXT_BLOCK: + blocks = TEXT_BLOCK; + break; + case COLOR_BLOCK: + blocks = COLOR_BLOCK; + break; + case FULL_BLOCK: + blocks = BLOCK_FULL; + break; + case HALF_BLOCK: + switch (direction) { + case L2R: + blocks = BLOCK_HALF_L2R; + break; + case R2L: + blocks = BLOCK_HALF_R2L; + break; + case B2T: + blocks = BLOCK_HALF_B2T; + break; + case T2B: + blocks = BLOCK_HALF_T2B; + break; + default: + throw new IllegalStateException("Unknown direction: " + direction); + } + break; + case FRACTIONAL_BLOCK: + switch (direction) { + case L2R: + blocks = BLOCK_FRAC_L2R; + break; + case B2T: + blocks = BLOCK_FRAC_B2T; + break; + case R2L: + case T2B: + // We shouldn't get here because we fall back to half blocks in these cases + throw new IllegalStateException( + "Unsupported direction: " + + direction + + " for design: " + + activeDesign); + default: + throw new IllegalStateException("Unknown direction: " + direction); + } + break; + default: + throw new IllegalStateException("Unknown design: " + activeDesign); + } + + Size size = canvas.size(); + boolean reversed = direction == Direction.R2L || direction == Direction.T2B; + boolean horizontal = direction.orientation == Orientation.HORIZONTAL; + int maxSize = horizontal ? size.width() : size.height(); + int nroBlocksPerChar = blocks.length; + int maxSizeInFractions = maxSize * nroBlocksPerChar; + double interval = dmax - dmin; + int barWidthInFractions = (int) (((dx - dmin) / interval) * maxSizeInFractions); + boolean overflow = barWidthInFractions > maxSizeInFractions; + int fullChunks = overflow ? maxSize - 1 : barWidthInFractions / nroBlocksPerChar; + int remainder = overflow ? 0 : barWidthInFractions % nroBlocksPerChar; + + int x = !reversed && horizontal ? 0 : size.width() - 1; + int y = reversed || horizontal ? 0 : size.height() - 1; + // Place full blocks first + for (int i = 0; i < fullChunks; i++) { + canvas.setCharAt(x, y, Style.F_UNSTYLED, blocks[0]); + x += direction.dx; + y += direction.dy; + } + // Append remainder partial block if any + if (remainder > 0) { + canvas.setCharAt(x, y, Style.F_UNSTYLED, blocks[remainder]); + x += direction.dx; + y += direction.dy; + } else if (overflow) { // Or an overflow block + canvas.setCharAt(x, y, Style.F_UNSTYLED, BLOCK_OVERFLOW); + x += direction.dx; + y += direction.dy; + } + // Fill the rest with spaces + int sizeLeft = maxSize - fullChunks - (overflow || remainder > 0 ? 1 : 0); + for (int i = 0; i < sizeLeft; i++) { + canvas.setCharAt(x, y, Style.F_UNSTYLED, ' '); + x += direction.dx; + y += direction.dy; + } + } +} diff --git a/twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/plot/MathPlot.java b/twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/plot/MathPlot.java new file mode 100644 index 0000000..0cf42d4 --- /dev/null +++ b/twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/plot/MathPlot.java @@ -0,0 +1,271 @@ +package org.codejive.twinkle.components.graphs.plot; + +import java.util.function.Function; +import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.core.component.Canvas; +import org.codejive.twinkle.core.component.Component; +import org.codejive.twinkle.core.component.Size; +import org.jspecify.annotations.NonNull; + +public class MathPlot implements Component { + private final Plot plot; + private Origin origin; + private Number minXValue; + private Number maxXValue; + private Number minYValue; + private Number maxYValue; + private double originX; + private double originY; + + public enum Origin { + N, + S, + E, + W, + NE, + NW, + SE, + SW, + CENTER + } + + public static MathPlot of(Size size) { + return new MathPlot(Plot.of(size)); + } + + public static MathPlot of(Canvas canvas) { + return new MathPlot(Plot.of(canvas)); + } + + public static MathPlot of(Plot plot) { + return new MathPlot(plot); + } + + public MathPlot(Plot plot) { + this.plot = plot; + ranges(-1.0d, 1.0d, -1.0d, 1.0d); + origin(Origin.CENTER); + } + + @Override + public @NonNull Size size() { + return plot.size(); + } + + public Origin origin() { + return origin; + } + + public MathPlot origin(Origin origin) { + this.origin = origin; + int width = plot.plotSize().width(); + int height = plot.plotSize().height(); + switch (origin) { + case N: + originX = (width - 1) / 2.0; + originY = height - 1; + break; + case S: + originX = (width - 1) / 2.0; + originY = 0.0; + break; + case E: + originX = width - 1; + originY = (height - 1) / 2.0; + break; + case W: + originX = 0.0; + originY = (height - 1) / 2.0; + break; + case NE: + originX = width - 1; + originY = height - 1; + break; + case NW: + originX = 0.0; + originY = height - 1; + break; + case SE: + originX = width - 1; + originY = 0.0; + break; + case SW: + originX = 0.0; + originY = 0.0; + break; + case CENTER: + default: + originX = (width - 1) / 2.0; + originY = (height - 1) / 2.0; + break; + } + return this; + } + + public Number getMinXValue() { + return minXValue; + } + + public MathPlot minXValue(Number minXValue) { + this.minXValue = minXValue; + return this; + } + + public Number maxXValue() { + return maxXValue; + } + + public MathPlot maxXValue(Number maxXValue) { + this.maxXValue = maxXValue; + return this; + } + + public MathPlot xRange(Number minXValue, Number maxXValue) { + this.minXValue = minXValue; + this.maxXValue = maxXValue; + return this; + } + + public Number minYValue() { + return minYValue; + } + + public MathPlot minYValue(Number minYValue) { + this.minYValue = minYValue; + return this; + } + + public Number maxYValue() { + return maxYValue; + } + + public MathPlot maxYValue(Number maxYValue) { + this.maxYValue = maxYValue; + return this; + } + + public MathPlot yRange(Number minYValue, Number maxYValue) { + this.minYValue = minYValue; + this.maxYValue = maxYValue; + return this; + } + + public MathPlot ranges(Number minXValue, Number maxXValue, Number minYValue, Number maxYValue) { + this.minXValue = minXValue; + this.maxXValue = maxXValue; + this.minYValue = minYValue; + this.maxYValue = maxYValue; + return this; + } + + public @NonNull Style currentStyle() { + return plot.currentStyle(); + } + + public long currentStyleState() { + return plot.currentStyleState(); + } + + public MathPlot currentStyle(Style currentStyle) { + plot.currentStyle(currentStyle); + return this; + } + + public MathPlot currentStyleState(long currentStyleState) { + plot.currentStyleState(currentStyleState); + return this; + } + + public MathPlot plot(Function func) { + double xRange = maxXValue.doubleValue() - minXValue.doubleValue(); + double yRange = maxYValue.doubleValue() - minYValue.doubleValue(); + Size plotSize = plot.plotSize(); + int width = plotSize.width(); + int height = plotSize.height(); + if (width <= 0 || height <= 0 || xRange <= 0.0 || yRange <= 0.0) { + return this; + } + + // Precompute default positions for zero in the numeric range (where numeric 0 maps in + // default mapping). + double defaultZeroX = (0.0 - minXValue.doubleValue()) / xRange * (width - 1); + double defaultZeroY = (0.0 - minYValue.doubleValue()) / yRange * (height - 1); + + double deltaX = originX - defaultZeroX; + double deltaY = originY - defaultZeroY; + + boolean prevValid = false; + int prevX = 0; + int prevY = 0; + + for (int xi = 0; xi < width; xi++) { + // map pixel x to numeric x in [minX, maxX] + double tX = (width == 1) ? 0.0 : (double) xi / (width - 1); + double scaledX = minXValue.doubleValue() + tX * xRange; + + double scaledY = func.apply(scaledX); + if (Double.isNaN(scaledY) || Double.isInfinite(scaledY)) { + prevValid = false; + continue; + } + + // map numeric y to pixel y in default mapping (minY -> 0, maxY -> height-1) + double defaultPixelX = (scaledX - minXValue.doubleValue()) / xRange * (width - 1); + double defaultPixelY = (scaledY - minYValue.doubleValue()) / yRange * (height - 1); + + // apply origin shift so numeric zero lands where requested + int px = (int) Math.round(defaultPixelX + deltaX); + int py = (int) Math.round(defaultPixelY + deltaY); + + boolean currValid = !(px < 0 || px >= width || py < 0 || py >= height); + + if (prevValid && currValid) { + // draw line between prev and current using Bresenham + int x0 = prevX; + int y0 = prevY; + int x1 = px; + int y1 = py; + int dx = Math.abs(x1 - x0); + int sx = x0 < x1 ? 1 : -1; + int dy = Math.abs(y1 - y0); + int sy = y0 < y1 ? 1 : -1; + int err = dx - dy; + + while (true) { + if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) { + plot.plot(x0, y0); + } + if (x0 == x1 && y0 == y1) { + break; + } + int e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + x0 += sx; + } + if (e2 < dx) { + err += dx; + y0 += sy; + } + } + prevX = px; + prevY = py; + } else if (currValid) { + // start new segment (or single point) and record as previous + plot.plot(px, py); + prevX = px; + prevY = py; + prevValid = true; + } else { + // current invalid -> break continuity + prevValid = false; + } + } + return this; + } + + @Override + public void render() { + plot.render(); + } +} diff --git a/twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/plot/Plot.java b/twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/plot/Plot.java new file mode 100644 index 0000000..f350555 --- /dev/null +++ b/twinkle-chart/src/main/java/org/codejive/twinkle/components/graphs/plot/Plot.java @@ -0,0 +1,175 @@ +package org.codejive.twinkle.components.graphs.plot; + +import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.core.component.Canvas; +import org.codejive.twinkle.core.component.Component; +import org.codejive.twinkle.core.component.Panel; +import org.codejive.twinkle.core.component.Size; +import org.jspecify.annotations.NonNull; + +public class Plot implements Component { + private final Canvas canvas; + private final int cOrgX; + private final int cOrgY; + private final Size plotSize; + private long currentStyleState = Style.F_UNSTYLED; + + private static final char BLOCK_FULL = '\u2588'; + private static final char EMPTY = ' '; + + private static final char DOT_LOWER_LEFT = '\u2596'; + private static final char DOT_LOWER_RIGHT = '\u2597'; + private static final char DOT_UPPER_LEFT = '\u2598'; + private static final char DOT_UPPER_RIGHT = '\u259d'; + + private static final char DOTS_TL_BR = '\u259a'; + private static final char DOTS_BL_TR = '\u259e'; + + private static final char BLOCK_LEFT_HALF = '\u258c'; // full, left half + private static final char BLOCK_RIGHT_HALF = '\u2590'; // full, right half + private static final char BLOCK_TOP_HALF = '\u2580'; // full, top half + private static final char BLOCK_BOTTOM_HALF = '\u2584'; // full, bottom half + + private static final char HOLE_LOWER_LEFT = '\u259c'; + private static final char HOLE_LOWER_RIGHT = '\u259b'; + private static final char HOLE_UPPER_LEFT = '\u259f'; + private static final char HOLE_UPPER_RIGHT = '\u2599'; + + private static final char[] ALL_DOTS = { + EMPTY, DOT_LOWER_LEFT, DOT_LOWER_RIGHT, BLOCK_BOTTOM_HALF, + DOT_UPPER_LEFT, BLOCK_LEFT_HALF, DOTS_TL_BR, HOLE_UPPER_RIGHT, + DOT_UPPER_RIGHT, DOTS_BL_TR, BLOCK_RIGHT_HALF, HOLE_UPPER_LEFT, + BLOCK_TOP_HALF, HOLE_LOWER_RIGHT, HOLE_LOWER_LEFT, BLOCK_FULL + }; + + private static final char[] SINGLE_DOTS = { + DOT_LOWER_LEFT, DOT_LOWER_RIGHT, DOT_UPPER_LEFT, DOT_UPPER_RIGHT + }; + + private static final int dotIndex[] = {1, 2, 4, 7, 6, 13, 14, 8, 9, 11}; + + public static Plot of(Size size) { + return new Plot(Panel.of(size)); + } + + public static Plot of(Canvas canvas) { + return new Plot(canvas); + } + + public Plot(Canvas canvas) { + this.canvas = canvas; + this.cOrgX = 0; + this.cOrgY = canvas.size().height() - 1; + this.plotSize = Size.of(canvas.size().width() * 2, canvas.size().height() * 2); + } + + @Override + public @NonNull Size size() { + return canvas.size(); + } + + public @NonNull Size plotSize() { + return plotSize; + } + + public @NonNull Style currentStyle() { + return Style.of(currentStyleState); + } + + public long currentStyleState() { + return currentStyleState; + } + + public Plot currentStyle(Style currentStyle) { + this.currentStyleState = currentStyle.state(); + return this; + } + + public Plot currentStyleState(long currentStyleState) { + this.currentStyleState = currentStyleState; + return this; + } + + @Override + public void render() { + // There's nothing to do here since Plot directly manipulates the canvas + } + + public Plot plot(int x, int y) { + return plot(x, y, currentStyleState); + } + + public Plot plot(int x, int y, Style style) { + return plot(x, y, style.state()); + } + + public Plot plot(int x, int y, long styleState) { + int cx = cOrgX + x / 2; + int cy = cOrgY - y / 2; + int rx = x % 2; + int ry = y % 2; + char newDot = selectDot(rx, ry); + char existingDot = canvas.charAt(cx, cy); + char combinedDot = combineDots(existingDot, newDot); + canvas.setCharAt(cx, cy, styleState, combinedDot); + return this; + } + + public Plot unplot(int x, int y) { + int cx = cOrgX + x / 2; + int cy = cOrgY - y / 2; + int rx = x % 2; + int ry = y % 2; + char removeDot = selectDot(rx, ry); + char existingDot = canvas.charAt(cx, cy); + char combinedDot = uncombineDots(existingDot, removeDot); + canvas.setCharAt(cx, cy, currentStyleState, combinedDot); + return this; + } + + public Plot clear() { + for (int y = 0; y < size().height(); y++) { + for (int x = 0; x < size().width(); x++) { + canvas.setCharAt(x, y, currentStyleState, ' '); + } + } + return this; + } + + private char selectDot(int rx, int ry) { + int dotIdx = ry * 2 + rx; + return SINGLE_DOTS[dotIdx]; + } + + private int charToDotIndex(char c) { + if (c == '\u2584') { + return 3; + } else if (c == '\u258c') { + return 5; + } else if (c == '\u2590') { + return 10; + } else if (c == '\u2580') { + return 12; + } else if (c == '\u2588') { + return 15; + } else if (c >= '\u2596' && c <= '\u259f') { + int idx = c - '\u2596'; + return dotIndex[idx]; + } + return 0; + } + + private char combineDots(char existing, char added) { + int existingIdx = charToDotIndex(existing); + int addedIdx = charToDotIndex(added); + int combinedIdx = existingIdx | addedIdx; + return ALL_DOTS[combinedIdx]; + } + + private char uncombineDots(char existing, char removed) { + int existingIdx = charToDotIndex(existing); + int removedIdx = charToDotIndex(removed); + int combinedIdx = existingIdx & (~removedIdx); + return ALL_DOTS[combinedIdx]; + } +} diff --git a/twinkle-chart/src/test/java/examples/BarDemo.java b/twinkle-chart/src/test/java/examples/BarDemo.java new file mode 100644 index 0000000..47a7bae --- /dev/null +++ b/twinkle-chart/src/test/java/examples/BarDemo.java @@ -0,0 +1,65 @@ +package examples; + +import org.codejive.twinkle.components.graphs.bar.Bar; +import org.codejive.twinkle.components.graphs.bar.BarConfig; +import org.codejive.twinkle.components.graphs.bar.FracBarConfig; +import org.codejive.twinkle.core.component.Canvas; +import org.codejive.twinkle.core.component.Panel; + +public class BarDemo { + public static void main(String[] args) { + System.out.println("Simple Bar:"); + printSimpleBar(); + + System.out.println("Horizontal Bars:"); + printHorizontalBars(); + + System.out.println("Vertical Bars:"); + printVerticalBars(); + } + + private static void printSimpleBar() { + Panel pnl = Panel.of(20, 1); + Bar b = Bar.bar().setValue(42); + b.render(pnl); + System.out.println(pnl.toString()); + } + + private static void printHorizontalBars() { + Panel pnl = Panel.of(20, 4); + FracBarConfig cfg = FracBarConfig.create(); + renderHorizontal(pnl, cfg); + System.out.println(pnl.toString()); + + cfg.direction(BarConfig.Direction.R2L); + renderHorizontal(pnl, cfg); + System.out.println(pnl.toString()); + } + + private static void renderHorizontal(Panel pnl, FracBarConfig cfg) { + for (int i = 0; i < pnl.size().height(); i++) { + Canvas v = pnl.view(0, i, 20, 1); + Bar b = new Bar(cfg).setValue(30 + i * 27); + b.render(v); + } + } + + private static void printVerticalBars() { + Panel pnl = Panel.of(16, 8); + FracBarConfig cfg = FracBarConfig.create().direction(BarConfig.Direction.B2T); + renderVertical(pnl, cfg); + System.out.println(pnl.toString()); + + cfg.direction(BarConfig.Direction.T2B); + renderVertical(pnl, cfg); + System.out.println(pnl.toString()); + } + + private static void renderVertical(Panel pnl, FracBarConfig cfg) { + for (int i = 0; i < pnl.size().width(); i++) { + Canvas v = pnl.view(i, 0, 1, 8); + Bar b = new Bar(cfg).setValue(30 + i * 5.4d); + b.render(v); + } + } +} diff --git a/twinkle-chart/src/test/java/examples/MathPlotDemo.java b/twinkle-chart/src/test/java/examples/MathPlotDemo.java new file mode 100644 index 0000000..9102d26 --- /dev/null +++ b/twinkle-chart/src/test/java/examples/MathPlotDemo.java @@ -0,0 +1,14 @@ +package examples; + +import org.codejive.twinkle.components.graphs.plot.MathPlot; +import org.codejive.twinkle.core.component.Panel; + +public class MathPlotDemo { + public static void main(String[] args) { + Panel pnl = Panel.of(40, 20); + MathPlot p = MathPlot.of(pnl).ranges(-2 * Math.PI, 2 * Math.PI, -1.0, 1.0); + // plot a sine wave + p.plot(Math::sin); + System.out.println(pnl.toString()); + } +} diff --git a/twinkle-core/pom.xml b/twinkle-core/pom.xml new file mode 100644 index 0000000..e67a51e --- /dev/null +++ b/twinkle-core/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + + org.codejive.twinkle + twinkle + 1.0-SNAPSHOT + ../pom.xml + + + twinkle-core + jar + + Core module for Twinkle TUI library + + + + org.codejive.twinkle + twinkle-ansi + 1.0-SNAPSHOT + compile + + + + + diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Canvas.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Canvas.java new file mode 100644 index 0000000..5e7fe09 --- /dev/null +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Canvas.java @@ -0,0 +1,41 @@ +package org.codejive.twinkle.core.component; + +import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.core.text.StyledCharSequence; +import org.jspecify.annotations.NonNull; + +public interface Canvas extends Sized { + char charAt(int x, int y); + + int codepointAt(int x, int y); + + @NonNull String graphemeAt(int x, int y); + + long styleStateAt(int x, int y); + + @NonNull Style styleAt(int x, int y); + + void setCharAt(int x, int y, @NonNull Style style, char c); + + void setCharAt(int x, int y, long styleState, char c); + + void setCharAt(int x, int y, @NonNull Style style, int cp); + + void setCharAt(int x, int y, long styleState, int cp); + + void setCharAt(int x, int y, @NonNull Style style, @NonNull CharSequence grapheme); + + void setCharAt(int x, int y, long styleState, @NonNull CharSequence grapheme); + + int putStringAt(int x, int y, @NonNull Style style, @NonNull CharSequence str); + + int putStringAt(int x, int y, long styleState, @NonNull CharSequence str); + + int putStringAt(int x, int y, @NonNull StyledCharSequence str); + + default @NonNull Canvas view(int left, int top, int width, int height) { + return view(new Rect(left, top, width, height)); + } + + @NonNull Canvas view(@NonNull Rect rect); +} diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Component.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Component.java new file mode 100644 index 0000000..64ae20e --- /dev/null +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Component.java @@ -0,0 +1,5 @@ +package org.codejive.twinkle.core.component; + +public interface Component extends Sized { + void render(); +} diff --git a/src/main/java/org/codejive/context/terminal/FlexRect.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/FlexRect.java similarity index 83% rename from src/main/java/org/codejive/context/terminal/FlexRect.java rename to twinkle-core/src/main/java/org/codejive/twinkle/core/component/FlexRect.java index bb1c8bc..c8fcc61 100644 --- a/src/main/java/org/codejive/context/terminal/FlexRect.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/FlexRect.java @@ -1,4 +1,6 @@ -package org.codejive.context.terminal; +package org.codejive.twinkle.core.component; + +import org.jspecify.annotations.NonNull; /** * This class defines a rectangle (similar to Rect) but with the ability to have @@ -14,7 +16,7 @@ public FlexRect(int left, int top, int width, int height) { this.height = height; } - public Rect actualRect(Size availableSize) { + public @NonNull Rect actualRect(@NonNull Size availableSize) { Rect availableRect = new Rect(0, 0, availableSize.width(), availableSize.height()); int w = width >= 0 ? width : availableSize.width() - left + width + 1; int h = height >= 0 ? height : availableSize.height() - top + height + 1; diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Panel.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Panel.java new file mode 100644 index 0000000..d079e64 --- /dev/null +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Panel.java @@ -0,0 +1,40 @@ +package org.codejive.twinkle.core.component; + +import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.core.text.StyledBuffer; +import org.jspecify.annotations.NonNull; + +public interface Panel extends Canvas { + + @NonNull Panel resize(@NonNull Size newSize); + + @Override + default @NonNull PanelView view(int left, int top, int width, int height) { + return view(new Rect(left, top, width, height)); + } + + @Override + @NonNull PanelView view(@NonNull Rect rect); + + String toAnsiString(); + + default String toAnsiString(Style currentStyle) { + return toAnsiString(currentStyle.state()); + } + + String toAnsiString(long currentStyleState); + + static @NonNull Panel of(int width, int height) { + return of(Size.of(width, height)); + } + + static @NonNull Panel of(@NonNull Size size) { + return new StyledBufferPanel(size); + } + + static @NonNull Panel of(@NonNull StyledBuffer buffer) { + Rect rect = Rect.of(buffer.length(), 1); + StyledBuffer[] lines = new StyledBuffer[] {buffer}; + return new StyledBufferPanel(rect, lines); + } +} diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/component/PanelView.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/PanelView.java new file mode 100644 index 0000000..2225c05 --- /dev/null +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/PanelView.java @@ -0,0 +1,7 @@ +package org.codejive.twinkle.core.component; + +public interface PanelView extends Panel { + PanelView moveTo(int x, int y); + + PanelView moveBy(int dx, int dy); +} diff --git a/src/main/java/org/codejive/context/terminal/Rect.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Rect.java similarity index 65% rename from src/main/java/org/codejive/context/terminal/Rect.java rename to twinkle-core/src/main/java/org/codejive/twinkle/core/component/Rect.java index 701cc95..6777257 100644 --- a/src/main/java/org/codejive/context/terminal/Rect.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Rect.java @@ -1,8 +1,26 @@ -package org.codejive.context.terminal; +package org.codejive.twinkle.core.component; -public class Rect extends Size { +import org.jspecify.annotations.NonNull; + +public class Rect extends Size implements Sized { private final int left, top; + public static @NonNull Rect of(int width, int height) { + return new Rect(0, 0, width, height); + } + + public static @NonNull Rect of(@NonNull Size size) { + return new Rect(0, 0, size.width(), size.height()); + } + + public static @NonNull Rect of(int left, int top, int width, int height) { + return new Rect(left, top, width, height); + } + + public static @NonNull Rect of(int left, int top, @NonNull Size size) { + return new Rect(left, top, size.width(), size.height()); + } + public Rect(int left, int top, int width, int height) { super(width, height); this.left = left; @@ -25,25 +43,26 @@ public int bottom() { return top + height() - 1; } - public Size size() { - return new Size(width(), height()); + @Override + public @NonNull Size size() { + return this; } - public boolean outside(Rect other) { + public boolean outside(@NonNull Rect other) { return top() > other.bottom() || bottom() < other.top() || left() > other.right() || right() < other.left(); } - public boolean inside(Rect other) { + public boolean inside(@NonNull Rect other) { return top() >= other.top() && left() >= other.left() && bottom() <= other.bottom() && right() <= other.right(); } - public boolean overlap(Rect other) { + public boolean overlap(@NonNull Rect other) { return !outside(other) && !inside(other); } @@ -55,7 +74,7 @@ public Rect grow(int leftAmount, int topAmount, int rightAmount, int bottomAmoun Math.max(height() + topAmount + bottomAmount, 0)); } - public Rect limited(Rect availableRect) { + public Rect limited(@NonNull Rect availableRect) { int l = Math.max(left, availableRect.left()); int t = Math.max(top, availableRect.top()); int r = Math.min(right(), availableRect.right()); diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Rectangular.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Rectangular.java new file mode 100644 index 0000000..40c7a69 --- /dev/null +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Rectangular.java @@ -0,0 +1,7 @@ +package org.codejive.twinkle.core.component; + +import org.jspecify.annotations.NonNull; + +public interface Rectangular { + @NonNull Rect rect(); +} diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Renderable.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Renderable.java new file mode 100644 index 0000000..91dc19b --- /dev/null +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Renderable.java @@ -0,0 +1,5 @@ +package org.codejive.twinkle.core.component; + +public interface Renderable { + void render(Canvas canvas); +} diff --git a/src/main/java/org/codejive/context/terminal/Size.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Size.java similarity index 80% rename from src/main/java/org/codejive/context/terminal/Size.java rename to twinkle-core/src/main/java/org/codejive/twinkle/core/component/Size.java index 7ba7bd9..e8480d2 100644 --- a/src/main/java/org/codejive/context/terminal/Size.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Size.java @@ -1,11 +1,16 @@ -package org.codejive.context.terminal; +package org.codejive.twinkle.core.component; import java.util.Objects; +import org.jspecify.annotations.NonNull; public class Size { private final int width; private final int height; + public static @NonNull Size of(int width, int height) { + return new Size(width, height); + } + public Size(int width, int height) { assert width >= 0; assert height >= 0; diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Sized.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Sized.java new file mode 100644 index 0000000..a602776 --- /dev/null +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/Sized.java @@ -0,0 +1,7 @@ +package org.codejive.twinkle.core.component; + +import org.jspecify.annotations.NonNull; + +public interface Sized { + @NonNull Size size(); +} diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/component/StyledBufferPanel.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/StyledBufferPanel.java new file mode 100644 index 0000000..594c503 --- /dev/null +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/component/StyledBufferPanel.java @@ -0,0 +1,284 @@ +package org.codejive.twinkle.core.component; + +import static org.codejive.twinkle.core.text.StyledBuffer.REPLACEMENT_CHAR; + +import org.codejive.twinkle.ansi.Ansi; +import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.core.text.StyledBuffer; +import org.codejive.twinkle.core.text.StyledCharSequence; +import org.jspecify.annotations.NonNull; + +public class StyledBufferPanel implements Panel { + protected @NonNull Rect rect; + protected @NonNull StyledBuffer[] lines; + + public StyledBufferPanel(@NonNull Size size) { + this.rect = Rect.of(size); + this.lines = new StyledBuffer[size.height()]; + for (int i = 0; i < size.height(); i++) { + lines[i] = createBuffer(size.width()); + } + } + + protected StyledBufferPanel(@NonNull Rect rect, @NonNull StyledBuffer[] lines) { + this.rect = rect; + this.lines = lines; + } + + @Override + public @NonNull Size size() { + return rect.size(); + } + + protected @NonNull Rect rect() { + return rect; + } + + @Override + public char charAt(int x, int y) { + if (outside(x, y, 0)) { + return REPLACEMENT_CHAR; + } + return line(y).charAt(applyXOffset(x)); + } + + @Override + public int codepointAt(int x, int y) { + if (outside(x, y, 0)) { + return REPLACEMENT_CHAR; + } + return line(y).codepointAt(applyXOffset(x)); + } + + @Override + public @NonNull String graphemeAt(int x, int y) { + if (outside(x, y, 0)) { + return String.valueOf(REPLACEMENT_CHAR); + } + return line(y).graphemeAt(applyXOffset(x)); + } + + @Override + public @NonNull Style styleAt(int x, int y) { + if (outside(x, y, 0)) { + return Style.UNSTYLED; + } + return line(y).styleAt(applyXOffset(x)); + } + + @Override + public long styleStateAt(int x, int y) { + if (outside(x, y, 0)) { + return Style.F_UNSTYLED; + } + return line(y).styleStateAt(applyXOffset(x)); + } + + @Override + public void setCharAt(int x, int y, @NonNull Style style, char c) { + setCharAt(x, y, style.state(), c); + } + + @Override + public void setCharAt(int x, int y, long styleState, char c) { + if (outside(x, y, 0)) { + return; + } + line(y).setCharAt(applyXOffset(x), styleState, c); + } + + @Override + public void setCharAt(int x, int y, @NonNull Style style, int cp) { + setCharAt(x, y, style.state(), cp); + } + + @Override + public void setCharAt(int x, int y, long styleState, int cp) { + if (outside(x, y, 0)) { + return; + } + line(y).setCharAt(applyXOffset(x), styleState, cp); + } + + @Override + public void setCharAt(int x, int y, @NonNull Style style, @NonNull CharSequence grapheme) { + setCharAt(x, y, style.state(), grapheme); + } + + @Override + public void setCharAt(int x, int y, long styleState, @NonNull CharSequence grapheme) { + if (outside(x, y, 0)) { + return; + } + line(y).setCharAt(applyXOffset(x), styleState, grapheme); + } + + @Override + public int putStringAt(int x, int y, @NonNull Style style, @NonNull CharSequence str) { + return putStringAt(x, y, style.state(), str); + } + + @Override + public int putStringAt(int x, int y, long styleState, @NonNull CharSequence str) { + if (outside(x, y, str.length())) { + return str.length(); + } + return line(y).putStringAt(applyXOffset(x), styleState, str); + } + + @Override + public int putStringAt(int x, int y, @NonNull StyledCharSequence str) { + if (outside(x, y, str.length())) { + return str.length(); + } + return line(y).putStringAt(applyXOffset(x), str); + } + + @Override + public @NonNull Panel resize(@NonNull Size newSize) { + if (newSize.equals(size())) { + return this; + } + StyledBuffer[] newLines = new StyledBuffer[newSize.height()]; + for (int i = 0; i < newSize.height(); i++) { + if (i < lines.length) { + newLines[i] = lines[i].resize(newSize.width()); + } else { + newLines[i] = createBuffer(newSize.width()); + } + } + lines = newLines; + Rect r = rect(); + rect = Rect.of(r.left(), r.top(), newSize); + return this; + } + + private @NonNull StyledBuffer createBuffer(int width) { + return StyledBuffer.of(width); + } + + @Override + public @NonNull PanelView view(@NonNull Rect viewRect) { + return new StyledBufferPanelView(this, viewRect, lines); + } + + private StyledBuffer line(int y) { + y = applyYOffset(y); + return lines[y]; + } + + private int applyXOffset(int x) { + return x + rect().left(); + } + + private int applyYOffset(int y) { + return y + rect().top(); + } + + private boolean outside(int x, int y, int length) { + int xAdjusted = applyXOffset(x); + Rect r = rect(); + return (xAdjusted + length) < r.left() || xAdjusted > r.right() || invalidYOffset(y); + } + + private boolean invalidYOffset(int y) { + int yAdjusted = applyYOffset(y); + Rect r = rect(); + return yAdjusted < r.top() || yAdjusted > r.bottom() || y < 0 || y >= lines.length; + } + + @Override + public String toString() { + // Assuming only single-width characters for capacity estimation + // plus one extra for newline + int initialCapacity = (size().width() + 1) * size().height(); + StringBuilder sb = new StringBuilder(initialCapacity); + for (int y = 0; y < size().height(); y++) { + sb.append(line(y).toString()); + if (y < size().height() - 1) { + sb.append('\n'); + } + } + return sb.toString(); + } + + @Override + public String toAnsiString() { + // Assuming only single-width characters for capacity estimation + // plus 20 extra for escape codes and newline + int initialCapacity = (size().width() + 20) * size().height(); + StringBuilder sb = new StringBuilder(initialCapacity); + sb.append(Ansi.STYLE_RESET); + return toAnsiString(sb, Style.F_UNSTYLED).toString(); + } + + @Override + public String toAnsiString(long currentStyleState) { + // Assuming only single-width characters for capacity estimation + // plus 20 extra for escape codes and newline + int initialCapacity = (size().width() + 1) * size().height(); + StringBuilder sb = new StringBuilder(initialCapacity); + return toAnsiString(sb, currentStyleState).toString(); + } + + private @NonNull StringBuilder toAnsiString(StringBuilder sb, long currentStyleState) { + for (int y = 0; y < size().height(); y++) { + sb.append(line(y).toAnsiString(currentStyleState)); + currentStyleState = line(y).styleStateAt(size().width() - 1); + if (y < size().height() - 1) { + sb.append('\n'); + } + } + return sb; + } + + public static class StyledBufferPanelView extends StyledBufferPanel implements PanelView { + protected final @NonNull StyledBufferPanel parentPanel; + + protected StyledBufferPanelView( + @NonNull StyledBufferPanel parentPanel, + @NonNull Rect rect, + @NonNull StyledBuffer[] lines) { + super(rect, lines); + this.parentPanel = parentPanel; + } + + @Override + protected @NonNull Rect rect() { + Rect pr = parentPanel.rect(); + return Rect.of( + this.rect.left() + pr.left(), + this.rect.top() + pr.top(), + Math.min( + this.rect.size().width(), + Math.max(0, pr.size().width() - this.rect.left())), + Math.min( + this.rect.size().height(), + Math.max(0, pr.size().height() - this.rect.top()))); + } + + @Override + public @NonNull Panel resize(@NonNull Size newSize) { + if (newSize.equals(size())) { + return this; + } + Rect r = rect(); + rect = Rect.of(r.left(), r.top(), newSize); + return this; + } + + @Override + public PanelView moveTo(int x, int y) { + Rect r = rect(); + rect = Rect.of(x, y, r.size()); + return this; + } + + @Override + public PanelView moveBy(int dx, int dy) { + Rect r = rect(); + rect = Rect.of(r.left() + dx, r.top() + dy, r.size()); + return this; + } + } +} diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/components/Frame.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/components/Frame.java new file mode 100644 index 0000000..529cee0 --- /dev/null +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/components/Frame.java @@ -0,0 +1,38 @@ +package org.codejive.twinkle.core.components; + +import org.codejive.twinkle.core.component.Canvas; +import org.codejive.twinkle.core.component.Component; +import org.codejive.twinkle.core.component.Panel; +import org.codejive.twinkle.core.component.Size; +import org.jspecify.annotations.NonNull; + +public class Frame implements Component { + private final Canvas canvas; + private final Canvas innerCanvas; + + public static Frame of(Size size) { + return new Frame(Panel.of(size)); + } + + public static Frame of(Canvas canvas) { + return new Frame(canvas); + } + + public Frame(Canvas canvas) { + this.canvas = canvas; + if (canvas.size().width() < 2 || canvas.size().height() < 2) { + this.innerCanvas = canvas; + } else { + this.innerCanvas = + canvas.view(1, 1, canvas.size().width() - 2, canvas.size().height() - 2); + } + } + + @Override + public @NonNull Size size() { + return canvas.size(); + } + + @Override + public void render() {} +} diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledBuffer.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledBuffer.java new file mode 100644 index 0000000..3b2cb83 --- /dev/null +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledBuffer.java @@ -0,0 +1,82 @@ +package org.codejive.twinkle.core.text; + +import org.codejive.twinkle.ansi.Style; +import org.jspecify.annotations.NonNull; + +public interface StyledBuffer extends StyledCharSequence { + + char REPLACEMENT_CHAR = '\uFFFD'; + + default void setCharAt(int index, @NonNull Style style, char c) { + setCharAt(index, style.state(), c); + } + + void setCharAt(int index, long styleState, char c); + + default void setCharAt(int index, @NonNull Style style, int cp) { + setCharAt(index, style.state(), cp); + } + + void setCharAt(int index, long styleState, int cp); + + default void setCharAt(int index, @NonNull Style style, @NonNull CharSequence grapheme) { + setCharAt(index, style.state(), grapheme); + } + + void setCharAt(int index, long styleState, @NonNull CharSequence grapheme); + + default int putStringAt(int index, @NonNull Style style, @NonNull CharSequence str) { + return putStringAt(index, style.state(), str); + } + + int putStringAt(int index, long styleState, @NonNull CharSequence str); + + int putStringAt(int index, @NonNull StyledCharSequence str); + + @NonNull StyledBuffer resize(int newSize); + + /** + * Converts the buffer to an ANSI string, including ANSI escape codes for styles. This method + * resets the current style to default at the start of the string. + * + * @return The ANSI string representation of the styled buffer. + */ + @NonNull String toAnsiString(); + + /** + * Converts the buffer to an ANSI string, including ANSI escape codes for styles. This method + * takes into account the provided current style to generate a result that is as efficient as + * possible in terms of ANSI codes. + * + * @param currentStyle The current style to start with. + * @return The ANSI string representation of the styled buffer. + */ + default @NonNull String toAnsiString(Style currentStyle) { + return toAnsiString(currentStyle.state()); + } + + /** + * Converts the buffer to an ANSI string, including ANSI escape codes for styles. This method + * takes into account the provided current style to generate a result that is as efficient as + * possible in terms of ANSI codes. + * + * @param currentStyle The current style to start with. + * @return The ANSI string representation of the styled buffer. + */ + @NonNull String toAnsiString(long currentStyle); + + StyledBuffer EMPTY = + new StyledCodepointBuffer(0) { + @Override + public @NonNull StyledCodepointBuffer resize(int newSize) { + if (newSize != 0) { + throw new UnsupportedOperationException("Cannot resize EMPTY"); + } + return this; + } + }; + + static @NonNull StyledBuffer of(int width) { + return new StyledCodepointBuffer(width); + } +} diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCharSequence.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCharSequence.java new file mode 100644 index 0000000..844294c --- /dev/null +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCharSequence.java @@ -0,0 +1,43 @@ +package org.codejive.twinkle.core.text; + +import org.codejive.twinkle.ansi.Style; +import org.jspecify.annotations.NonNull; + +public interface StyledCharSequence { + + int length(); + + /** + * Returns the {@code char} value at the specified index. In contrast to the original {@link + * CharSequence#charAt(int)} specification, this method never throws an exception and always + * returns a valid character. If the index is out of bounds, it returns the Unicode replacement + * character. + * + * @param index the index of the {@code char} value to be returned + * @return the specified {@code char} value + */ + char charAt(int index); + + int codepointAt(int i); + + @NonNull String graphemeAt(int i); + + long styleStateAt(int i); + + @NonNull Style styleAt(int i); + + // @Override + @NonNull StyledCharSequence subSequence(int start, int end); + + static @NonNull StyledCharSequence fromString(@NonNull Style style, @NonNull String str) { + StyledStringBuilder builder = new StyledStringBuilder(str.length()); + builder.append(style, str); + return builder; + } + + static @NonNull StyledCharSequence fromString(long styleState, @NonNull String str) { + StyledStringBuilder builder = new StyledStringBuilder(str.length()); + builder.append(styleState, str); + return builder; + } +} diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCodepointBuffer.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCodepointBuffer.java new file mode 100644 index 0000000..00002c0 --- /dev/null +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCodepointBuffer.java @@ -0,0 +1,299 @@ +package org.codejive.twinkle.core.text; + +import org.codejive.twinkle.ansi.Ansi; +import org.codejive.twinkle.ansi.Style; +import org.jspecify.annotations.NonNull; + +public class StyledCodepointBuffer implements StyledBuffer { + protected int[] cpBuffer; + protected long[] styleBuffer; + + public StyledCodepointBuffer(int size) { + cpBuffer = new int[size]; + styleBuffer = new long[size]; + } + + protected StyledCodepointBuffer(int[] cpBuffer, long[] styleBuffer) { + if (cpBuffer.length != styleBuffer.length) { + throw new IllegalArgumentException( + "Codepoint buffer and style buffer must have the same length"); + } + this.cpBuffer = cpBuffer; + this.styleBuffer = styleBuffer; + } + + @Override + public int length() { + return cpBuffer.length; + } + + @Override + public char charAt(int index) { + if (invalidIndex(index)) { + return REPLACEMENT_CHAR; + } + if (Character.charCount(cpBuffer[index]) == 2) { + // TODO log warning about extended Unicode characters not being supported + return REPLACEMENT_CHAR; + } + return (char) cpBuffer[index]; + } + + @Override + public int codepointAt(int index) { + if (invalidIndex(index)) { + return REPLACEMENT_CHAR; + } + return cpBuffer[index]; + } + + @Override + public @NonNull String graphemeAt(int index) { + if (invalidIndex(index)) { + return String.valueOf(REPLACEMENT_CHAR); + } + return new String(Character.toChars(cpBuffer[index])); + } + + @Override + public long styleStateAt(int index) { + if (invalidIndex(index)) { + return Style.F_UNSTYLED; + } + return styleBuffer[index]; + } + + @Override + public @NonNull Style styleAt(int index) { + if (invalidIndex(index)) { + return Style.UNSTYLED; + } + return Style.of(styleBuffer[index]); + } + + @Override + public void setCharAt(int index, long styleState, char ch) { + if (invalidIndex(index)) { + return; + } + if (Character.isSurrogate(ch)) { + // TODO log warning about surrogate characters not being supported + ch = REPLACEMENT_CHAR; + } + setCharAt_(index, styleState, ch); + } + + private void setCharAt_(int index, long styleState, char ch) { + if (Character.isSurrogate(ch)) { + // TODO log warning about surrogate characters not being supported + ch = REPLACEMENT_CHAR; + } + cpBuffer[index] = ch; + styleBuffer[index] = styleState; + } + + @Override + public void setCharAt(int index, long styleState, int cp) { + if (invalidIndex(index)) { + return; + } + setCharAt_(index, styleState, cp); + } + + private void setCharAt_(int index, long styleState, int cp) { + cpBuffer[index] = cp; + styleBuffer[index] = styleState; + } + + @Override + public void setCharAt(int index, long styleState, @NonNull CharSequence grapheme) { + if (invalidIndex(index)) { + return; + } + setCharAt_(index, styleState, grapheme); + } + + private void setCharAt_(int index, long styleState, @NonNull CharSequence grapheme) { + if (grapheme.length() == 0) { + return; + } + int cp; + if (codepointCount(grapheme) > 1) { + // TODO log warning about extended Unicode graphemes not being supported + cp = REPLACEMENT_CHAR; + } else { + cp = codepointAt(grapheme, 0); + } + cpBuffer[index] = cp; + styleBuffer[index] = styleState; + } + + @Override + public int putStringAt(int index, long styleState, @NonNull CharSequence str) { + if (outside(index, str.length())) { + return str.length(); + } + // TODO this code can be optimized by avoiding calculating codepointCount + // and simply looping until the end of the char sequence is reached + int cpsCount = codepointCount(str); + int minIndex = 0; + int maxIndex = cpBuffer.length; + int startIndex = Math.max(index, minIndex); + int strStart = Math.max(startIndex - index, 0); + int endIndex = Math.min(index + cpsCount, maxIndex); + int len = endIndex - startIndex; + for (int i = 0; i < len; ) { + int cp = codepointAt(str, strStart + i); + setCharAt_(startIndex + i, styleState, cp); + i += Character.charCount(cp); + } + return cpsCount; + } + + @Override + public int putStringAt(int index, @NonNull StyledCharSequence str) { + if (outside(index, str.length())) { + return str.length(); + } + int minIndex = 0; + int maxIndex = cpBuffer.length; + int startIndex = Math.max(index, minIndex); + int endIndex = Math.min(index + str.length(), maxIndex); + int strStart = Math.max(startIndex - index, 0); + int len = endIndex - startIndex; + for (int i = 0; i < len; i++) { + setCharAt_( + startIndex + i, str.styleStateAt(strStart + i), str.codepointAt(strStart + i)); + } + return str.length(); + } + + @Override + public @NonNull StyledCharSequence subSequence(int start, int end) { + if (start < 0 || end > length() || start > end) { + throw new IndexOutOfBoundsException( + "Invalid subsequence range: " + start + " to " + end); + } + int subLength = end - start; + int[] subCpBuffer = new int[subLength]; + long[] subStyleBuffer = new long[subLength]; + System.arraycopy(cpBuffer, start, subCpBuffer, 0, subLength); + System.arraycopy(styleBuffer, start, subStyleBuffer, 0, subLength); + return new StyledCodepointBuffer(subCpBuffer, subStyleBuffer); + } + + @Override + public @NonNull StyledCodepointBuffer resize(int newSize) { + if (newSize == cpBuffer.length) { + return this; + } + int[] newCpBuffer = new int[newSize]; + long[] newStyleBuffer = new long[newSize]; + int copyLength = Math.min(newSize, length()); + System.arraycopy(cpBuffer, 0, newCpBuffer, 0, copyLength); + System.arraycopy(styleBuffer, 0, newStyleBuffer, 0, copyLength); + cpBuffer = newCpBuffer; + styleBuffer = newStyleBuffer; + return this; + } + + private static int codepointCount(@NonNull CharSequence str) { + int count = 0; + for (int i = 0; i < str.length(); ) { + int cp = codepointAt(str, i); + count++; + i += Character.charCount(cp); + } + return count; + } + + private static int codepointAt(@NonNull CharSequence str, int index) { + if (index < 0 || index >= str.length()) { + return REPLACEMENT_CHAR; + } + char ch = str.charAt(index); + if (Character.isHighSurrogate(ch) && (index + 1) < str.length()) { + char low = str.charAt(index + 1); + if (Character.isLowSurrogate(low)) { + return Character.toCodePoint(ch, low); + } + } else if (Character.isLowSurrogate(ch) && index > 0) { + char high = str.charAt(index - 1); + if (Character.isHighSurrogate(high)) { + return Character.toCodePoint(high, ch); + } + } + return ch; + } + + private boolean invalidIndex(int index) { + return index < 0 || index >= cpBuffer.length; + } + + private boolean outside(int index, int length) { + return (index + length) <= 0 || index >= cpBuffer.length; + } + + @Override + public @NonNull String toString() { + // Assuming only single-width characters for capacity estimation + int initialCapacity = length(); + StringBuilder sb = new StringBuilder(initialCapacity); + for (int i = 0; i < length(); i++) { + int cp = cpBuffer[i]; + if (cp == '\0') { + cp = ' '; + } + sb.appendCodePoint(cp); + } + return sb.toString(); + } + + @Override + public @NonNull String toAnsiString() { + // Assuming only single-width characters for capacity estimation + // plus 20 extra for escape codes + int initialCapacity = length() + 20; + StringBuilder sb = new StringBuilder(initialCapacity); + sb.append(Ansi.STYLE_RESET); + return toAnsiString(sb, Style.UNSTYLED.state()).toString(); + } + + /** + * Converts the buffer to an ANSI string, including ANSI escape codes for styles and colors. + * This method takes into account the provided current style to generate a result that is as + * efficient as possible in terms of ANSI codes. + * + *

A system property "twinkle.styledbuffer.toAnsi" can be set to "fast" or "short" to choose + * between two strategies for generating the ANSI string. The "fast" strategy generates the ANSI + * string in a single pass, while the "short" strategy generates two ANSI strings and returns + * the shorter one. The default is "short". + * + * @param styleState The current style to start with. + * @return The ANSI string representation of the styled buffer. + */ + @Override + public @NonNull String toAnsiString(long styleState) { + // Assuming only single-width characters for capacity estimation + // plus 20 extra for escape codes + int initialCapacity = length() + 20; + StringBuilder sb = new StringBuilder(initialCapacity); + return toAnsiString(sb, styleState).toString(); + } + + private @NonNull StringBuilder toAnsiString(StringBuilder sb, long lastStyleState) { + for (int i = 0; i < length(); i++) { + if (styleBuffer[i] != lastStyleState) { + Style style = Style.of(styleBuffer[i]); + sb.append(style.toAnsiString()); + lastStyleState = styleBuffer[i]; + } + int cp = cpBuffer[i]; + if (cp == '\0') { + cp = ' '; + } + sb.appendCodePoint(cp); + } + return sb; + } +} diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledStringBuilder.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledStringBuilder.java new file mode 100644 index 0000000..de47983 --- /dev/null +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledStringBuilder.java @@ -0,0 +1,170 @@ +package org.codejive.twinkle.core.text; + +import org.codejive.twinkle.ansi.Style; +import org.jspecify.annotations.NonNull; + +public class StyledStringBuilder implements StyledCharSequence { + private StyledBuffer buffer; + private int length; + + private static final int INITIAL_CAPACITY = 16; + + public static StyledStringBuilder create() { + return new StyledStringBuilder(INITIAL_CAPACITY); + } + + public static StyledStringBuilder create(int initialCapacity) { + return new StyledStringBuilder(initialCapacity); + } + + public static StyledStringBuilder of(Style style, CharSequence str) { + return of(style.state(), str); + } + + public static StyledStringBuilder of(long styleState, CharSequence str) { + StyledStringBuilder builder = new StyledStringBuilder(str.length() + INITIAL_CAPACITY); + builder.append(styleState, str); + return builder; + } + + public static StyledStringBuilder of(StyledCharSequence str) { + StyledStringBuilder builder = new StyledStringBuilder(str.length() + INITIAL_CAPACITY); + builder.append(str); + return builder; + } + + public StyledStringBuilder(int initialCapacity) { + this.buffer = StyledBuffer.of(initialCapacity); + this.length = 0; + } + + public StyledStringBuilder append(StyledCharSequence str) { + ensureCapacity(str.length()); + length += buffer.putStringAt(length, str); + return this; + } + + public StyledStringBuilder append(Style style, CharSequence str) { + ensureCapacity(str.length()); + length += buffer.putStringAt(length, style, str); + return this; + } + + public StyledStringBuilder append(long styleState, CharSequence str) { + ensureCapacity(str.length()); + length += buffer.putStringAt(length, styleState, str); + return this; + } + + public StyledStringBuilder append(Style style, Object obj) { + String str = String.valueOf(obj); + return append(style, str); + } + + public StyledStringBuilder append(long styleState, Object obj) { + String str = String.valueOf(obj); + return append(styleState, str); + } + + public StyledStringBuilder append(Style style, char ch) { + String str = String.valueOf(ch); + return append(style, str); + } + + public StyledStringBuilder append(long styleState, char ch) { + String str = String.valueOf(ch); + return append(styleState, str); + } + + public StyledStringBuilder append(Style style, long number) { + String str = String.valueOf(number); + return append(style, str); + } + + public StyledStringBuilder append(long styleState, long number) { + String str = String.valueOf(number); + return append(styleState, str); + } + + public StyledStringBuilder append(Style style, int number) { + String str = String.valueOf(number); + return append(style, str); + } + + public StyledStringBuilder append(long styleState, int number) { + String str = String.valueOf(number); + return append(styleState, str); + } + + public StyledStringBuilder append(Style style, double number) { + String str = String.valueOf(number); + return append(style, str); + } + + public StyledStringBuilder append(long styleState, double number) { + String str = String.valueOf(number); + return append(styleState, str); + } + + public StyledStringBuilder append(Style style, float number) { + String str = String.valueOf(number); + return append(style, str); + } + + public StyledStringBuilder append(long styleState, float number) { + String str = String.valueOf(number); + return append(styleState, str); + } + + public StyledStringBuilder append(Style style, boolean bool) { + String str = String.valueOf(bool); + return append(style, str); + } + + public StyledStringBuilder append(long styleState, boolean bool) { + String str = String.valueOf(bool); + return append(styleState, str); + } + + @Override + public int length() { + return length; + } + + @Override + public char charAt(int index) { + return buffer.charAt(index); + } + + @Override + public int codepointAt(int index) { + return buffer.codepointAt(index); + } + + @Override + public @NonNull String graphemeAt(int index) { + return buffer.graphemeAt(index); + } + + @Override + public long styleStateAt(int index) { + return buffer.styleStateAt(index); + } + + @Override + public @NonNull Style styleAt(int index) { + return buffer.styleAt(index); + } + + @Override + public @NonNull StyledCharSequence subSequence(int start, int end) { + return buffer.subSequence(start, end); + } + + private void ensureCapacity(int extraLength) { + int requiredLength = length + extraLength; + if (requiredLength > buffer.length()) { + buffer = buffer.resize(requiredLength + 2 * INITIAL_CAPACITY); + } + } +} diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/StyledBufferTimings.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/StyledBufferTimings.java new file mode 100644 index 0000000..225735f --- /dev/null +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/StyledBufferTimings.java @@ -0,0 +1,93 @@ +package org.codejive.twinkle.core.text; + +public class StyledBufferTimings { + private static int iterations = 1_000_000; + + public static void main(String[] args) { + System.out.println("Basics:"); + String simple = "0123456789"; + titer( + "Simple string length", + 10 * iterations, + () -> { + int len = simple.length(); + int total = 0; + for (int i = 0; i < len; i++) { + total += simple.charAt(i); + } + }); + titer( + "Simple string codePointCount", + 10 * iterations, + () -> { + int len = simple.codePointCount(0, simple.length()); + int total = 0; + for (int i = 0; i < len; ) { + int cp = simple.codePointAt(i); + total += cp; + i += Character.charCount(cp); + } + }); + titer( + "Simple string codepoints.length", + 10 * iterations, + () -> { + int total = 0; + for (int i : simple.codePoints().toArray()) { + total += i; + } + }); + + System.out.println("Timing simple strings:"); + timeSimpleString(StyledBuffer.of(1000)); + + System.out.println("Timing strings with surrogates:"); + timeStringWithSurrogates(StyledBuffer.of(1000)); + } + + private static void timeSimpleString(StyledBuffer buffer) { + titer( + buffer.getClass().getSimpleName(), + () -> { + for (int i = 0; i < 500; i += 10) { + buffer.putStringAt(i, 0, "0123456789"); + } + for (int i = 500; i < 1000; i += 10) { + buffer.putStringAt(i, 0, "0123456789"); + } + }); + } + + private static void timeStringWithSurrogates(StyledBuffer buffer) { + titer( + buffer.getClass().getSimpleName(), + () -> { + for (int i = 0; i < 500; i += 10) { + buffer.putStringAt(i, 0, "0123456789"); + } + for (int i = 500; i < 1000; i += 10) { + buffer.putStringAt(i, 0, "01234\uD83D\uDE8056789"); + } + }); + } + + private static void titer(String msg, Runnable func) { + time(msg, () -> iterate(iterations, func)); + } + + private static void titer(String msg, int iterations, Runnable func) { + time(msg, () -> iterate(iterations, func)); + } + + private static void iterate(int iterations, Runnable func) { + for (int iter = 0; iter < iterations; iter++) { + func.run(); + } + } + + private static void time(String msg, Runnable func) { + long startTime = System.nanoTime(); + func.run(); + System.out.println(msg + " " + (System.nanoTime() - startTime) / 1_000_000 + "ms"); + } +} diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestPanel.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestPanel.java new file mode 100644 index 0000000..eafb3b4 --- /dev/null +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestPanel.java @@ -0,0 +1,206 @@ +package org.codejive.twinkle.core.text; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.codejive.twinkle.ansi.Color; +import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.core.component.Canvas; +import org.codejive.twinkle.core.component.Panel; +import org.codejive.twinkle.core.component.PanelView; +import org.codejive.twinkle.core.component.Size; +import org.junit.jupiter.api.Test; + +public class TestPanel { + + @Test + public void testPanelCreation() { + Panel panel = Panel.of(10, 5); + Size size = panel.size(); + assertThat(size.width()).isEqualTo(10); + assertThat(size.height()).isEqualTo(5); + } + + @Test + public void testPanelDefaultInnerContent() { + Panel panel = Panel.of(10, 5); + Size size = panel.size(); + for (int y = 0; y < size.height(); y++) { + for (int x = 0; x < size.width(); x++) { + assertThat(panel.charAt(x, y)).isEqualTo('\0'); + assertThat(panel.styleAt(x, y)).isEqualTo(Style.UNSTYLED); + } + } + } + + @Test + public void testPanelDefaultOuterContent() { + Panel panel = Panel.of(10, 5); + Size size = panel.size(); + for (int y = -5; y < size.height() + 5; y++) { + for (int x = -5; x < size.width() + 5; x++) { + if (x >= 0 && x < size.width() && y >= 0 && y < size.height()) { + continue; // Skip inner content + } + assertThat(panel.charAt(x, y)).isEqualTo(StyledBuffer.REPLACEMENT_CHAR); + assertThat(panel.styleAt(x, y)).isEqualTo(Style.UNSTYLED); + } + } + } + + @Test + public void testPanelNewContents() { + Panel panel = createPanel(); + Size size = panel.size(); + for (int y = 0; y < size.height(); y++) { + for (int x = 0; x < size.width(); x++) { + assertThat(panel.charAt(x, y)).isEqualTo((char) ('A' + x + y * size.width())); + assertThat(panel.styleAt(x, y)).isEqualTo(Style.ofFgColor(Color.indexed(x))); + } + } + } + + @Test + public void testPanelView() { + Panel panel = createPanel(); + Canvas view = panel.view(1, 1, 3, 3); + Size size = view.size(); + for (int y = 0; y < size.height(); y++) { + for (int x = 0; x < size.width(); x++) { + assertThat(view.charAt(x, y)) + .isEqualTo((char) ('G' + x + y * panel.size().width())); + assertThat(view.styleAt(x, y)).isEqualTo(Style.ofFgColor(Color.indexed(x + 1))); + } + } + } + + @Test + public void testPanelViewOutside() { + Panel panel = createPanel(); + Canvas view = panel.view(1, 1, 3, 3); + Size size = view.size(); + for (int y = -2; y < size.height() + 2; y++) { + for (int x = -2; x < size.width() + 2; x++) { + if (x >= 0 && x < size.width() && y >= 0 && y < size.height()) { + continue; // Skip inner content + } + assertThat(view.charAt(x, y)).isEqualTo(StyledBuffer.REPLACEMENT_CHAR); + assertThat(view.styleAt(x, y)).isEqualTo(Style.UNSTYLED); + } + } + } + + @Test + public void testPanelNestedView() { + Panel panel = createPanel(); + Panel view1 = panel.view(1, 1, 3, 3); + Panel view2 = view1.view(1, 1, 2, 2); + Size size = view2.size(); + for (int y = 0; y < size.height(); y++) { + for (int x = 0; x < size.width(); x++) { + assertThat(view2.charAt(x, y)) + .isEqualTo((char) ('M' + x + y * panel.size().width())); + assertThat(view2.styleAt(x, y)).isEqualTo(Style.ofFgColor(Color.indexed(x + 2))); + } + } + } + + @Test + public void testPanelNestedViewOutside() { + Panel panel = createPanel(); + Panel view1 = panel.view(1, 1, 3, 3); + Panel view2 = view1.view(1, 1, 2, 2); + Size size = view2.size(); + for (int y = -2; y < size.height() + 2; y++) { + for (int x = -2; x < size.width() + 2; x++) { + if (x >= 0 && x < size.width() && y >= 0 && y < size.height()) { + continue; // Skip inner content + } + assertThat(view2.charAt(x, y)).isEqualTo(StyledBuffer.REPLACEMENT_CHAR); + assertThat(view2.styleAt(x, y)).isEqualTo(Style.UNSTYLED); + } + } + } + + @Test + public void testPanelNestedViewMoved() { + Panel panel = createPanel(); + PanelView view1 = panel.view(1, 1, 3, 3); + Panel view2 = view1.view(1, 1, 2, 2); + + view1.moveBy(1, 1); + + Size size = view2.size(); + for (int y = 0; y < size.height(); y++) { + for (int x = 0; x < size.width(); x++) { + assertThat(view2.charAt(x, y)) + .isEqualTo((char) ('S' + x + y * panel.size().width())); + assertThat(view2.styleAt(x, y)).isEqualTo(Style.ofFgColor(Color.indexed(x + 3))); + } + } + } + + @Test + public void testPanelNestedViewMovedFullyOutside() { + Panel panel = createPanel(); + PanelView view1 = panel.view(1, 1, 3, 3); + Panel view2 = view1.view(1, 1, 2, 2); + + view1.moveBy(10, 10); + + Size size = view2.size(); + for (int y = 0; y < size.height(); y++) { + for (int x = 0; x < size.width(); x++) { + assertThat(view2.charAt(x, y)).isEqualTo(StyledBuffer.REPLACEMENT_CHAR); + assertThat(view2.styleAt(x, y)).isEqualTo(Style.UNSTYLED); + } + } + } + + @Test + public void testPanelNestedViewMovedPartiallyOutside() { + Panel panel = createPanel(); + PanelView view1 = panel.view(1, 1, 3, 3); + Panel view2 = view1.view(1, 1, 2, 2); + + view1.moveTo(3, 3); + + Size size = view2.size(); + for (int y = 0; y < size.height(); y++) { + for (int x = 0; x < size.width(); x++) { + if (y == 0 && x == 0) { + assertThat(view2.charAt(x, y)).isEqualTo('Y'); + assertThat(view2.styleAt(x, y)) + .isEqualTo(Style.ofFgColor(Color.indexed(x + 4))); + } else { + assertThat(view2.charAt(x, y)).isEqualTo(StyledBuffer.REPLACEMENT_CHAR); + assertThat(view2.styleAt(x, y)).isEqualTo(Style.UNSTYLED); + } + } + } + } + + private Panel createPanel() { + Panel panel = Panel.of(5, 5); + Size size = panel.size(); + for (int y = 0; y < size.height(); y++) { + for (int x = 0; x < size.width(); x++) { + panel.setCharAt( + x, + y, + Style.ofFgColor(Color.indexed(x)), + (char) ('A' + x + y * size.width())); + } + } + return panel; + } + + private void printCanvas(Canvas canvas) { + Size size = canvas.size(); + for (int y = 0; y < size.height(); y++) { + for (int x = 0; x < size.width(); x++) { + System.out.print(canvas.charAt(x, y)); + } + System.out.println(); + } + } +} diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestStyledBuffer.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestStyledBuffer.java new file mode 100644 index 0000000..2d69ac7 --- /dev/null +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestStyledBuffer.java @@ -0,0 +1,105 @@ +package org.codejive.twinkle.core.text; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.codejive.twinkle.ansi.Ansi; +import org.codejive.twinkle.ansi.Style; +import org.junit.jupiter.api.Test; + +public class TestStyledBuffer { + @Test + public void testStyledBufferCreation() { + StyledBuffer buffer = StyledBuffer.of(10); + assertThat(buffer.length()).isEqualTo(10); + } + + @Test + public void testStyledBufferPutGetChar() { + StyledBuffer buffer = StyledBuffer.of(10); + for (int i = 0; i < buffer.length(); i++) { + buffer.setCharAt(i, Style.ITALIC.state(), (char) ('a' + i)); + } + for (int i = 0; i < buffer.length(); i++) { + assertThat(buffer.charAt(i)).isEqualTo((char) ('a' + i)); + assertThat(buffer.styleStateAt(i)).isEqualTo(Style.ITALIC.state()); + } + } + + @Test + public void testStyledBufferPutCharToString() { + StyledBuffer buffer = StyledBuffer.of(10); + for (int i = 0; i < buffer.length(); i++) { + buffer.setCharAt(i, Style.ITALIC.state(), (char) ('a' + i)); + } + assertThat(buffer.toString()).isEqualTo("abcdefghij"); + } + + @Test + public void testStyledBufferPutCharToAnsiString() { + StyledBuffer buffer = StyledBuffer.of(10); + for (int i = 0; i < buffer.length(); i++) { + Style style = i < 5 ? Style.ITALIC : Style.UNDERLINED; + buffer.setCharAt(i, style, (char) ('a' + i)); + } + assertThat(buffer.toAnsiString()) + .isEqualTo( + Ansi.STYLE_RESET + + Ansi.style(Ansi.ITALICIZED) + + "abcde" + + Ansi.style(Ansi.UNDERLINED) + + "fghij"); + } + + @Test + public void testStyledBufferPutCharToAnsiStringWithCurrentStyle() { + StyledBuffer buffer = StyledBuffer.of(10); + for (int i = 0; i < buffer.length(); i++) { + Style style = i < 5 ? Style.ITALIC : Style.UNDERLINED; + buffer.setCharAt(i, style, (char) ('a' + i)); + } + assertThat(buffer.toAnsiString(Style.F_ITALIC)) + .isEqualTo("abcde" + Ansi.style(Ansi.UNDERLINED) + "fghij"); + } + + @Test + public void testStyledBufferPutCharToAnsiStringWithUnderAndOverflow() { + StyledBuffer buffer = StyledBuffer.of(10); + for (int i = 0; i < buffer.length() + 10; i++) { + Style style = i < 10 ? Style.ITALIC : Style.UNDERLINED; + buffer.setCharAt(i - 5, style, (char) ('a' + i)); + } + assertThat(buffer.toAnsiString()) + .isEqualTo( + Ansi.STYLE_RESET + + Ansi.style(Ansi.ITALICIZED) + + "fghij" + + Ansi.style(Ansi.UNDERLINED) + + "klmno"); + } + + @Test + public void testStyledBufferPutStringGetChar() { + StyledBuffer buffer = StyledBuffer.of(10); + buffer.putStringAt(0, Style.ITALIC, "abcdefghij"); + for (int i = 0; i < buffer.length(); i++) { + assertThat(buffer.charAt(i)).isEqualTo((char) ('a' + i)); + assertThat(buffer.styleStateAt(i)).isEqualTo(Style.ITALIC.state()); + } + } + + @Test + public void testStyledBufferPutStyledString() { + StyledBuffer buffer = StyledBuffer.of(10); + buffer.putStringAt(0, StyledStringBuilder.of(Style.ITALIC, "abcdefghij")); + assertThat(buffer.toAnsiString()) + .isEqualTo(Ansi.STYLE_RESET + Ansi.style(Ansi.ITALICIZED) + "abcdefghij"); + } + + @Test + public void testStyledBufferPutStyledStringWithUnderAndOverflow() { + StyledBuffer buffer = StyledBuffer.of(10); + buffer.putStringAt(-5, StyledStringBuilder.of(Style.ITALIC, "xxxxxabcdefghijxxxxx")); + assertThat(buffer.toAnsiString()) + .isEqualTo(Ansi.STYLE_RESET + Ansi.style(Ansi.ITALICIZED) + "abcdefghij"); + } +} diff --git a/twinkle-tui/pom.xml b/twinkle-tui/pom.xml new file mode 100644 index 0000000..637640f --- /dev/null +++ b/twinkle-tui/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + + org.codejive.twinkle + twinkle + 1.0-SNAPSHOT + ../pom.xml + + + twinkle-tui + + + 8 + 8 + examples.Boxes + UTF-8 + 3.22.0 + + + + + org.codejive.twinkle + twinkle-core + 1.0-SNAPSHOT + compile + + + org.jline + jline-terminal + ${version.jline} + + + org.jline + jline-terminal-jansi + ${version.jline} + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + ${mainClass} + true + + + + + + + diff --git a/src/main/java/examples/Boxes.java b/twinkle-tui/src/main/java/examples/Boxes.java similarity index 90% rename from src/main/java/examples/Boxes.java rename to twinkle-tui/src/main/java/examples/Boxes.java index bd680ee..8320fd6 100644 --- a/src/main/java/examples/Boxes.java +++ b/twinkle-tui/src/main/java/examples/Boxes.java @@ -7,13 +7,13 @@ import java.io.IOException; import java.time.Instant; import java.util.Arrays; -import org.codejive.context.render.BorderRenderer; -import org.codejive.context.render.Box; -import org.codejive.context.render.BoxRenderer; -import org.codejive.context.styles.Style; -import org.codejive.context.terminal.Screen; -import org.codejive.context.terminal.Term; -import org.codejive.context.util.ScrollBuffer; +import org.codejive.twinkle.tui.render.BorderRenderer; +import org.codejive.twinkle.tui.render.Box; +import org.codejive.twinkle.tui.render.BoxRenderer; +import org.codejive.twinkle.tui.styles.Style; +import org.codejive.twinkle.tui.terminal.Screen; +import org.codejive.twinkle.tui.terminal.Term; +import org.codejive.twinkle.tui.util.ScrollBuffer; import org.jline.utils.AttributedString; import org.jline.utils.AttributedStringBuilder; diff --git a/src/main/java/examples/FullPanel.java b/twinkle-tui/src/main/java/examples/FullPanel.java similarity index 85% rename from src/main/java/examples/FullPanel.java rename to twinkle-tui/src/main/java/examples/FullPanel.java index d8b3848..dbcd5bb 100644 --- a/src/main/java/examples/FullPanel.java +++ b/twinkle-tui/src/main/java/examples/FullPanel.java @@ -6,11 +6,11 @@ import java.io.IOException; import java.util.Collections; -import org.codejive.context.render.BorderRenderer; -import org.codejive.context.render.Box; -import org.codejive.context.terminal.Canvas; -import org.codejive.context.terminal.Screen; -import org.codejive.context.terminal.Term; +import org.codejive.twinkle.core.component.Canvas; +import org.codejive.twinkle.tui.render.BorderRenderer; +import org.codejive.twinkle.tui.render.Box; +import org.codejive.twinkle.tui.terminal.Screen; +import org.codejive.twinkle.tui.terminal.Term; import org.jline.utils.AttributedStringBuilder; public class FullPanel { diff --git a/src/main/java/examples/InlinePanel.java b/twinkle-tui/src/main/java/examples/InlinePanel.java similarity index 87% rename from src/main/java/examples/InlinePanel.java rename to twinkle-tui/src/main/java/examples/InlinePanel.java index 58c031e..18f8c50 100644 --- a/src/main/java/examples/InlinePanel.java +++ b/twinkle-tui/src/main/java/examples/InlinePanel.java @@ -6,10 +6,10 @@ import java.io.IOException; import java.util.Collections; -import org.codejive.context.render.BorderRenderer; -import org.codejive.context.render.Box; -import org.codejive.context.terminal.Screen; -import org.codejive.context.terminal.Term; +import org.codejive.twinkle.tui.render.BorderRenderer; +import org.codejive.twinkle.tui.render.Box; +import org.codejive.twinkle.tui.terminal.Screen; +import org.codejive.twinkle.tui.terminal.Term; import org.jline.utils.AttributedStringBuilder; public class InlinePanel { diff --git a/src/main/java/examples/SimpleDom.java b/twinkle-tui/src/main/java/examples/SimpleDom.java similarity index 73% rename from src/main/java/examples/SimpleDom.java rename to twinkle-tui/src/main/java/examples/SimpleDom.java index b8abe73..4ecb16b 100644 --- a/src/main/java/examples/SimpleDom.java +++ b/twinkle-tui/src/main/java/examples/SimpleDom.java @@ -5,14 +5,14 @@ import java.io.IOException; import java.util.List; -import org.codejive.context.ciml.dom.ContextDocument; -import org.codejive.context.ciml.dom.PanelElement; -import org.codejive.context.ciml.dom.ScreenElement; -import org.codejive.context.ciml.layout.DomLayouter; -import org.codejive.context.render.Box; -import org.codejive.context.render.BoxRenderer; -import org.codejive.context.terminal.Screen; -import org.codejive.context.terminal.Term; +import org.codejive.twinkle.tui.ciml.dom.ContextDocument; +import org.codejive.twinkle.tui.ciml.dom.PanelElement; +import org.codejive.twinkle.tui.ciml.dom.ScreenElement; +import org.codejive.twinkle.tui.ciml.layout.DomLayouter; +import org.codejive.twinkle.tui.render.Box; +import org.codejive.twinkle.tui.render.BoxRenderer; +import org.codejive.twinkle.tui.terminal.Screen; +import org.codejive.twinkle.tui.terminal.Term; public class SimpleDom { private final Term term; diff --git a/src/main/java/examples/Util.java b/twinkle-tui/src/main/java/examples/Util.java similarity index 85% rename from src/main/java/examples/Util.java rename to twinkle-tui/src/main/java/examples/Util.java index c985f3e..0f834e1 100644 --- a/src/main/java/examples/Util.java +++ b/twinkle-tui/src/main/java/examples/Util.java @@ -1,10 +1,10 @@ package examples; -import org.codejive.context.render.Box; -import org.codejive.context.styles.Property; -import org.codejive.context.styles.Style; -import org.codejive.context.styles.Unit; -import org.codejive.context.styles.Value; +import org.codejive.twinkle.tui.render.Box; +import org.codejive.twinkle.tui.styles.Property; +import org.codejive.twinkle.tui.styles.Style; +import org.codejive.twinkle.tui.styles.Unit; +import org.codejive.twinkle.tui.styles.Value; class Util { diff --git a/src/main/java/org/codejive/context/ciml/dom/ContextDocument.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/ciml/dom/ContextDocument.java similarity index 89% rename from src/main/java/org/codejive/context/ciml/dom/ContextDocument.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/ciml/dom/ContextDocument.java index ed576b1..0098324 100644 --- a/src/main/java/org/codejive/context/ciml/dom/ContextDocument.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/ciml/dom/ContextDocument.java @@ -1,7 +1,7 @@ -package org.codejive.context.ciml.dom; +package org.codejive.twinkle.tui.ciml.dom; import com.sun.org.apache.xerces.internal.dom.DocumentImpl; -import org.codejive.context.styles.Style; +import org.codejive.twinkle.tui.styles.Style; import org.w3c.dom.DOMException; import org.w3c.dom.Element; diff --git a/src/main/java/org/codejive/context/ciml/dom/ContextElement.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/ciml/dom/ContextElement.java similarity index 97% rename from src/main/java/org/codejive/context/ciml/dom/ContextElement.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/ciml/dom/ContextElement.java index 427c873..fc59d94 100644 --- a/src/main/java/org/codejive/context/ciml/dom/ContextElement.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/ciml/dom/ContextElement.java @@ -1,7 +1,7 @@ -package org.codejive.context.ciml.dom; +package org.codejive.twinkle.tui.ciml.dom; import com.sun.org.apache.xerces.internal.dom.ElementNSImpl; -import org.codejive.context.styles.Style; +import org.codejive.twinkle.tui.styles.Style; import org.w3c.dom.Attr; import org.w3c.dom.DOMException; diff --git a/src/main/java/org/codejive/context/ciml/dom/PanelElement.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/ciml/dom/PanelElement.java similarity index 78% rename from src/main/java/org/codejive/context/ciml/dom/PanelElement.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/ciml/dom/PanelElement.java index 3acda2d..70721dc 100644 --- a/src/main/java/org/codejive/context/ciml/dom/PanelElement.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/ciml/dom/PanelElement.java @@ -1,4 +1,4 @@ -package org.codejive.context.ciml.dom; +package org.codejive.twinkle.tui.ciml.dom; public class PanelElement extends ContextElement { diff --git a/src/main/java/org/codejive/context/ciml/dom/ScreenElement.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/ciml/dom/ScreenElement.java similarity index 78% rename from src/main/java/org/codejive/context/ciml/dom/ScreenElement.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/ciml/dom/ScreenElement.java index 318424b..e240698 100644 --- a/src/main/java/org/codejive/context/ciml/dom/ScreenElement.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/ciml/dom/ScreenElement.java @@ -1,4 +1,4 @@ -package org.codejive.context.ciml.dom; +package org.codejive.twinkle.tui.ciml.dom; public class ScreenElement extends ContextElement { diff --git a/src/main/java/org/codejive/context/ciml/layout/DomLayouter.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/ciml/layout/DomLayouter.java similarity index 50% rename from src/main/java/org/codejive/context/ciml/layout/DomLayouter.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/ciml/layout/DomLayouter.java index 088293f..04a5be3 100644 --- a/src/main/java/org/codejive/context/ciml/layout/DomLayouter.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/ciml/layout/DomLayouter.java @@ -1,8 +1,8 @@ -package org.codejive.context.ciml.layout; +package org.codejive.twinkle.tui.ciml.layout; import java.util.List; -import org.codejive.context.ciml.dom.ContextDocument; -import org.codejive.context.render.Box; +import org.codejive.twinkle.tui.ciml.dom.ContextDocument; +import org.codejive.twinkle.tui.render.Box; public class DomLayouter { public List layout(ContextDocument doc) { diff --git a/src/main/java/org/codejive/context/events/Event.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/events/Event.java similarity index 53% rename from src/main/java/org/codejive/context/events/Event.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/events/Event.java index f21b512..44aa87c 100644 --- a/src/main/java/org/codejive/context/events/Event.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/events/Event.java @@ -1,4 +1,4 @@ -package org.codejive.context.events; +package org.codejive.twinkle.tui.events; public interface Event { T target(); diff --git a/src/main/java/org/codejive/context/events/EventEmitter.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/events/EventEmitter.java similarity index 92% rename from src/main/java/org/codejive/context/events/EventEmitter.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/events/EventEmitter.java index c83f6b8..f987f01 100644 --- a/src/main/java/org/codejive/context/events/EventEmitter.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/events/EventEmitter.java @@ -1,4 +1,4 @@ -package org.codejive.context.events; +package org.codejive.twinkle.tui.events; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/org/codejive/context/events/EventListener.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/events/EventListener.java similarity index 67% rename from src/main/java/org/codejive/context/events/EventListener.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/events/EventListener.java index 4a8b09a..021fd18 100644 --- a/src/main/java/org/codejive/context/events/EventListener.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/events/EventListener.java @@ -1,4 +1,4 @@ -package org.codejive.context.events; +package org.codejive.twinkle.tui.events; public interface EventListener { void handleEvent(T event); diff --git a/twinkle-tui/src/main/java/org/codejive/twinkle/tui/events/EventTarget.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/events/EventTarget.java new file mode 100644 index 0000000..c3ba0eb --- /dev/null +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/events/EventTarget.java @@ -0,0 +1,3 @@ +package org.codejive.twinkle.tui.events; + +public interface EventTarget {} diff --git a/src/main/java/org/codejive/context/events/ResizeEvent.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/events/ResizeEvent.java similarity index 79% rename from src/main/java/org/codejive/context/events/ResizeEvent.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/events/ResizeEvent.java index 5416c9c..f088419 100644 --- a/src/main/java/org/codejive/context/events/ResizeEvent.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/events/ResizeEvent.java @@ -1,6 +1,6 @@ -package org.codejive.context.events; +package org.codejive.twinkle.tui.events; -import org.codejive.context.terminal.Size; +import org.codejive.twinkle.core.component.Size; public class ResizeEvent implements Event { private final T target; diff --git a/src/main/java/org/codejive/context/render/BorderRenderer.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/render/BorderRenderer.java similarity index 86% rename from src/main/java/org/codejive/context/render/BorderRenderer.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/render/BorderRenderer.java index ae11422..9502446 100644 --- a/src/main/java/org/codejive/context/render/BorderRenderer.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/render/BorderRenderer.java @@ -1,8 +1,8 @@ -package org.codejive.context.render; +package org.codejive.twinkle.tui.render; -import org.codejive.context.terminal.Canvas; -import org.codejive.context.terminal.Rect; -import org.codejive.context.util.Util; +import org.codejive.twinkle.core.component.Canvas; +import org.codejive.twinkle.core.component.Rect; +import org.codejive.twinkle.tui.util.Util; import org.jline.utils.AttributedString; public class BorderRenderer { diff --git a/src/main/java/org/codejive/context/render/Box.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/render/Box.java similarity index 93% rename from src/main/java/org/codejive/context/render/Box.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/render/Box.java index e7474e1..911ed00 100644 --- a/src/main/java/org/codejive/context/render/Box.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/render/Box.java @@ -1,10 +1,10 @@ -package org.codejive.context.render; +package org.codejive.twinkle.tui.render; import java.util.List; -import org.codejive.context.styles.Property; -import org.codejive.context.styles.Style; -import org.codejive.context.terminal.Rect; -import org.codejive.context.terminal.Rectangular; +import org.codejive.twinkle.core.component.Rect; +import org.codejive.twinkle.core.component.Rectangular; +import org.codejive.twinkle.tui.styles.Property; +import org.codejive.twinkle.tui.styles.Style; import org.jline.utils.AttributedString; public class Box implements Rectangular { diff --git a/src/main/java/org/codejive/context/render/BoxRenderer.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/render/BoxRenderer.java similarity index 84% rename from src/main/java/org/codejive/context/render/BoxRenderer.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/render/BoxRenderer.java index e10547a..911558c 100644 --- a/src/main/java/org/codejive/context/render/BoxRenderer.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/render/BoxRenderer.java @@ -1,6 +1,6 @@ -package org.codejive.context.render; +package org.codejive.twinkle.tui.render; -import org.codejive.context.terminal.Canvas; +import org.codejive.twinkle.core.component.Canvas; import org.jline.utils.AttributedString; public class BoxRenderer { diff --git a/src/main/java/org/codejive/context/styles/CascadingStyle.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/styles/CascadingStyle.java similarity index 97% rename from src/main/java/org/codejive/context/styles/CascadingStyle.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/styles/CascadingStyle.java index 48916a1..46eae8d 100644 --- a/src/main/java/org/codejive/context/styles/CascadingStyle.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/styles/CascadingStyle.java @@ -1,4 +1,4 @@ -package org.codejive.context.styles; +package org.codejive.twinkle.tui.styles; import java.util.HashSet; import java.util.Set; diff --git a/src/main/java/org/codejive/context/styles/Property.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/styles/Property.java similarity index 94% rename from src/main/java/org/codejive/context/styles/Property.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/styles/Property.java index 6c8cc1d..1751fa6 100644 --- a/src/main/java/org/codejive/context/styles/Property.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/styles/Property.java @@ -1,6 +1,6 @@ -package org.codejive.context.styles; +package org.codejive.twinkle.tui.styles; -import static org.codejive.context.styles.Type.*; +import static org.codejive.twinkle.tui.styles.Type.*; public enum Property { left(false, length, percentage, INHERIT, INITIAL, REVERT, UNSET), diff --git a/src/main/java/org/codejive/context/styles/Style.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/styles/Style.java similarity index 97% rename from src/main/java/org/codejive/context/styles/Style.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/styles/Style.java index da1c060..c88c690 100644 --- a/src/main/java/org/codejive/context/styles/Style.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/styles/Style.java @@ -1,4 +1,4 @@ -package org.codejive.context.styles; +package org.codejive.twinkle.tui.styles; import java.util.Arrays; import java.util.HashMap; diff --git a/src/main/java/org/codejive/context/styles/Type.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/styles/Type.java similarity index 89% rename from src/main/java/org/codejive/context/styles/Type.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/styles/Type.java index 79e31be..e7e5048 100644 --- a/src/main/java/org/codejive/context/styles/Type.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/styles/Type.java @@ -1,4 +1,4 @@ -package org.codejive.context.styles; +package org.codejive.twinkle.tui.styles; public enum Type { color, diff --git a/src/main/java/org/codejive/context/styles/Unit.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/styles/Unit.java similarity index 59% rename from src/main/java/org/codejive/context/styles/Unit.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/styles/Unit.java index ddd1c09..d66d339 100644 --- a/src/main/java/org/codejive/context/styles/Unit.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/styles/Unit.java @@ -1,4 +1,4 @@ -package org.codejive.context.styles; +package org.codejive.twinkle.tui.styles; public enum Unit { px, diff --git a/src/main/java/org/codejive/context/styles/Value.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/styles/Value.java similarity index 94% rename from src/main/java/org/codejive/context/styles/Value.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/styles/Value.java index 788c5ee..96a6020 100644 --- a/src/main/java/org/codejive/context/styles/Value.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/styles/Value.java @@ -1,10 +1,10 @@ -package org.codejive.context.styles; +package org.codejive.twinkle.tui.styles; -import static org.codejive.context.styles.Type.integer; -import static org.codejive.context.styles.Type.length; -import static org.codejive.context.styles.Type.number; -import static org.codejive.context.styles.Type.percentage; -import static org.codejive.context.styles.Type.string; +import static org.codejive.twinkle.tui.styles.Type.integer; +import static org.codejive.twinkle.tui.styles.Type.length; +import static org.codejive.twinkle.tui.styles.Type.number; +import static org.codejive.twinkle.tui.styles.Type.percentage; +import static org.codejive.twinkle.tui.styles.Type.string; import java.util.Optional; import java.util.regex.Matcher; diff --git a/src/main/java/org/codejive/context/terminal/Input.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/Input.java similarity index 77% rename from src/main/java/org/codejive/context/terminal/Input.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/Input.java index e76949b..0a86760 100644 --- a/src/main/java/org/codejive/context/terminal/Input.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/Input.java @@ -1,4 +1,4 @@ -package org.codejive.context.terminal; +package org.codejive.twinkle.tui.terminal; import java.io.IOException; diff --git a/twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/Resizeable.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/Resizeable.java new file mode 100644 index 0000000..8cbc0e3 --- /dev/null +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/Resizeable.java @@ -0,0 +1,7 @@ +package org.codejive.twinkle.tui.terminal; + +import org.codejive.twinkle.core.component.Size; + +public interface Resizeable { + void onResize(Size newSize); +} diff --git a/twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/Screen.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/Screen.java new file mode 100644 index 0000000..3f7c030 --- /dev/null +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/Screen.java @@ -0,0 +1,8 @@ +package org.codejive.twinkle.tui.terminal; + +import org.codejive.twinkle.core.component.Canvas; +import org.codejive.twinkle.tui.events.EventTarget; + +public interface Screen extends Canvas, EventTarget, Resizeable { + void update(); +} diff --git a/src/main/java/org/codejive/context/terminal/Term.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/Term.java similarity index 75% rename from src/main/java/org/codejive/context/terminal/Term.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/Term.java index c505959..63bb4a3 100644 --- a/src/main/java/org/codejive/context/terminal/Term.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/Term.java @@ -1,11 +1,11 @@ -package org.codejive.context.terminal; +package org.codejive.twinkle.tui.terminal; -import java.io.Closeable; import java.io.Flushable; import java.io.IOException; -import org.codejive.context.terminal.impl.JlineTerm; +import org.codejive.twinkle.core.component.Size; +import org.codejive.twinkle.tui.terminal.impl.JlineTerm; -public interface Term extends Flushable, Closeable, Resizeable { +public interface Term extends Flushable, AutoCloseable, Resizeable { static Term create() throws IOException { return new JlineTerm(); } diff --git a/src/main/java/org/codejive/context/terminal/impl/BufferedScreen.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/impl/BufferedScreen.java similarity index 89% rename from src/main/java/org/codejive/context/terminal/impl/BufferedScreen.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/impl/BufferedScreen.java index 9024cc9..2438dea 100644 --- a/src/main/java/org/codejive/context/terminal/impl/BufferedScreen.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/impl/BufferedScreen.java @@ -1,11 +1,16 @@ -package org.codejive.context.terminal.impl; +package org.codejive.twinkle.tui.terminal.impl; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; -import org.codejive.context.events.EventEmitter; -import org.codejive.context.events.ResizeEvent; -import org.codejive.context.terminal.*; +import org.codejive.twinkle.core.component.FlexRect; +import org.codejive.twinkle.core.component.Rect; +import org.codejive.twinkle.core.component.Size; +import org.codejive.twinkle.tui.events.EventEmitter; +import org.codejive.twinkle.tui.events.ResizeEvent; +import org.codejive.twinkle.terminal.*; +import org.codejive.twinkle.tui.terminal.Screen; +import org.codejive.twinkle.tui.terminal.Term; import org.jline.utils.AttributedCharSequence; import org.jline.utils.AttributedString; import org.jline.utils.AttributedStringBuilder; diff --git a/src/main/java/org/codejive/context/terminal/impl/InputImpl.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/impl/JlineInput.java similarity index 71% rename from src/main/java/org/codejive/context/terminal/impl/InputImpl.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/impl/JlineInput.java index 379b126..dfa5159 100644 --- a/src/main/java/org/codejive/context/terminal/impl/InputImpl.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/impl/JlineInput.java @@ -1,14 +1,14 @@ -package org.codejive.context.terminal.impl; +package org.codejive.twinkle.tui.terminal.impl; import java.io.IOException; -import org.codejive.context.terminal.Input; +import org.codejive.twinkle.tui.terminal.Input; import org.jline.utils.NonBlockingReader; -public class InputImpl implements Input { +public class JlineInput implements Input { private final JlineTerm term; private final NonBlockingReader reader; - public InputImpl(JlineTerm term) { + public JlineInput(JlineTerm term) { this.term = term; this.reader = term.terminal.reader(); } diff --git a/src/main/java/org/codejive/context/terminal/impl/JlineTerm.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/impl/JlineTerm.java similarity index 78% rename from src/main/java/org/codejive/context/terminal/impl/JlineTerm.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/impl/JlineTerm.java index 700dcf9..f0ecd4c 100644 --- a/src/main/java/org/codejive/context/terminal/impl/JlineTerm.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/terminal/impl/JlineTerm.java @@ -1,13 +1,13 @@ -package org.codejive.context.terminal.impl; +package org.codejive.twinkle.tui.terminal.impl; import java.io.IOException; -import org.codejive.context.events.EventEmitter; -import org.codejive.context.events.EventTarget; -import org.codejive.context.events.ResizeEvent; -import org.codejive.context.terminal.Input; -import org.codejive.context.terminal.Screen; -import org.codejive.context.terminal.Size; -import org.codejive.context.terminal.Term; +import org.codejive.twinkle.core.component.Size; +import org.codejive.twinkle.tui.events.EventEmitter; +import org.codejive.twinkle.tui.events.EventTarget; +import org.codejive.twinkle.tui.events.ResizeEvent; +import org.codejive.twinkle.tui.terminal.Input; +import org.codejive.twinkle.tui.terminal.Screen; +import org.codejive.twinkle.tui.terminal.Term; import org.jline.terminal.Attributes; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; @@ -41,7 +41,7 @@ public Integer maxColors() { @Override public Input input() { - return new InputImpl(this); + return new JlineInput(this); } @Override diff --git a/twinkle-tui/src/main/java/org/codejive/twinkle/tui/util/EventEmittingReader.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/util/EventEmittingReader.java new file mode 100644 index 0000000..6198565 --- /dev/null +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/util/EventEmittingReader.java @@ -0,0 +1,228 @@ +package org.codejive.twinkle.tui.util; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.Reader; +import org.jline.utils.Log; +import org.jline.utils.NonBlockingReader; +import org.jline.utils.Timeout; + +public class EventEmittingReader extends NonBlockingReader implements AutoCloseable { + public static final int READ_EXPIRED = -2; + + private final Reader in; + + private int lastChar = READ_EXPIRED; + private boolean threadIsReading = false; + private IOException exception = null; + private long threadDelay = 60 * 1000; + private Thread thread; + + public EventEmittingReader(Reader in) { + this.in = in; + } + + private synchronized void startReadingThreadIfNeeded() { + if (thread == null) { + thread = new Thread(this::run); + thread.setName("EventEmittingReader non blocking reader thread"); + thread.setDaemon(true); + thread.start(); + } + } + + @Override + public void close() throws IOException { + /* + * The underlying input stream is closed first. This means that if the + * I/O thread was blocked waiting on input, it will be woken for us. + */ + in.close(); + if (thread != null) { + notify(); + } + } + + @Override + public synchronized boolean ready() throws IOException { + return lastChar >= 0 || in.ready(); + } + + @Override + public int readBuffered(char[] b, int off, int len, long timeout) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || off + len < b.length) { + throw new IllegalArgumentException(); + } else if (len == 0) { + return 0; + } else if (exception != null) { + assert lastChar == READ_EXPIRED; + IOException toBeThrown = exception; + exception = null; + throw toBeThrown; + } else if (lastChar >= -1) { + b[0] = (char) lastChar; + lastChar = READ_EXPIRED; + return 1; + } else if (!threadIsReading && timeout <= 0) { + return in.read(b, off, len); + } else { + // TODO: rework implementation to read as much as possible + int c = read(timeout, false); + if (c >= 0) { + b[off] = (char) c; + return 1; + } else { + return c; + } + } + } + + /** + * Attempts to read a character from the input stream for a specific period of time. + * + * @param timeout The amount of time to wait for the character + * @return The character read, -1 if EOF is reached, or -2 if the read timed out. + */ + protected synchronized int read(long timeout, boolean isPeek) throws IOException { + /* + * If the thread hit an IOException, we report it. + */ + if (exception != null) { + assert lastChar == READ_EXPIRED; + IOException toBeThrown = exception; + if (!isPeek) exception = null; + throw toBeThrown; + } + + /* + * If there was a pending character from the thread, then + * we send it. If the timeout is 0L or the thread was shut down + * then do a local read. + */ + if (lastChar >= -1) { + assert exception == null; + } else if (!isPeek && timeout <= 0L && !threadIsReading) { + lastChar = in.read(); + } else { + /* + * If the thread isn't reading already, then ask it to do so. + */ + if (!threadIsReading) { + threadIsReading = true; + startReadingThreadIfNeeded(); + notifyAll(); + } + + /* + * So the thread is currently doing the reading for us. So + * now we play the waiting game. + */ + Timeout t = new Timeout(timeout); + while (!t.elapsed()) { + try { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + wait(t.timeout()); + } catch (InterruptedException e) { + exception = (IOException) new InterruptedIOException().initCause(e); + } + + if (exception != null) { + assert lastChar == READ_EXPIRED; + + IOException toBeThrown = exception; + if (!isPeek) exception = null; + throw toBeThrown; + } + + if (lastChar >= -1) { + assert exception == null; + break; + } + } + } + + /* + * ch is the character that was just read. Either we set it because + * a local read was performed or the read thread set it (or failed to + * change it). We will return it's value, but if this was a peek + * operation, then we leave it in place. + */ + int ret = lastChar; + if (!isPeek) { + lastChar = READ_EXPIRED; + } + return ret; + } + + private void run() { + Log.debug("NonBlockingReader start"); + boolean needToRead; + + try { + while (true) { + + /* + * Synchronize to grab variables accessed by both this thread + * and the accessing thread. + */ + synchronized (this) { + needToRead = this.threadIsReading; + + try { + /* + * Nothing to do? Then wait. + */ + if (!needToRead) { + wait(threadDelay); + } + } catch (InterruptedException e) { + /* IGNORED */ + } + + needToRead = this.threadIsReading; + if (!needToRead) { + return; + } + } + + /* + * We're not shutting down, but we need to read. This cannot + * happen while we are holding the lock (which we aren't now). + */ + int charRead = READ_EXPIRED; + IOException failure = null; + try { + charRead = in.read(); + // if (charRead < 0) { + // continue; + // } + } catch (IOException e) { + failure = e; + // charRead = -1; + } + + /* + * Re-grab the lock to update the state. + */ + synchronized (this) { + exception = failure; + lastChar = charRead; + threadIsReading = false; + notify(); + } + } + } catch (Throwable t) { + Log.warn("Error in NonBlockingReader thread", t); + } finally { + Log.debug("NonBlockingReader shutdown"); + synchronized (this) { + thread = null; + threadIsReading = false; + } + } + } +} diff --git a/src/main/java/org/codejive/context/util/ScrollBuffer.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/util/ScrollBuffer.java similarity index 99% rename from src/main/java/org/codejive/context/util/ScrollBuffer.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/util/ScrollBuffer.java index 142f2a6..e9704cf 100644 --- a/src/main/java/org/codejive/context/util/ScrollBuffer.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/util/ScrollBuffer.java @@ -1,4 +1,4 @@ -package org.codejive.context.util; +package org.codejive.twinkle.tui.util; import java.util.Arrays; import org.jline.utils.AttributedString; diff --git a/src/main/java/org/codejive/context/util/Util.java b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/util/Util.java similarity index 86% rename from src/main/java/org/codejive/context/util/Util.java rename to twinkle-tui/src/main/java/org/codejive/twinkle/tui/util/Util.java index e06ffea..3952de8 100644 --- a/src/main/java/org/codejive/context/util/Util.java +++ b/twinkle-tui/src/main/java/org/codejive/twinkle/tui/util/Util.java @@ -1,4 +1,4 @@ -package org.codejive.context.util; +package org.codejive.twinkle.tui.util; public class Util { public static String repeat(String s, int times) { From e26b03b19b5eb7f88517aef4762eb70e27403dc3 Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Wed, 17 Dec 2025 18:01:29 +0100 Subject: [PATCH 6/6] ci: Update JDK version from 11 to 17 in CI workflow --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5955f8..0797165 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,10 +17,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v4 with: - java-version: '11' + java-version: '17' distribution: 'temurin' cache: maven - name: Build with Maven