Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

# Twinkle

Twinkle is a Java framework for creating text-based user interfaces (TUIs) in terminal emulators. It provides a layered architecture with components for text manipulation, screen rendering, shapes/borders, and terminal abstraction.
Twinkle is a Java framework for creating text-based user interfaces (TUIs) in terminal emulators. It provides a layered architecture with components for text manipulation, screen rendering, shapes/borders, image encoding, and terminal abstraction.

## Architecture

Expand All @@ -10,6 +10,7 @@ The project follows a layered architecture:
- **Foundation**: Text utilities and ANSI escape code support
- **Rendering**: Screen buffers and double-buffering for flicker-free rendering
- **UI Components**: Drawing utilities for borders and shapes
- **Images**: [twinkle-image](/twinkle-image/) is a completely stand-alone library for rendering images in terminals
- **Terminal Access**: Abstraction layer with pluggable implementations

## Modules
Expand All @@ -34,6 +35,13 @@ The project follows a layered architecture:
- Line and corner styles
- Drawing utilities

- **`twinkle-image`** - Terminal image encoding framework (no dependencies)
- **Sixel** - Legacy DEC format (xterm, mlterm)
- **Kitty** - Modern format (Kitty, WezTerm)
- **iTerm2** - Inline format (iTerm2, WezTerm)
- **Block** - Unicode block-based fallback rendering
- Pluggable encoder implementations

### Terminal Implementations

- **`twinkle-terminal`** - Terminal access and management abstraction API
Expand All @@ -48,6 +56,7 @@ The project follows a layered architecture:

- **`examples`** - Example programs demonstrating Twinkle capabilities
- `BouncingTwinkleDemo` - Animated demo with bouncing text and ASCII borders
- `ImageEncoderDemo` - Image rendering demonstration with automatic encoder detection

## Building

Expand All @@ -64,16 +73,23 @@ After building, you can run the example programs to see Twinkle in action. This
```bash
# Bouncing animation demo
jbang bounce

# Image encoder demo
jbang image
```

Or if you want to use regular Java commands:

```bash
# Bouncing animation demo
java -jar examples/target/examples-1.0-SNAPSHOT.jar org.codejive.twinkle.examples.BouncingTwinkleDemo

# Image encoder demo
java -jar examples/target/examples-1.0-SNAPSHOT.jar org.codejive.twinkle.examples.ImageEncoderDemo
```

## Requirements

- Java 8 or higher (tests require Java 21)
- A terminal emulator with ANSI support
- For image rendering: Terminal with Sixel, Kitty, iTerm2, or Unicode block support
6 changes: 6 additions & 0 deletions examples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.codejive.twinkle</groupId>
<artifactId>twinkle-image</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.lalyos</groupId>
<artifactId>jfiglet</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package org.codejive.twinkle.demos;

// spotless:off
//DEPS org.codejive.twinkle:twinkle-terminal-aesh:1.0-SNAPSHOT
//DEPS org.codejive.twinkle:twinkle-image:1.0-SNAPSHOT
// spotless:on

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import org.codejive.twinkle.image.ImageEncoder;
import org.codejive.twinkle.image.ImageEncoders;
import org.codejive.twinkle.terminal.Terminal;

/**
* Demo application showing how to use the terminal image encoding framework.
*
* <p>This example demonstrates rendering images to the terminal using different encoders (Sixel,
* Kitty, iTerm2, and block-based Unicode rendering).
*
* <p>Usage: {@code ImageEncoderDemo [--encoder=<name>] [--all]}
*
* <p>Supported encoder names: sixel, kitty, iterm2, block-full, block-half, block-quadrant,
* block-sextant, block-octant.
*/
public class ImageEncoderDemo {

public static void main(String[] args) throws Exception {
try (Terminal terminal = Terminal.getDefault()) {
PrintWriter writer = terminal.writer();

// Create a simple test image
BufferedImage testImage = createTestImage(200, 150);

// Define target size in terminal rows/columns
int targetWidth = 20; // 20 columns wide
int targetHeight = 10; // 10 rows tall

writer.println("=== Image Encoder Demo ===");

boolean fitImage = true;

String encoderName = getEncoderArg(args);

if (encoderName != null) {
// Use a specific encoder requested via --encoder=
ImageEncoder.Provider provider = findProvider(encoderName);
if (provider == null) {
writer.println("Unknown encoder: " + encoderName);
writer.println(
"Available: sixel, kitty, iterm2, block-full, block-half,"
+ " block-quadrant, block-sextant, block-octant");
writer.flush();
return;
}
writer.println("Using encoder: " + provider.name());
writer.println();
writer.println("Rendering with " + provider.name() + " encoder:");
writer.flush();
renderImage(
provider.create(testImage, targetWidth, targetHeight, fitImage), writer);
writer.println("\n");
} else {
// Detect the best encoder for the current terminal
ImageEncoder.Provider bestProvider = ImageEncoders.best();
ImageEncoder detectedEncoder =
bestProvider.create(testImage, targetWidth, targetHeight, fitImage);
writer.println("Detected encoder: " + bestProvider.name());
writer.println();

// Try rendering with the detected encoder
writer.println("Rendering with " + bestProvider.name() + " encoder:");
writer.flush();
renderImage(detectedEncoder, writer);
writer.println("\n");

// Optionally try all available encoders
if (shouldTestAllEncoders(args)) {
writer.println("\n--- Testing all encoders ---\n");

for (ImageEncoder.Provider provider : ImageEncoders.providers()) {
testEncoder(
provider.name(),
provider.create(testImage, targetWidth, targetHeight, fitImage),
writer);
}
}
}

writer.println("\nDemo complete!");
writer.flush();
}
}

private static String getEncoderArg(String[] args) {
for (String arg : args) {
if (arg.startsWith("--encoder=")) {
return arg.substring("--encoder=".length());
}
}
return null;
}

private static ImageEncoder.Provider findProvider(String name) {
String normalized = name.toLowerCase();
List<ImageEncoder.Provider> all = ImageEncoders.providers();
for (ImageEncoder.Provider provider : all) {
String providerKey = provider.name().toLowerCase();
if (providerKey.equals(normalized)) {
return provider;
}
}
return null;
}

private static void testEncoder(String name, ImageEncoder encoder, PrintWriter writer)
throws IOException {
writer.println(name + " encoder:");
writer.flush();
renderImage(encoder, writer);
writer.println("\n");
}

private static void renderImage(ImageEncoder encoder, Appendable output) throws IOException {
encoder.render(output);
}

/**
* Creates a simple test image with a gradient and some shapes.
*
* @param width the image width
* @param height the image height
* @return the created test image
*/
private static BufferedImage createTestImage(int width, int height) {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();

// Draw gradient background
for (int y = 0; y < height; y++) {
float hue = (float) y / height;
Color color = Color.getHSBColor(hue, 0.8f, 0.9f);
g.setColor(color);
g.fillRect(0, y, width, 1);
}

// Draw some shapes
g.setColor(Color.WHITE);
g.fillOval(width / 4, height / 4, width / 2, height / 2);

g.setColor(Color.BLACK);
g.drawString("Test Image", width / 3, height / 2);

g.dispose();
return image;
}

private static boolean shouldTestAllEncoders(String[] args) {
for (String arg : args) {
if ("--all".equals(arg) || "-a".equals(arg)) {
return true;
}
}
return false;
}
}
6 changes: 5 additions & 1 deletion jbang-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
"bounce": {
"description": "A simple TUI that demonstrates a bouncing Twinkle.",
"script-ref": "examples/src/main/java/org/codejive/twinkle/demos/BouncingTwinkleDemo.java"
},
"image": {
"description": "A simple TUI that demonstrates image rendering.",
"script-ref": "examples/src/main/java/org/codejive/twinkle/demos/ImageEncoderDemo.java"
}
}
}
}
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<module>twinkle-text</module>
<module>twinkle-screen</module>
<module>twinkle-shapes</module>
<module>twinkle-image</module>
<module>twinkle-terminal</module>
<module>twinkle-terminal-aesh</module>
<module>twinkle-terminal-jline</module>
Expand Down
Loading
Loading