1 package com.guinetik.hexafun.examples.tasks.tui;
2
3 import static com.guinetik.hexafun.examples.tui.Ansi.*;
4
5 import com.guinetik.hexafun.examples.tasks.Task;
6 import com.guinetik.hexafun.examples.tasks.TaskApp;
7 import com.guinetik.hexafun.examples.tasks.TaskStatus;
8 import com.guinetik.hexafun.examples.tui.HexaTerminal;
9 import com.guinetik.hexafun.examples.tui.View;
10 import com.guinetik.hexafun.examples.tui.Widgets;
11 import com.guinetik.hexafun.fun.Result;
12 import java.io.BufferedReader;
13 import java.io.InputStreamReader;
14 import java.util.List;
15 import java.util.function.Function;
16 import java.util.function.UnaryOperator;
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 public class TasksTUI {
34
35
36
37
38
39
40
41
42 public record State(
43 TaskApp app,
44 int width,
45 String status,
46 String statusColor,
47 boolean running
48 ) {
49 private static final int MIN_WIDTH = 60;
50 private static final int KANBAN_MIN_WIDTH = 80;
51
52
53 public static State initial(TaskApp app) {
54 return new State(app, detectWidth(), "", GREEN, true);
55 }
56
57
58 public State withStatus(String msg, String color) {
59 return new State(app, width, msg, color, running);
60 }
61
62 public State withWidth(int w) {
63 return new State(
64 app,
65 Math.max(MIN_WIDTH, w),
66 status,
67 statusColor,
68 running
69 );
70 }
71
72 public State refreshWidth() {
73 return withWidth(detectWidth());
74 }
75
76 public State stop() {
77 return new State(app, width, "Goodbye!", CYAN, false);
78 }
79
80
81 public boolean isKanban() {
82 return width >= KANBAN_MIN_WIDTH;
83 }
84
85 public List<Task> tasks() {
86 return app.listTasks();
87 }
88
89 public List<Task> byStatus(TaskStatus s) {
90 return tasks()
91 .stream()
92 .filter(t -> t.status() == s)
93 .toList();
94 }
95
96
97 private static int detectWidth() {
98 return HexaTerminal.detectWidth(MIN_WIDTH, 120);
99 }
100 }
101
102
103
104
105
106
107
108
109
110 public static class Views {
111
112
113 public static View<State> screen() {
114 return clear()
115 .andThen(header())
116 .andThen(stats())
117 .andThen(View.when(State::isKanban, kanban(), tasks()))
118 .andThen(menu())
119 .andThen(status())
120 .andThen(prompt());
121 }
122
123
124
125
126
127 public static View<State> clear() {
128 return state -> CLEAR + CURSOR_HOME;
129 }
130
131 public static View<State> header() {
132 return state -> {
133 int w = state.width();
134 int headerWidth = w - 4;
135 String title = ICON_TASK + " TASK MANAGER";
136 String mode = state.isKanban() ? "KANBAN" : "LIST";
137 String hint = state.isKanban()
138 ? ""
139 : " (<" + State.KANBAN_MIN_WIDTH + " cols)";
140 String subtitle =
141 "HexaFun Demo [" + w + "w " + mode + hint + "]";
142
143 return lines(
144 "",
145 color(
146 " " +
147 DBOX_TOP_LEFT +
148 repeat(DBOX_HORIZONTAL, headerWidth) +
149 DBOX_TOP_RIGHT,
150 CYAN
151 ),
152 color(" " + DBOX_VERTICAL, CYAN) +
153 color(center(title, headerWidth), BOLD, BRIGHT_WHITE) +
154 color(DBOX_VERTICAL, CYAN),
155 color(" " + DBOX_VERTICAL, CYAN) +
156 color(center(subtitle, headerWidth), DIM) +
157 color(DBOX_VERTICAL, CYAN),
158 color(
159 " " +
160 DBOX_BOTTOM_LEFT +
161 repeat(DBOX_HORIZONTAL, headerWidth) +
162 DBOX_BOTTOM_RIGHT,
163 CYAN
164 ),
165 ""
166 );
167 };
168 }
169
170 public static View<State> stats() {
171 return state -> {
172 List<Task> tasks = state.tasks();
173 long total = tasks.size();
174 long todo = state.byStatus(TaskStatus.TODO).size();
175 long doing = state.byStatus(TaskStatus.DOING).size();
176 long done = state.byStatus(TaskStatus.DONE).size();
177 int percent = total > 0 ? (int) ((done * 100) / total) : 0;
178
179
180 int available = state.width() - 6;
181 int segW = available / 3;
182 int extra = available % 3;
183
184 String todoSeg = color(
185 center(BULLET + " " + todo + " TODO", segW + extra),
186 BG_YELLOW,
187 BLACK,
188 BOLD
189 );
190 String doingSeg = color(
191 center(ICON_FLAME + " " + doing + " DOING", segW),
192 BG_MAGENTA,
193 BRIGHT_WHITE,
194 BOLD
195 );
196 String doneSeg = color(
197 center(CHECK + " " + done + " DONE", segW),
198 BG_GREEN,
199 BLACK,
200 BOLD
201 );
202
203
204 int barW = state.width() - 8;
205 int filled = (barW * percent) / 100;
206 int empty = barW - filled;
207
208 return lines(
209 " " +
210 todoSeg +
211 color(PL_LEFT, YELLOW, BG_MAGENTA) +
212 doingSeg +
213 color(PL_LEFT, MAGENTA, BG_GREEN) +
214 doneSeg +
215 color(PL_LEFT, GREEN),
216 "",
217 " " +
218 color(repeat(BLOCK_FULL, filled), GREEN) +
219 color(repeat(BLOCK_LIGHT, empty), BRIGHT_BLACK) +
220 color(String.format(" %3d%%", percent), DIM),
221 ""
222 );
223 };
224 }
225
226 public static View<State> tasks() {
227 return state -> {
228 int w = state.width();
229 List<Task> tasks = state.tasks();
230 int lineW = w - 4 - 9;
231
232 StringBuilder sb = new StringBuilder();
233 sb
234 .append(
235 color(
236 " " +
237 BOX_HORIZONTAL +
238 " TASKS " +
239 repeat(BOX_HORIZONTAL, lineW),
240 DIM
241 )
242 )
243 .append("\n\n");
244
245 if (tasks.isEmpty()) {
246 sb
247 .append(
248 color(
249 " " +
250 center(
251 "No tasks yet. Press [a] to add one!",
252 w - 4
253 ),
254 DIM,
255 ITALIC
256 )
257 )
258 .append("\n\n");
259 } else {
260 int idx = 1;
261 for (Task task : tasks) {
262 sb.append(taskCard(task, idx++, w));
263 }
264 }
265 return sb.toString();
266 };
267 }
268
269 private static String taskCard(Task task, int index, int width) {
270 String icon = switch (task.status()) {
271 case TODO -> BULLET;
272 case DOING -> ICON_FLAME;
273 case DONE -> CHECK;
274 };
275 String iconColor = switch (task.status()) {
276 case TODO -> YELLOW;
277 case DOING -> MAGENTA;
278 case DONE -> GREEN;
279 };
280 String titleColor = task.completed() ? DIM : BRIGHT_WHITE;
281 String suffix = switch (task.status()) {
282 case DONE -> color(" (done)", DIM, GREEN);
283 case DOING -> color(" (wip)", DIM, MAGENTA);
284 default -> "";
285 };
286
287 int suffixLen = task.status() == TaskStatus.DONE
288 ? 8
289 : (task.status() == TaskStatus.DOING ? 7 : 0);
290 int available = width - 8 - suffixLen - 2;
291 String title = task.title();
292 if (title.length() > available) {
293 title = title.substring(0, Math.max(0, available - 3)) + "...";
294 }
295
296 StringBuilder sb = new StringBuilder();
297 sb
298 .append(" ")
299 .append(color(index + " ", BRIGHT_BLACK))
300 .append(color(icon + " ", iconColor))
301 .append(color(title, titleColor))
302 .append(suffix)
303 .append("\n");
304
305 if (task.description() != null && !task.description().isBlank()) {
306 int descW = width - 10;
307 String desc = task.description();
308 if (desc.length() > descW) desc =
309 desc.substring(0, Math.max(0, descW - 3)) + "...";
310 sb.append(color(" " + desc, DIM)).append("\n");
311 }
312 sb.append("\n");
313 return sb.toString();
314 }
315
316 public static View<State> kanban() {
317 return state -> {
318 List<Task> todo = state.byStatus(TaskStatus.TODO);
319 List<Task> doing = state.byStatus(TaskStatus.DOING);
320 List<Task> done = state.byStatus(TaskStatus.DONE);
321
322 int totalW = state.width() - 4;
323 int innerW = totalW - 2;
324 int colW = innerW / 3;
325 int extra = innerW % 3;
326 int c1 = colW + extra,
327 c2 = colW,
328 c3 = colW;
329
330 StringBuilder sb = new StringBuilder();
331
332
333 sb
334 .append(" ")
335 .append(
336 color(
337 BOX_TOP_LEFT +
338 repeat(BOX_HORIZONTAL, c1 - 2) +
339 BOX_TOP_RIGHT,
340 YELLOW
341 )
342 )
343 .append(" ")
344 .append(
345 color(
346 BOX_TOP_LEFT +
347 repeat(BOX_HORIZONTAL, c2 - 2) +
348 BOX_TOP_RIGHT,
349 MAGENTA
350 )
351 )
352 .append(" ")
353 .append(
354 color(
355 BOX_TOP_LEFT +
356 repeat(BOX_HORIZONTAL, c3 - 2) +
357 BOX_TOP_RIGHT,
358 GREEN
359 )
360 )
361 .append("\n");
362
363
364 sb
365 .append(" ")
366 .append(color(BOX_VERTICAL, YELLOW))
367 .append(
368 color(
369 center(
370 BULLET + " TODO (" + todo.size() + ")",
371 c1 - 2
372 ),
373 BOLD,
374 YELLOW
375 )
376 )
377 .append(color(BOX_VERTICAL, YELLOW))
378 .append(" ")
379 .append(color(BOX_VERTICAL, MAGENTA))
380 .append(
381 color(
382 center(
383 ICON_FLAME + " DOING (" + doing.size() + ")",
384 c2 - 2
385 ),
386 BOLD,
387 MAGENTA
388 )
389 )
390 .append(color(BOX_VERTICAL, MAGENTA))
391 .append(" ")
392 .append(color(BOX_VERTICAL, GREEN))
393 .append(
394 color(
395 center(
396 CHECK + " DONE (" + done.size() + ")",
397 c3 - 2
398 ),
399 BOLD,
400 GREEN
401 )
402 )
403 .append(color(BOX_VERTICAL, GREEN))
404 .append("\n");
405
406
407 sb
408 .append(" ")
409 .append(
410 color(
411 BOX_T_RIGHT +
412 repeat(BOX_HORIZONTAL, c1 - 2) +
413 BOX_T_LEFT,
414 YELLOW
415 )
416 )
417 .append(" ")
418 .append(
419 color(
420 BOX_T_RIGHT +
421 repeat(BOX_HORIZONTAL, c2 - 2) +
422 BOX_T_LEFT,
423 MAGENTA
424 )
425 )
426 .append(" ")
427 .append(
428 color(
429 BOX_T_RIGHT +
430 repeat(BOX_HORIZONTAL, c3 - 2) +
431 BOX_T_LEFT,
432 GREEN
433 )
434 )
435 .append("\n");
436
437
438 int maxRows = Math.max(
439 todo.size(),
440 Math.max(doing.size(), done.size())
441 );
442 if (maxRows == 0) maxRows = 1;
443
444 int todoIdx = 1,
445 doingIdx = todo.size() + 1,
446 doneIdx = todo.size() + doing.size() + 1;
447
448 for (int i = 0; i < maxRows; i++) {
449 sb.append(" ").append(color(BOX_VERTICAL, YELLOW));
450 sb.append(
451 i < todo.size()
452 ? color(
453 kanbanCard(todo.get(i), todoIdx++, c1 - 2),
454 BRIGHT_WHITE
455 )
456 : repeat(" ", c1 - 2)
457 );
458 sb.append(color(BOX_VERTICAL, YELLOW)).append(" ");
459
460 sb.append(color(BOX_VERTICAL, MAGENTA));
461 sb.append(
462 i < doing.size()
463 ? color(
464 kanbanCard(doing.get(i), doingIdx++, c2 - 2),
465 BRIGHT_MAGENTA
466 )
467 : repeat(" ", c2 - 2)
468 );
469 sb.append(color(BOX_VERTICAL, MAGENTA)).append(" ");
470
471 sb.append(color(BOX_VERTICAL, GREEN));
472 sb.append(
473 i < done.size()
474 ? color(
475 kanbanCard(done.get(i), doneIdx++, c3 - 2),
476 DIM
477 )
478 : repeat(" ", c3 - 2)
479 );
480 sb.append(color(BOX_VERTICAL, GREEN)).append("\n");
481 }
482
483
484 sb
485 .append(" ")
486 .append(
487 color(
488 BOX_BOTTOM_LEFT +
489 repeat(BOX_HORIZONTAL, c1 - 2) +
490 BOX_BOTTOM_RIGHT,
491 YELLOW
492 )
493 )
494 .append(" ")
495 .append(
496 color(
497 BOX_BOTTOM_LEFT +
498 repeat(BOX_HORIZONTAL, c2 - 2) +
499 BOX_BOTTOM_RIGHT,
500 MAGENTA
501 )
502 )
503 .append(" ")
504 .append(
505 color(
506 BOX_BOTTOM_LEFT +
507 repeat(BOX_HORIZONTAL, c3 - 2) +
508 BOX_BOTTOM_RIGHT,
509 GREEN
510 )
511 )
512 .append("\n\n");
513
514 return sb.toString();
515 };
516 }
517
518 private static String kanbanCard(Task task, int index, int width) {
519 String title = task.title();
520 int maxLen = width - 4;
521 if (title.length() > maxLen) title =
522 title.substring(0, Math.max(0, maxLen - 2)) + "..";
523 return pad(" " + index + " " + title, width);
524 }
525
526 public static View<State> menu() {
527 return state ->
528 lines(
529 color(
530 " " + repeat(BOX_HORIZONTAL, state.width() - 4),
531 DIM
532 ),
533 "",
534 " " +
535 color("[a]", CYAN, BOLD) +
536 color(" " + ICON_ADD + " Add ", CYAN) +
537 color("[v]", BLUE, BOLD) +
538 color(" " + ICON_EYE + " View ", BLUE) +
539 color("[s]", MAGENTA, BOLD) +
540 color(" " + ICON_FLAME + " Start ", MAGENTA) +
541 color("[c]", GREEN, BOLD) +
542 color(" " + CHECK + " Done ", GREEN) +
543 color("[d]", RED, BOLD) +
544 color(" " + ICON_TRASH + " Del ", RED) +
545 color("[q]", BRIGHT_BLACK, BOLD) +
546 color(" " + CROSS + " Quit", BRIGHT_BLACK),
547 ""
548 );
549 }
550
551 public static View<State> status() {
552 return state ->
553 state.status().isEmpty()
554 ? "\n"
555 : color(
556 " " + ARROW_RIGHT + " " + state.status(),
557 state.statusColor()
558 ) +
559 "\n\n";
560 }
561
562 public static View<State> prompt() {
563 return state -> color(" > ", CYAN, BOLD);
564 }
565
566 }
567
568
569
570
571
572
573
574
575
576 @FunctionalInterface
577 public interface Action extends Function<State, Result<State>> {
578
579 default Action andThen(Action next) {
580 return state -> this.apply(state).flatMap(next);
581 }
582
583
584 static Action of(UnaryOperator<State> transform) {
585 return state -> Result.ok(transform.apply(state));
586 }
587
588
589 static Action fromResult(Function<State, Result<State>> f) {
590 return f::apply;
591 }
592 }
593
594
595
596
597 public static class Actions {
598
599 public static Result<State> create(
600 State state,
601 String title,
602 String desc
603 ) {
604 return state
605 .app()
606 .createTask(title, desc)
607 .map(task ->
608 state.withStatus("Created: " + task.title(), GREEN)
609 );
610 }
611
612 public static Result<State> start(State state, String taskId) {
613 return state
614 .app()
615 .startTask(taskId)
616 .map(task ->
617 state.withStatus("Started: " + task.title(), MAGENTA)
618 );
619 }
620
621 public static Result<State> complete(State state, String taskId) {
622 return state
623 .app()
624 .completeTask(taskId)
625 .map(task ->
626 state.withStatus("Completed: " + task.title(), GREEN)
627 );
628 }
629
630 public static Result<State> delete(State state, String taskId) {
631 return state
632 .app()
633 .deleteTask(taskId)
634 .map(ok -> state.withStatus("Deleted task", RED));
635 }
636
637
638 public static Result<State> quickAction(State state, int index) {
639 List<Task> tasks = state.tasks();
640 if (index < 1 || index > tasks.size()) {
641 return Result.fail("Invalid task number: " + index);
642 }
643
644 Task task = tasks.get(index - 1);
645 return switch (task.status()) {
646 case TODO -> start(state, task.id());
647 case DOING -> complete(state, task.id());
648 case DONE -> Result.ok(
649 state.withStatus(
650 "Already completed: " + task.title(),
651 YELLOW
652 )
653 );
654 };
655 }
656 }
657
658
659
660
661
662 private final BufferedReader reader;
663 private final View<State> screen;
664
665 public TasksTUI() {
666 this.reader = new BufferedReader(new InputStreamReader(System.in));
667 this.screen = Views.screen();
668 }
669
670
671 private void render(State state) {
672 System.out.print(screen.apply(state));
673 System.out.flush();
674 }
675
676
677 private String readLine() {
678 try {
679 return reader.readLine();
680 } catch (Exception e) {
681 return null;
682 }
683 }
684
685
686 private String prompt(String message) {
687 System.out.print(color(" " + message + ": ", CYAN));
688 System.out.flush();
689 return readLine();
690 }
691
692
693 public void run(TaskApp app) {
694 State state = State.initial(app);
695
696
697 System.out.print(ALT_SCREEN_ON);
698 System.out.flush();
699
700
701 Runtime.getRuntime().addShutdownHook(new Thread(this::restoreTerminal));
702
703 try {
704 while (state.running()) {
705 state = state.refreshWidth();
706 render(state);
707
708 String input = readLine();
709 state = processInput(state, input);
710 }
711
712 render(state);
713 } finally {
714 restoreTerminal();
715 }
716 }
717
718
719 private void restoreTerminal() {
720 System.out.print(ALT_SCREEN_OFF);
721 System.out.flush();
722 }
723
724
725 private State processInput(State state, String input) {
726 if (
727 input == null ||
728 input.equalsIgnoreCase("q") ||
729 input.equalsIgnoreCase("quit")
730 ) {
731 return state.stop();
732 }
733
734 String cmd = input.toLowerCase().trim();
735
736
737 Result<State> result = switch (cmd) {
738 case "a", "add" -> handleAdd(state);
739 case "v", "view" -> handleView(state);
740 case "s", "start" -> handleStart(state);
741 case "c", "complete" -> handleComplete(state);
742 case "d", "delete" -> handleDelete(state);
743 case "" -> Result.ok(state);
744 default -> {
745 if (cmd.matches("\\d+")) {
746 yield Actions.quickAction(state, Integer.parseInt(cmd));
747 }
748 yield Result.ok(
749 state.withStatus("Unknown command: " + input, RED)
750 );
751 }
752 };
753
754
755 return result.fold(
756 error -> state.withStatus("Error: " + error, RED),
757 newState -> newState
758 );
759 }
760
761
762
763
764
765 private Result<State> handleAdd(State state) {
766 System.out.println();
767 String title = prompt("Title");
768 if (title == null || title.isBlank()) {
769 return Result.ok(
770 state.withStatus("Cancelled - title required", YELLOW)
771 );
772 }
773 String desc = prompt("Description (optional)");
774 return Actions.create(state, title, desc != null ? desc : "");
775 }
776
777 private Result<State> handleView(State state) {
778 List<Task> tasks = state.tasks();
779 if (tasks.isEmpty()) {
780 return Result.ok(state.withStatus("No tasks to view!", YELLOW));
781 }
782
783 System.out.println();
784 System.out.println(color(" Select task to view:", DIM));
785 for (int i = 0; i < tasks.size(); i++) {
786 Task t = tasks.get(i);
787 String icon = switch (t.status()) {
788 case TODO -> color(BULLET, YELLOW);
789 case DOING -> color(ICON_FLAME, MAGENTA);
790 case DONE -> color(CHECK, GREEN);
791 };
792 System.out.println(
793 color(" [" + (i + 1) + "] ", BRIGHT_BLACK) + icon + " " + t.title()
794 );
795 }
796
797 String input = prompt("Task number");
798 if (input == null || input.isBlank()) {
799 return Result.ok(state.withStatus("Cancelled", YELLOW));
800 }
801
802 try {
803 int num = Integer.parseInt(input.trim());
804 if (num < 1 || num > tasks.size()) {
805 return Result.fail("Invalid task number");
806 }
807 Task task = tasks.get(num - 1);
808
809
810 System.out.println();
811 int w = state.width() - 4;
812 System.out.println(color(" " + repeat(BOX_HORIZONTAL, w), CYAN));
813 System.out.println();
814
815
816 String statusIcon = switch (task.status()) {
817 case TODO -> color(BULLET + " TODO", YELLOW, BOLD);
818 case DOING -> color(ICON_FLAME + " DOING", MAGENTA, BOLD);
819 case DONE -> color(CHECK + " DONE", GREEN, BOLD);
820 };
821 System.out.println(" " + color(task.title(), BRIGHT_WHITE, BOLD));
822 System.out.println(" " + statusIcon);
823 System.out.println();
824
825
826 if (task.description() != null && !task.description().isBlank()) {
827 System.out.println(color(" Description:", DIM));
828
829 String desc = task.description();
830 int maxWidth = w - 4;
831 while (desc.length() > maxWidth) {
832 int breakPoint = desc.lastIndexOf(' ', maxWidth);
833 if (breakPoint <= 0) breakPoint = maxWidth;
834 System.out.println(" " + desc.substring(0, breakPoint));
835 desc = desc.substring(breakPoint).trim();
836 }
837 if (!desc.isEmpty()) {
838 System.out.println(" " + desc);
839 }
840 } else {
841 System.out.println(color(" No description", DIM, ITALIC));
842 }
843
844 System.out.println();
845 System.out.println(color(" " + repeat(BOX_HORIZONTAL, w), CYAN));
846 System.out.println();
847 prompt("Press Enter to continue");
848
849 return Result.ok(state.withStatus("Viewed: " + task.title(), BLUE));
850 } catch (NumberFormatException e) {
851 return Result.fail("Invalid number");
852 }
853 }
854
855 private Result<State> handleStart(State state) {
856 List<Task> todo = state.byStatus(TaskStatus.TODO);
857 if (todo.isEmpty()) {
858 return Result.ok(
859 state.withStatus("No TODO tasks to start!", YELLOW)
860 );
861 }
862
863 System.out.println();
864 System.out.println(color(" TODO tasks:", DIM));
865 for (int i = 0; i < todo.size(); i++) {
866 System.out.println(
867 color(" [" + (i + 1) + "] ", BRIGHT_BLACK) +
868 todo.get(i).title()
869 );
870 }
871
872 String input = prompt("Task number to start");
873 if (input == null || input.isBlank()) {
874 return Result.ok(state.withStatus("Cancelled", YELLOW));
875 }
876
877 try {
878 int num = Integer.parseInt(input.trim());
879 if (num < 1 || num > todo.size()) {
880 return Result.fail("Invalid task number");
881 }
882 return Actions.start(state, todo.get(num - 1).id());
883 } catch (NumberFormatException e) {
884 return Result.fail("Invalid number");
885 }
886 }
887
888 private Result<State> handleComplete(State state) {
889 List<Task> doing = state.byStatus(TaskStatus.DOING);
890 List<Task> completable = !doing.isEmpty()
891 ? doing
892 : state.byStatus(TaskStatus.TODO);
893
894 if (completable.isEmpty()) {
895 return Result.ok(state.withStatus("No tasks to complete!", YELLOW));
896 }
897
898 System.out.println();
899 String label = !doing.isEmpty() ? "DOING tasks:" : "TODO tasks:";
900 System.out.println(color(" " + label, DIM));
901 for (int i = 0; i < completable.size(); i++) {
902 System.out.println(
903 color(" [" + (i + 1) + "] ", BRIGHT_BLACK) +
904 completable.get(i).title()
905 );
906 }
907
908 String input = prompt("Task number to complete");
909 if (input == null || input.isBlank()) {
910 return Result.ok(state.withStatus("Cancelled", YELLOW));
911 }
912
913 try {
914 int num = Integer.parseInt(input.trim());
915 if (num < 1 || num > completable.size()) {
916 return Result.fail("Invalid task number");
917 }
918 return Actions.complete(state, completable.get(num - 1).id());
919 } catch (NumberFormatException e) {
920 return Result.fail("Invalid number");
921 }
922 }
923
924 private Result<State> handleDelete(State state) {
925 List<Task> tasks = state.tasks();
926 if (tasks.isEmpty()) {
927 return Result.ok(state.withStatus("No tasks to delete!", YELLOW));
928 }
929
930 System.out.println();
931 System.out.println(color(" All tasks:", DIM));
932 for (int i = 0; i < tasks.size(); i++) {
933 Task t = tasks.get(i);
934 String icon = t.completed()
935 ? color(CHECK, GREEN)
936 : color(BULLET, YELLOW);
937 System.out.println(
938 color(" [" + (i + 1) + "] ", BRIGHT_BLACK) +
939 icon +
940 " " +
941 t.title()
942 );
943 }
944
945 String input = prompt("Task number to delete");
946 if (input == null || input.isBlank()) {
947 return Result.ok(state.withStatus("Cancelled", YELLOW));
948 }
949
950 try {
951 int num = Integer.parseInt(input.trim());
952 if (num < 1 || num > tasks.size()) {
953 return Result.fail("Invalid task number");
954 }
955 return Actions.delete(state, tasks.get(num - 1).id());
956 } catch (NumberFormatException e) {
957 return Result.fail("Invalid number");
958 }
959 }
960
961
962
963
964
965 public static void main(String[] args) {
966 TaskApp app = TaskApp.withInMemoryRepo();
967
968
969 app.createTask(
970 "Learn HexaFun",
971 "Study the fluent DSL and port registry"
972 );
973 app.createTask("Write tests", "Ensure everything works correctly");
974
975 Result<Task> tui = app.createTask(
976 "Build a TUI",
977 "Create a terminal user interface"
978 );
979 tui.map(t -> app.startTask(t.id()));
980
981 Result<Task> docs = app.createTask(
982 "Read the docs",
983 "Check out the documentation"
984 );
985 docs.map(t ->
986 app.startTask(t.id()).map(started -> app.completeTask(started.id()))
987 );
988
989 new TasksTUI().run(app);
990 }
991 }