JinConsole is a Java 17 Swing-based console UI toolkit for building keyboard-driven desktop apps with a retro text-console feel. It provides a JinCanvas app lifecycle, a character-cell drawing API, focusable widgets, simple dialogs, file drop handling, and optional pixel rendering for richer components such as images.
- Character-cell rendering with
JinGraphics - Swing window management with fullscreen and multi-monitor shortcuts
- Focusable widgets with keyboard navigation
- Built-in UI widgets for buttons, inputs, tables, trees, directories, images, graphs, and diff review
- Color tags in text strings, such as
<#green>Hello - Optional AI-assisted text completion through
OllamaAPI - Maven project setup with Java 17
- Java 17 or newer
- Maven 3.8 or newer
Clone the repository and compile the project:
git clone <repository-url>
cd JinConsole
mvn compileThe project uses Maven and keeps source files under src/.
Run the included starter app:
mvn exec:java -Dexec.mainClass=lib.console.main.MyJinConsoleAppYou can also run the calculator or login examples:
mvn exec:java -Dexec.mainClass=lib.console.main.Calculator
mvn exec:java -Dexec.mainClass=lib.console.main.MyLoginScreenCreate a class that extends JinCanvas, implement the lifecycle methods, then start it with JinConsole.start(...).
package lib.console.main;
import java.awt.Graphics2D;
import java.awt.event.KeyEvent;
import lib.console.widgets.ButtonWidget;
public class MyApp extends JinCanvas {
private ButtonWidget quitButton;
public static void main(String[] args) {
JinConsole.start(new MyApp(), "My App", 40, 30, 1280, 720);
}
@Override
public void init() {
quitButton = new ButtonWidget("Quit", 2, 2, 1);
quitButton.setCallback(() -> System.exit(0));
quitButton.takeFocus();
addWidget(quitButton);
}
@Override
public void tick(float delta) {
// Update app state here.
}
@Override
public void draw(JinGraphics t2d) {
t2d.drawString("<#green>Hello JinConsole", 2, 1);
}
@Override
public void render(Graphics2D g2d) {
// Optional pixel-level drawing after the console is rendered.
}
@Override
public void destroy() {
// Cleanup here.
}
@Override
public void scroll(int amount) {
}
@Override
public void keyDown(KeyEvent e) {
}
@Override
public void keyUp(KeyEvent e) {
}
}JinCanvas gives each app a small set of lifecycle hooks:
init()creates widgets and sets initial focus.tick(float delta)updates app state once per frame.draw(JinGraphics t2d)draws console-cell content over widgets.render(Graphics2D g2d)draws pixel graphics after the console is rendered.destroy()cleans up before the app exits.scroll(int amount)handles scroll-wheel events.keyDown(KeyEvent e)andkeyUp(KeyEvent e)handle app-level keyboard input.onFilesDropped(List<File> files)can be overridden to handle drag-and-drop files.
Tab: move focus to the next linked widget.Shift + Tab: move focus to the previous linked widget.EnterorSpace: activate focused buttons, options, checkboxes, and table rows.F11: toggle fullscreen.F12: move the window to the next display.
Link focusable widgets with setNext(...) and setPrevious(...):
usernameInput.setNext(passwordInput);
passwordInput.setNext(loginButton);
usernameInput.takeFocus();JinGraphics draws in console cells. The top-left cell is (0, 0).
t2d.drawString("Plain text", 2, 2);
t2d.drawString("<#green>Green text", 2, 3);
t2d.drawBox(1, 1, 20, 5);
t2d.drawLine('-', 1, 8, 20, 8);
t2d.drawChar('*', 10, 10);Use ColorExtractor.cleanString(...) when measuring strings that contain color tags:
String text = "<#green>Hello JinConsole";
String clean = ColorExtractor.cleanString(text);
t2d.drawString(text, JinConsole.getColumns() / 2 - clean.length() / 2, 5);JinConsole includes simple blocking dialog helpers:
String name = JinConsole.prompt("Name:");
boolean confirmed = JinConsole.confirmDialog("Continue?");
int choice = JinConsole.optionsDialog("Pick one", "New", "Open", "Exit");
JinConsole.messageBox("Saved!");All widgets live in lib.console.widgets. Add widgets with addWidget(widget), and call takeFocus() on the widget that should receive keyboard input first.
A focusable button that runs a callback when the user presses Enter or Space.
ButtonWidget button = new ButtonWidget("Save", 2, 2, 1);
button.setCallback(() -> JinConsole.messageBox("Saved"));
button.takeFocus();
addWidget(button);Constructor:
new ButtonWidget(String title, int x, int y, int padding)A single-line or multi-line text input with cursor movement, selection, clipboard operations, undo/redo, optional line numbers, optional command history, and optional AI assistance.
InputWidget input = new InputWidget("Command:", 2, 5, 40);
input.commandHistoryEnabled = true;
input.onNewLine = () -> {
String command = input.getValue();
input.addCommandToHistory(command);
input.setValue("");
};
input.takeFocus();
addWidget(input);Multi-line input:
InputWidget editor = new InputWidget("Notes", 2, 8, 60, 10);
editor.legalCharacters += "\n\t";
editor.displayLineNumbers = true;
addWidget(editor);Constructors:
new InputWidget(String title, int x, int y, int width)
new InputWidget(String title, int x, int y, int width, int height)Useful fields and methods:
value,getValue(),setValue(String value)obscurefor password-style inputlegalCharactersto control accepted charactersdisplayLineNumberscommandHistoryEnabledandaddCommandToHistory(...)canAssistfor AI-assisted completiononNewLinecallback
A simple multi-line text editor widget backed by multiple InputWidget rows.
MultilineInput multiline = new MultilineInput(2, 2, 60, 8);
multiline.value = "Line one\nLine two";
multiline.updateInputs();
multiline.takeFocus();
addWidget(multiline);Constructor:
new MultilineInput(int x, int y, int width, int height)Displays static or dynamically updated text.
TextWidget status = new TextWidget(2, 2, 40, 3);
status.text = "Ready";
addWidget(status);Constructor:
new TextWidget(int column, int row, int width, int height)A keyboard-selectable list of options. Use Up and Down to move, then Enter or Space to select.
OptionsWidget options = new OptionsWidget(2, 4, "New File", "Open", "Exit");
options.setCallback((index, option) -> {
JinConsole.messageBox("Selected: " + option);
});
options.takeFocus();
addWidget(options);Scrollable panel mode:
options.title = "Actions";
options.panelWidth = 30;
options.panelHeight = 8;
options.onCancel = () -> options.blur();Constructor:
new OptionsWidget(int x, int y, String... options)A toggleable checkbox with an optional callback.
CheckboxWidget checkbox = new CheckboxWidget(2, 2, "Enable logs");
checkbox.setOnToggle(() -> {
System.out.println("Checked: " + checkbox.isChecked());
});
checkbox.takeFocus();
addWidget(checkbox);Constructor:
new CheckboxWidget(int x, int y, String label)Displays rows of data with selectable rows. Use Up and Down to move. Override onEnter() for row actions.
TableWidget table = new TableWidget("Users", 2, 2, "Name", "Role") {
@Override
public void onEnter() {
Object[] row = data.get(getSelected());
JinConsole.messageBox("Selected " + row[0]);
}
};
table.data.add(new Object[] {"Ada", "Admin"});
table.data.add(new Object[] {"Grace", "Developer"});
table.maxRows = 6;
table.takeFocus();
addWidget(table);Constructor:
new TableWidget(String title, int x, int y, String... headers)Useful fields and methods:
datasetData(ArrayList<Object[]> data)maxRowsalignmentwithTableWidget.LEFT,CENTER, orRIGHTgetSelected()andsetSelected(int selected)
An editable table with typed columns. Press Enter on a row to edit the first editable column, then use Enter to advance or commit and Escape to cancel.
EditableTableWidget table = new EditableTableWidget("Inventory", 2, 2, "Item", "Count", "Price");
table.data.add(new Object[] {"Keyboard", 3, 49.99});
table.data.add(new Object[] {"Mouse", 8, 19.99});
table.setColumnTypes(String.class, Integer.class, Double.class);
table.setColumnEditable(1, true);
table.setColumnEditable(2, true);
table.setColumnUnit(2, "USD");
table.takeFocus();
addWidget(table);Constructor:
new EditableTableWidget(String title, int x, int y, String... headers)A file browser built on TableWidget. Directories are shown first, ... moves to the parent directory, and handleFile(...) can be overridden for file actions.
DirectoryWidget files = new DirectoryWidget("Files", new File("."), 2, 2, 40) {
@Override
public void handleFile(File file) {
JinConsole.messageBox("Opened: " + file.getName());
}
};
files.setFilter(file -> !file.isHidden());
files.takeFocus();
addWidget(files);Constructor:
new DirectoryWidget(String title, File dir, int x, int y, int width)Displays a collapsible JSON-backed tree. Use Up and Down to move, Right to expand, Left to collapse, and Enter to toggle.
JSONObject treeData = new JSONObject()
.put("name", "Project")
.put("expanded", true)
.put("children", new JSONArray()
.put(new JSONObject().put("name", "src").put("expanded", true))
.put(new JSONObject().put("name", "README.md")));
TreeViewWidget tree = new TreeViewWidget(2, 2, 40, 10);
tree.loadFromJSON(treeData);
tree.takeFocus();
addWidget(tree);Constructor:
new TreeViewWidget(int x, int y, int width, int height)Draws a simple line graph from ArrayList<Point.Double> data.
ArrayList<Point.Double> points = new ArrayList<>();
points.add(new Point.Double(0, 1));
points.add(new Point.Double(1, 4));
points.add(new Point.Double(2, 2));
LineGraphWidget graph = new LineGraphWidget(2, 2, 30, 10);
graph.setData(points);
addWidget(graph);Constructor:
new LineGraphWidget(int x, int y, int width, int height)Displays a BufferedImage or Base64-encoded image with optional aspect-ratio preservation. It draws a console border and renders the image at pixel level.
BufferedImage image = ImageIO.read(new File("image.png"));
ImageWidget imageWidget = new ImageWidget(2, 2);
imageWidget.setImage(image);
imageWidget.imgWidth = 320;
imageWidget.imgHeight = 180;
imageWidget.keepAspectRatio = true;
imageWidget.setCallback(() -> JinConsole.messageBox("Image selected"));
addWidget(imageWidget);Constructor:
new ImageWidget(int x, int y)Displays the current date and time once per second.
TimeWidget clock = new TimeWidget(2, 2);
addWidget(clock);Constructor:
new TimeWidget(int x, int y)A ready-made TableWidget example containing month names and day counts.
MonthsTableWidget months = new MonthsTableWidget();
months.takeFocus();
addWidget(months);Constructor:
new MonthsTableWidget()A full-screen diff review panel with accept, reject, accept-all, and cancel actions.
GitDiffReviewWidget review = new GitDiffReviewWidget(0, 0, JinConsole.getColumns(), JinConsole.getRows());
review.setChange(0, 1, "src/App.java", diffText);
review.listener = new GitDiffReviewWidget.Listener() {
@Override
public void onAccept() {
review.destroy();
}
@Override
public void onReject() {
review.destroy();
}
@Override
public void onAcceptAll() {
review.destroy();
}
@Override
public void onCancel() {
review.destroy();
}
};
review.takeFocus();
addWidget(review);Constructor:
new GitDiffReviewWidget(int column, int row, int width, int height)Controls:
Y: acceptN: rejectA: accept allEscape: cancelUp,Down,Page Up,Page Down: scroll
Every Widget inherits focus and visibility helpers:
widget.takeFocus();
widget.blur();
widget.setVisible(false);
widget.destroy();Widgets also inherit JinCanvas page support. A widget is visible when its page matches the parent canvas page.
settingsButton.setPage("SETTINGS");
setPage("SETTINGS");src/lib/console/main Core window, canvas, rendering, and example apps
src/lib/console/widgets Built-in widgets
src/lib/console/util Utility classes for colors, files, syntax, HTTP, and AI APIs
pom.xml Maven build configuration
JinConsole currently uses:
org.seleniumhq.selenium:selenium-javaorg.json:json
See pom.xml for exact versions.
- The default console size is controlled by the
JinConsole.start(...)arguments. - Use
JinConsole.getColumns()andJinConsole.getRows()to position widgets relative to the current console dimensions. Settingscontrols font, cell size, spacing, and background.OllamaAPIdefaults to a local Ollama server athttp://localhost:11434and also contains provider support for OpenAI, Claude, and LM Studio.

