TasksTUI.java
package com.guinetik.hexafun.examples.tasks.tui;
import static com.guinetik.hexafun.examples.tui.Ansi.*;
import com.guinetik.hexafun.examples.tasks.Task;
import com.guinetik.hexafun.examples.tasks.TaskApp;
import com.guinetik.hexafun.examples.tasks.TaskStatus;
import com.guinetik.hexafun.examples.tui.HexaTerminal;
import com.guinetik.hexafun.examples.tui.View;
import com.guinetik.hexafun.examples.tui.Widgets;
import com.guinetik.hexafun.fun.Result;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.List;
import java.util.function.Function;
import java.util.function.UnaryOperator;
/**
* Functional TUI for the Tasks application.
*
* <p>Demonstrates functional composition in a terminal UI using the
* shared {@link com.guinetik.hexafun.examples.tui} package:
* <ul>
* <li>{@link State} - Immutable state record</li>
* <li>{@link View} - Pure function S → String (from shared tui package)</li>
* <li>{@link Views} - Composable view components</li>
* <li>{@link Widgets} - Reusable TUI widgets (from shared tui package)</li>
* <li>Single {@link #render(State)} side effect</li>
* </ul>
*
* <p>The pattern: {@code render(views.screen().apply(state))}
*/
public class TasksTUI {
// ═══════════════════════════════════════════════════════════════════
// IMMUTABLE STATE
// ═══════════════════════════════════════════════════════════════════
/**
* Immutable TUI state. All state transitions return new State instances.
*/
public record State(
TaskApp app,
int width,
String status,
String statusColor,
boolean running
) {
private static final int MIN_WIDTH = 60;
private static final int KANBAN_MIN_WIDTH = 80;
/** Create initial state */
public static State initial(TaskApp app) {
return new State(app, detectWidth(), "", GREEN, true);
}
/** State transitions - all return new State */
public State withStatus(String msg, String color) {
return new State(app, width, msg, color, running);
}
public State withWidth(int w) {
return new State(
app,
Math.max(MIN_WIDTH, w),
status,
statusColor,
running
);
}
public State refreshWidth() {
return withWidth(detectWidth());
}
public State stop() {
return new State(app, width, "Goodbye!", CYAN, false);
}
/** Derived state */
public boolean isKanban() {
return width >= KANBAN_MIN_WIDTH;
}
public List<Task> tasks() {
return app.listTasks();
}
public List<Task> byStatus(TaskStatus s) {
return tasks()
.stream()
.filter(t -> t.status() == s)
.toList();
}
/** Terminal width detection - delegates to shared Terminal utility */
private static int detectWidth() {
return HexaTerminal.detectWidth(MIN_WIDTH, 120);
}
}
// ═══════════════════════════════════════════════════════════════════
// VIEWS - PURE VIEW COMPONENTS (using shared View<S> interface)
// ═══════════════════════════════════════════════════════════════════
/**
* Pure view functions using the shared {@link View} interface.
* Each transforms State → String with no side effects.
*/
public static class Views {
/** The complete screen - composed from all views */
public static View<State> screen() {
return clear()
.andThen(header())
.andThen(stats())
.andThen(View.when(State::isKanban, kanban(), tasks()))
.andThen(menu())
.andThen(status())
.andThen(prompt());
}
// ─────────────────────────────────────────────────────────────
// Individual view components
// ─────────────────────────────────────────────────────────────
public static View<State> clear() {
return state -> CLEAR + CURSOR_HOME;
}
public static View<State> header() {
return state -> {
int w = state.width();
int headerWidth = w - 4;
String title = ICON_TASK + " TASK MANAGER";
String mode = state.isKanban() ? "KANBAN" : "LIST";
String hint = state.isKanban()
? ""
: " (<" + State.KANBAN_MIN_WIDTH + " cols)";
String subtitle =
"HexaFun Demo [" + w + "w " + mode + hint + "]";
return lines(
"",
color(
" " +
DBOX_TOP_LEFT +
repeat(DBOX_HORIZONTAL, headerWidth) +
DBOX_TOP_RIGHT,
CYAN
),
color(" " + DBOX_VERTICAL, CYAN) +
color(center(title, headerWidth), BOLD, BRIGHT_WHITE) +
color(DBOX_VERTICAL, CYAN),
color(" " + DBOX_VERTICAL, CYAN) +
color(center(subtitle, headerWidth), DIM) +
color(DBOX_VERTICAL, CYAN),
color(
" " +
DBOX_BOTTOM_LEFT +
repeat(DBOX_HORIZONTAL, headerWidth) +
DBOX_BOTTOM_RIGHT,
CYAN
),
""
);
};
}
public static View<State> stats() {
return state -> {
List<Task> tasks = state.tasks();
long total = tasks.size();
long todo = state.byStatus(TaskStatus.TODO).size();
long doing = state.byStatus(TaskStatus.DOING).size();
long done = state.byStatus(TaskStatus.DONE).size();
int percent = total > 0 ? (int) ((done * 100) / total) : 0;
// Fluid segment widths
int available = state.width() - 6;
int segW = available / 3;
int extra = available % 3;
String todoSeg = color(
center(BULLET + " " + todo + " TODO", segW + extra),
BG_YELLOW,
BLACK,
BOLD
);
String doingSeg = color(
center(ICON_FLAME + " " + doing + " DOING", segW),
BG_MAGENTA,
BRIGHT_WHITE,
BOLD
);
String doneSeg = color(
center(CHECK + " " + done + " DONE", segW),
BG_GREEN,
BLACK,
BOLD
);
// Progress bar
int barW = state.width() - 8;
int filled = (barW * percent) / 100;
int empty = barW - filled;
return lines(
" " +
todoSeg +
color(PL_LEFT, YELLOW, BG_MAGENTA) +
doingSeg +
color(PL_LEFT, MAGENTA, BG_GREEN) +
doneSeg +
color(PL_LEFT, GREEN),
"",
" " +
color(repeat(BLOCK_FULL, filled), GREEN) +
color(repeat(BLOCK_LIGHT, empty), BRIGHT_BLACK) +
color(String.format(" %3d%%", percent), DIM),
""
);
};
}
public static View<State> tasks() {
return state -> {
int w = state.width();
List<Task> tasks = state.tasks();
int lineW = w - 4 - 9;
StringBuilder sb = new StringBuilder();
sb
.append(
color(
" " +
BOX_HORIZONTAL +
" TASKS " +
repeat(BOX_HORIZONTAL, lineW),
DIM
)
)
.append("\n\n");
if (tasks.isEmpty()) {
sb
.append(
color(
" " +
center(
"No tasks yet. Press [a] to add one!",
w - 4
),
DIM,
ITALIC
)
)
.append("\n\n");
} else {
int idx = 1;
for (Task task : tasks) {
sb.append(taskCard(task, idx++, w));
}
}
return sb.toString();
};
}
private static String taskCard(Task task, int index, int width) {
String icon = switch (task.status()) {
case TODO -> BULLET;
case DOING -> ICON_FLAME;
case DONE -> CHECK;
};
String iconColor = switch (task.status()) {
case TODO -> YELLOW;
case DOING -> MAGENTA;
case DONE -> GREEN;
};
String titleColor = task.completed() ? DIM : BRIGHT_WHITE;
String suffix = switch (task.status()) {
case DONE -> color(" (done)", DIM, GREEN);
case DOING -> color(" (wip)", DIM, MAGENTA);
default -> "";
};
int suffixLen = task.status() == TaskStatus.DONE
? 8
: (task.status() == TaskStatus.DOING ? 7 : 0);
int available = width - 8 - suffixLen - 2;
String title = task.title();
if (title.length() > available) {
title = title.substring(0, Math.max(0, available - 3)) + "...";
}
StringBuilder sb = new StringBuilder();
sb
.append(" ")
.append(color(index + " ", BRIGHT_BLACK))
.append(color(icon + " ", iconColor))
.append(color(title, titleColor))
.append(suffix)
.append("\n");
if (task.description() != null && !task.description().isBlank()) {
int descW = width - 10;
String desc = task.description();
if (desc.length() > descW) desc =
desc.substring(0, Math.max(0, descW - 3)) + "...";
sb.append(color(" " + desc, DIM)).append("\n");
}
sb.append("\n");
return sb.toString();
}
public static View<State> kanban() {
return state -> {
List<Task> todo = state.byStatus(TaskStatus.TODO);
List<Task> doing = state.byStatus(TaskStatus.DOING);
List<Task> done = state.byStatus(TaskStatus.DONE);
int totalW = state.width() - 4;
int innerW = totalW - 2; // gutters
int colW = innerW / 3;
int extra = innerW % 3;
int c1 = colW + extra,
c2 = colW,
c3 = colW;
StringBuilder sb = new StringBuilder();
// Top borders
sb
.append(" ")
.append(
color(
BOX_TOP_LEFT +
repeat(BOX_HORIZONTAL, c1 - 2) +
BOX_TOP_RIGHT,
YELLOW
)
)
.append(" ")
.append(
color(
BOX_TOP_LEFT +
repeat(BOX_HORIZONTAL, c2 - 2) +
BOX_TOP_RIGHT,
MAGENTA
)
)
.append(" ")
.append(
color(
BOX_TOP_LEFT +
repeat(BOX_HORIZONTAL, c3 - 2) +
BOX_TOP_RIGHT,
GREEN
)
)
.append("\n");
// Headers
sb
.append(" ")
.append(color(BOX_VERTICAL, YELLOW))
.append(
color(
center(
BULLET + " TODO (" + todo.size() + ")",
c1 - 2
),
BOLD,
YELLOW
)
)
.append(color(BOX_VERTICAL, YELLOW))
.append(" ")
.append(color(BOX_VERTICAL, MAGENTA))
.append(
color(
center(
ICON_FLAME + " DOING (" + doing.size() + ")",
c2 - 2
),
BOLD,
MAGENTA
)
)
.append(color(BOX_VERTICAL, MAGENTA))
.append(" ")
.append(color(BOX_VERTICAL, GREEN))
.append(
color(
center(
CHECK + " DONE (" + done.size() + ")",
c3 - 2
),
BOLD,
GREEN
)
)
.append(color(BOX_VERTICAL, GREEN))
.append("\n");
// Separators
sb
.append(" ")
.append(
color(
BOX_T_RIGHT +
repeat(BOX_HORIZONTAL, c1 - 2) +
BOX_T_LEFT,
YELLOW
)
)
.append(" ")
.append(
color(
BOX_T_RIGHT +
repeat(BOX_HORIZONTAL, c2 - 2) +
BOX_T_LEFT,
MAGENTA
)
)
.append(" ")
.append(
color(
BOX_T_RIGHT +
repeat(BOX_HORIZONTAL, c3 - 2) +
BOX_T_LEFT,
GREEN
)
)
.append("\n");
// Task rows
int maxRows = Math.max(
todo.size(),
Math.max(doing.size(), done.size())
);
if (maxRows == 0) maxRows = 1;
int todoIdx = 1,
doingIdx = todo.size() + 1,
doneIdx = todo.size() + doing.size() + 1;
for (int i = 0; i < maxRows; i++) {
sb.append(" ").append(color(BOX_VERTICAL, YELLOW));
sb.append(
i < todo.size()
? color(
kanbanCard(todo.get(i), todoIdx++, c1 - 2),
BRIGHT_WHITE
)
: repeat(" ", c1 - 2)
);
sb.append(color(BOX_VERTICAL, YELLOW)).append(" ");
sb.append(color(BOX_VERTICAL, MAGENTA));
sb.append(
i < doing.size()
? color(
kanbanCard(doing.get(i), doingIdx++, c2 - 2),
BRIGHT_MAGENTA
)
: repeat(" ", c2 - 2)
);
sb.append(color(BOX_VERTICAL, MAGENTA)).append(" ");
sb.append(color(BOX_VERTICAL, GREEN));
sb.append(
i < done.size()
? color(
kanbanCard(done.get(i), doneIdx++, c3 - 2),
DIM
)
: repeat(" ", c3 - 2)
);
sb.append(color(BOX_VERTICAL, GREEN)).append("\n");
}
// Bottom borders
sb
.append(" ")
.append(
color(
BOX_BOTTOM_LEFT +
repeat(BOX_HORIZONTAL, c1 - 2) +
BOX_BOTTOM_RIGHT,
YELLOW
)
)
.append(" ")
.append(
color(
BOX_BOTTOM_LEFT +
repeat(BOX_HORIZONTAL, c2 - 2) +
BOX_BOTTOM_RIGHT,
MAGENTA
)
)
.append(" ")
.append(
color(
BOX_BOTTOM_LEFT +
repeat(BOX_HORIZONTAL, c3 - 2) +
BOX_BOTTOM_RIGHT,
GREEN
)
)
.append("\n\n");
return sb.toString();
};
}
private static String kanbanCard(Task task, int index, int width) {
String title = task.title();
int maxLen = width - 4;
if (title.length() > maxLen) title =
title.substring(0, Math.max(0, maxLen - 2)) + "..";
return pad(" " + index + " " + title, width);
}
public static View<State> menu() {
return state ->
lines(
color(
" " + repeat(BOX_HORIZONTAL, state.width() - 4),
DIM
),
"",
" " +
color("[a]", CYAN, BOLD) +
color(" " + ICON_ADD + " Add ", CYAN) +
color("[v]", BLUE, BOLD) +
color(" " + ICON_EYE + " View ", BLUE) +
color("[s]", MAGENTA, BOLD) +
color(" " + ICON_FLAME + " Start ", MAGENTA) +
color("[c]", GREEN, BOLD) +
color(" " + CHECK + " Done ", GREEN) +
color("[d]", RED, BOLD) +
color(" " + ICON_TRASH + " Del ", RED) +
color("[q]", BRIGHT_BLACK, BOLD) +
color(" " + CROSS + " Quit", BRIGHT_BLACK),
""
);
}
public static View<State> status() {
return state ->
state.status().isEmpty()
? "\n"
: color(
" " + ARROW_RIGHT + " " + state.status(),
state.statusColor()
) +
"\n\n";
}
public static View<State> prompt() {
return state -> color(" > ", CYAN, BOLD);
}
}
// ═══════════════════════════════════════════════════════════════════
// ACTIONS - STATE TRANSITIONS VIA RESULT
// ═══════════════════════════════════════════════════════════════════
/**
* Actions transform State → Result<State>.
* Success = new state, Failure = error message to display.
*/
@FunctionalInterface
public interface Action extends Function<State, Result<State>> {
/** Chain actions: if first succeeds, apply second */
default Action andThen(Action next) {
return state -> this.apply(state).flatMap(next);
}
/** Action that always succeeds with given state transform */
static Action of(UnaryOperator<State> transform) {
return state -> Result.ok(transform.apply(state));
}
/** Action from a Result-returning operation */
static Action fromResult(Function<State, Result<State>> f) {
return f::apply;
}
}
/**
* Action implementations for each command.
*/
public static class Actions {
public static Result<State> create(
State state,
String title,
String desc
) {
return state
.app()
.createTask(title, desc)
.map(task ->
state.withStatus("Created: " + task.title(), GREEN)
);
}
public static Result<State> start(State state, String taskId) {
return state
.app()
.startTask(taskId)
.map(task ->
state.withStatus("Started: " + task.title(), MAGENTA)
);
}
public static Result<State> complete(State state, String taskId) {
return state
.app()
.completeTask(taskId)
.map(task ->
state.withStatus("Completed: " + task.title(), GREEN)
);
}
public static Result<State> delete(State state, String taskId) {
return state
.app()
.deleteTask(taskId)
.map(ok -> state.withStatus("Deleted task", RED));
}
/** Quick action: advances task through workflow based on current status */
public static Result<State> quickAction(State state, int index) {
List<Task> tasks = state.tasks();
if (index < 1 || index > tasks.size()) {
return Result.fail("Invalid task number: " + index);
}
Task task = tasks.get(index - 1);
return switch (task.status()) {
case TODO -> start(state, task.id());
case DOING -> complete(state, task.id());
case DONE -> Result.ok(
state.withStatus(
"Already completed: " + task.title(),
YELLOW
)
);
};
}
}
// ═══════════════════════════════════════════════════════════════════
// RUNTIME - THE ONLY SIDE EFFECTS
// ═══════════════════════════════════════════════════════════════════
private final BufferedReader reader;
private final View<State> screen;
public TasksTUI() {
this.reader = new BufferedReader(new InputStreamReader(System.in));
this.screen = Views.screen();
}
/** The ONE place where we print to console */
private void render(State state) {
System.out.print(screen.apply(state));
System.out.flush();
}
/** Read a line of input */
private String readLine() {
try {
return reader.readLine();
} catch (Exception e) {
return null;
}
}
/** Prompt for additional input during an action */
private String prompt(String message) {
System.out.print(color(" " + message + ": ", CYAN));
System.out.flush();
return readLine();
}
/** Main loop: render → read → process → repeat */
public void run(TaskApp app) {
State state = State.initial(app);
// Enter alternate screen buffer (doesn't pollute scrollback)
System.out.print(ALT_SCREEN_ON);
System.out.flush();
// Ensure cleanup on exit
Runtime.getRuntime().addShutdownHook(new Thread(this::restoreTerminal));
try {
while (state.running()) {
state = state.refreshWidth();
render(state);
String input = readLine();
state = processInput(state, input);
}
render(state); // Final render with goodbye message
} finally {
restoreTerminal();
}
}
/** Restore terminal to normal state */
private void restoreTerminal() {
System.out.print(ALT_SCREEN_OFF);
System.out.flush();
}
/** Process input and return new state */
private State processInput(State state, String input) {
if (
input == null ||
input.equalsIgnoreCase("q") ||
input.equalsIgnoreCase("quit")
) {
return state.stop();
}
String cmd = input.toLowerCase().trim();
// Handle commands
Result<State> result = switch (cmd) {
case "a", "add" -> handleAdd(state);
case "v", "view" -> handleView(state);
case "s", "start" -> handleStart(state);
case "c", "complete" -> handleComplete(state);
case "d", "delete" -> handleDelete(state);
case "" -> Result.ok(state); // Just refresh
default -> {
if (cmd.matches("\\d+")) {
yield Actions.quickAction(state, Integer.parseInt(cmd));
}
yield Result.ok(
state.withStatus("Unknown command: " + input, RED)
);
}
};
// Fold result back to state
return result.fold(
error -> state.withStatus("Error: " + error, RED),
newState -> newState
);
}
// ─────────────────────────────────────────────────────────────────
// Interactive handlers (minimal I/O, delegate to Actions)
// ─────────────────────────────────────────────────────────────────
private Result<State> handleAdd(State state) {
System.out.println();
String title = prompt("Title");
if (title == null || title.isBlank()) {
return Result.ok(
state.withStatus("Cancelled - title required", YELLOW)
);
}
String desc = prompt("Description (optional)");
return Actions.create(state, title, desc != null ? desc : "");
}
private Result<State> handleView(State state) {
List<Task> tasks = state.tasks();
if (tasks.isEmpty()) {
return Result.ok(state.withStatus("No tasks to view!", YELLOW));
}
System.out.println();
System.out.println(color(" Select task to view:", DIM));
for (int i = 0; i < tasks.size(); i++) {
Task t = tasks.get(i);
String icon = switch (t.status()) {
case TODO -> color(BULLET, YELLOW);
case DOING -> color(ICON_FLAME, MAGENTA);
case DONE -> color(CHECK, GREEN);
};
System.out.println(
color(" [" + (i + 1) + "] ", BRIGHT_BLACK) + icon + " " + t.title()
);
}
String input = prompt("Task number");
if (input == null || input.isBlank()) {
return Result.ok(state.withStatus("Cancelled", YELLOW));
}
try {
int num = Integer.parseInt(input.trim());
if (num < 1 || num > tasks.size()) {
return Result.fail("Invalid task number");
}
Task task = tasks.get(num - 1);
// Display task details
System.out.println();
int w = state.width() - 4;
System.out.println(color(" " + repeat(BOX_HORIZONTAL, w), CYAN));
System.out.println();
// Title with status icon
String statusIcon = switch (task.status()) {
case TODO -> color(BULLET + " TODO", YELLOW, BOLD);
case DOING -> color(ICON_FLAME + " DOING", MAGENTA, BOLD);
case DONE -> color(CHECK + " DONE", GREEN, BOLD);
};
System.out.println(" " + color(task.title(), BRIGHT_WHITE, BOLD));
System.out.println(" " + statusIcon);
System.out.println();
// Description
if (task.description() != null && !task.description().isBlank()) {
System.out.println(color(" Description:", DIM));
// Word wrap description
String desc = task.description();
int maxWidth = w - 4;
while (desc.length() > maxWidth) {
int breakPoint = desc.lastIndexOf(' ', maxWidth);
if (breakPoint <= 0) breakPoint = maxWidth;
System.out.println(" " + desc.substring(0, breakPoint));
desc = desc.substring(breakPoint).trim();
}
if (!desc.isEmpty()) {
System.out.println(" " + desc);
}
} else {
System.out.println(color(" No description", DIM, ITALIC));
}
System.out.println();
System.out.println(color(" " + repeat(BOX_HORIZONTAL, w), CYAN));
System.out.println();
prompt("Press Enter to continue");
return Result.ok(state.withStatus("Viewed: " + task.title(), BLUE));
} catch (NumberFormatException e) {
return Result.fail("Invalid number");
}
}
private Result<State> handleStart(State state) {
List<Task> todo = state.byStatus(TaskStatus.TODO);
if (todo.isEmpty()) {
return Result.ok(
state.withStatus("No TODO tasks to start!", YELLOW)
);
}
System.out.println();
System.out.println(color(" TODO tasks:", DIM));
for (int i = 0; i < todo.size(); i++) {
System.out.println(
color(" [" + (i + 1) + "] ", BRIGHT_BLACK) +
todo.get(i).title()
);
}
String input = prompt("Task number to start");
if (input == null || input.isBlank()) {
return Result.ok(state.withStatus("Cancelled", YELLOW));
}
try {
int num = Integer.parseInt(input.trim());
if (num < 1 || num > todo.size()) {
return Result.fail("Invalid task number");
}
return Actions.start(state, todo.get(num - 1).id());
} catch (NumberFormatException e) {
return Result.fail("Invalid number");
}
}
private Result<State> handleComplete(State state) {
List<Task> doing = state.byStatus(TaskStatus.DOING);
List<Task> completable = !doing.isEmpty()
? doing
: state.byStatus(TaskStatus.TODO);
if (completable.isEmpty()) {
return Result.ok(state.withStatus("No tasks to complete!", YELLOW));
}
System.out.println();
String label = !doing.isEmpty() ? "DOING tasks:" : "TODO tasks:";
System.out.println(color(" " + label, DIM));
for (int i = 0; i < completable.size(); i++) {
System.out.println(
color(" [" + (i + 1) + "] ", BRIGHT_BLACK) +
completable.get(i).title()
);
}
String input = prompt("Task number to complete");
if (input == null || input.isBlank()) {
return Result.ok(state.withStatus("Cancelled", YELLOW));
}
try {
int num = Integer.parseInt(input.trim());
if (num < 1 || num > completable.size()) {
return Result.fail("Invalid task number");
}
return Actions.complete(state, completable.get(num - 1).id());
} catch (NumberFormatException e) {
return Result.fail("Invalid number");
}
}
private Result<State> handleDelete(State state) {
List<Task> tasks = state.tasks();
if (tasks.isEmpty()) {
return Result.ok(state.withStatus("No tasks to delete!", YELLOW));
}
System.out.println();
System.out.println(color(" All tasks:", DIM));
for (int i = 0; i < tasks.size(); i++) {
Task t = tasks.get(i);
String icon = t.completed()
? color(CHECK, GREEN)
: color(BULLET, YELLOW);
System.out.println(
color(" [" + (i + 1) + "] ", BRIGHT_BLACK) +
icon +
" " +
t.title()
);
}
String input = prompt("Task number to delete");
if (input == null || input.isBlank()) {
return Result.ok(state.withStatus("Cancelled", YELLOW));
}
try {
int num = Integer.parseInt(input.trim());
if (num < 1 || num > tasks.size()) {
return Result.fail("Invalid task number");
}
return Actions.delete(state, tasks.get(num - 1).id());
} catch (NumberFormatException e) {
return Result.fail("Invalid number");
}
}
// ═══════════════════════════════════════════════════════════════════
// MAIN
// ═══════════════════════════════════════════════════════════════════
public static void main(String[] args) {
TaskApp app = TaskApp.withInMemoryRepo();
// Sample data in different states
app.createTask(
"Learn HexaFun",
"Study the fluent DSL and port registry"
);
app.createTask("Write tests", "Ensure everything works correctly");
Result<Task> tui = app.createTask(
"Build a TUI",
"Create a terminal user interface"
);
tui.map(t -> app.startTask(t.id()));
Result<Task> docs = app.createTask(
"Read the docs",
"Check out the documentation"
);
docs.map(t ->
app.startTask(t.id()).map(started -> app.completeTask(started.id()))
);
new TasksTUI().run(app);
}
}