FilePicker
FilePicker is an interactive filesystem browser. The user navigates directories, and when they press the select key the picker records the chosen path.
Quick start
import com.williamcallahan.tui4j.compat.bubbles.filepicker.FilePicker;
import com.williamcallahan.tui4j.compat.bubbletea.*;
public class App implements Model {
private FilePicker picker;
private String selectedPath;
public App() {
picker = new FilePicker();
picker.setCurrentDirectory("/home/user/documents");
picker.setAllowedTypes(".txt", ".md");
picker.setShowHidden(false);
}
@Override
public Command init() {
return picker.init(); // Reads the initial directory
}
@Override
public UpdateResult<App> update(Message msg) {
if (msg instanceof KeyPressMessage kp && "q".equals(kp.key())) {
return UpdateResult.from(this, QuitMessage::new);
}
if (msg instanceof WindowSizeMessage ws) {
picker.setTerminalSize(ws.width(), ws.height());
}
UpdateResult<FilePicker> result = picker.update(msg);
picker = result.model();
// Check whether a file was selected this cycle
if (picker.didSelectFile(msg)) {
selectedPath = picker.selectedPath();
}
return UpdateResult.from(this, result.command());
}
@Override
public String view() {
return picker.view()
+ (selectedPath != null ? "\nSelected: " + selectedPath : "");
}
}
Configuration
picker.setCurrentDirectory("/tmp"); // Starting directory
picker.setAllowedTypes(".java", ".xml"); // Only these extensions are selectable (empty = all)
picker.setFileAllowed(true); // Allow selecting files (default: true)
picker.setDirAllowed(false); // Allow selecting directories (default: false)
picker.setShowHidden(true); // Show dot-files
picker.setShowPermissions(true); // Show Unix permission string
picker.setShowSize(true); // Show file size
picker.setHeight(20); // Fixed height (autoHeight disabled)
picker.setCursorChar("→"); // Custom cursor symbol
Selection detection
// After update(), inspect the message to detect selection
boolean fileChosen = picker.didSelectFile(msg); // A file was selected
boolean dirChosen = picker.didSelectDirectory(msg); // A directory was opened
String path = picker.selectedPath(); // The chosen path (or null)
Key bindings
| Key | Action |
|---|---|
↑ / k | Move cursor up |
↓ / j | Move cursor down |
PgUp | Page up |
PgDown | Page down |
g / Home | Go to first entry |
G / End | Go to last entry |
Enter / l | Open directory / select file |
Backspace / h | Go up to parent directory |
Paginator
Paginator is a lightweight page-navigation indicator. It renders either Arabic numerals (1/5) or dot characters (●○○○○).
Quick start
import com.williamcallahan.tui4j.compat.bubbles.paginator.Paginator;
import com.williamcallahan.tui4j.compat.bubbles.paginator.Type;
Paginator pager = new Paginator(
Paginator.Option.withTotalPages(5),
Paginator.Option.withPerPage(10)
);
pager.setType(Type.Dots); // or Type.Arabic
// In your update() method:
pager.update(msg); // Handles next/prev page key bindings
// In view():
String indicator = pager.view(); // "●○○○○" or "1/5"
Navigation
pager.nextPage();
pager.prevPage();
boolean last = pager.onLastPage();
boolean first = pager.onFirstPage();
int page = pager.page(); // Zero-based
int total = pager.totalPages(); // Count
// Compute page slice for your data array:
Paginator.Bounds b = pager.getSliceBounds(items.length);
List<Item> visible = items.subList(b.start(), b.end());
Dot style
pager.setActiveDot("•");
pager.setInactiveDot("○");
List embeds a Paginator internally. You only need to use Paginator directly when building a custom paging UI outside of the List component.Stopwatch
Stopwatch measures elapsed time and renders it in Go’s compact duration format (1h2m3s, 500ms).
Quick start
import com.williamcallahan.tui4j.compat.bubbles.stopwatch.Stopwatch;
import com.williamcallahan.tui4j.compat.bubbletea.*;
import java.time.Duration;
public class App implements Model {
private Stopwatch sw = new Stopwatch(); // Ticks every 1 s by default
@Override
public Command init() { return sw.init(); } // Starts the tick loop
@Override
public UpdateResult<App> update(Message msg) {
if (msg instanceof KeyPressMessage kp) {
return switch (kp.key()) {
case "s" -> UpdateResult.from(this, sw.toggle());
case "r" -> UpdateResult.from(this, sw.reset());
case "q" -> UpdateResult.from(this, QuitMessage::new);
default -> {
UpdateResult<Stopwatch> r = sw.update(msg);
sw = r.model();
yield UpdateResult.from(this, r.command());
}
};
}
UpdateResult<Stopwatch> r = sw.update(msg);
sw = r.model();
return UpdateResult.from(this, r.command());
}
@Override
public String view() { return "Elapsed: " + sw.view(); }
}
Methods
Stopwatch sw = new Stopwatch(); // 1-second tick interval
Stopwatch sw = new Stopwatch(Duration.ofMillis(100)); // 100 ms interval
Command startCmd = sw.start(); // Resume (or start) the tick loop
Command stopCmd = sw.stop(); // Pause
Command toggleCmd = sw.toggle(); // Flip between running and stopped
Command resetCmd = sw.reset(); // Zero the elapsed time
Duration elapsed = sw.elapsed();
boolean running = sw.running();
Timer
Timer counts down from a set duration and fires a TimeoutMessage when it reaches zero.
Quick start
import com.williamcallahan.tui4j.compat.bubbles.timer.*;
import com.williamcallahan.tui4j.compat.bubbletea.*;
import java.time.Duration;
public class App implements Model {
private Timer timer = new Timer(Duration.ofSeconds(30));
@Override
public Command init() { return timer.init(); }
@Override
public UpdateResult<App> update(Message msg) {
if (msg instanceof TimeoutMessage) {
// Timer expired — do something
return UpdateResult.from(this, QuitMessage::new);
}
if (msg instanceof KeyPressMessage kp && "s".equals(kp.key())) {
return UpdateResult.from(this, timer.toggle());
}
UpdateResult<Timer> r = timer.update(msg);
timer = r.model();
return UpdateResult.from(this, r.command());
}
@Override
public String view() { return "Time remaining: " + timer.view(); }
}
Methods
new Timer() // Zero duration, 1-second tick
new Timer(Duration.ofMinutes(5)) // 5-minute countdown
new Timer(Duration.ofMinutes(5), Duration.ofMillis(500)) // Custom tick interval
Command startCmd = timer.start();
Command stopCmd = timer.stop();
Command toggleCmd = timer.toggle();
Duration remaining = timer.timeout();
boolean timedout = timer.timedout();
boolean running = timer.running();
timer.setTimeout(Duration.ofSeconds(60)); // Reset the countdown
TimeoutMessage is fired once when timeout reaches zero. Handle it in your update() to trigger any post-timer logic.Cursor
Cursor is the blinking cursor block used internally by TextInput and Textarea. You can use it standalone in custom input components.
Modes
import com.williamcallahan.tui4j.compat.bubbles.cursor.*;
Cursor cursor = new Cursor();
Command cmd = cursor.setMode(CursorMode.Blink); // Animated blink (default)
cursor.setMode(CursorMode.Static); // Always visible, no blink
cursor.setMode(CursorMode.Hide); // Invisible cursor
Focus and blur
Command blinkCmd = cursor.focus(); // Begin blinking — pass the returned command to your UpdateResult
cursor.blur(); // Stop blinking, hide cursor
Rendering
cursor.setChar("a"); // Character rendered at the cursor position
cursor.setStyle(Style.newStyle().foreground(Color.color("99")));
cursor.setTextStyle(Style.newStyle()); // Style for the character behind the cursor
String rendered = cursor.view();
Blink speed
cursor.setBlinkSpeed(Duration.ofMillis(400)); // Default: 530 ms
Help
Help renders a compact key binding legend. It integrates with any component that implements the KeyMap interface (such as List and Table).
import com.williamcallahan.tui4j.compat.bubbles.help.Help;
import com.williamcallahan.tui4j.compat.bubbles.help.KeyMap;
Help help = new Help();
// Render the short (one-line) legend
String shortHelp = help.view(myKeyMap);
// Toggle full / short view in response to "?"
help.setShowAll(!help.showAll());
KeyMap requires two methods:
public interface KeyMap {
Binding[] shortHelp(); // Most important bindings for the one-line legend
Binding[][] fullHelp(); // All bindings grouped into columns
}
Spinner and progress
Loading indicators and progress bars.
Components overview
Back to all components.