-
Notifications
You must be signed in to change notification settings - Fork 22
Expand file tree
/
Copy pathREADME.md
More file actions
1259 lines (983 loc) · 41.7 KB
/
Copy pathREADME.md
File metadata and controls
1259 lines (983 loc) · 41.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000



<br>
<a href="https://discord.gg/5NZ2GKV5Cs">
<img alt="Varabyte Discord" src="https://img.shields.io/discord/886036660767305799.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2" />
</a>
[](https://twitter.com/intent/follow?screen_name=bitspittle)
# Kotter 🦦
```kotlin
session {
var wantsToLearn by liveVarOf(false)
section {
text("Would you like to learn "); cyan { text("Kotter") }; textLine("? (Y/n)")
text("> "); input(Completions("yes", "no"))
if (wantsToLearn) {
yellow(isBright = true) { p { textLine("""\(^o^)/""") } }
}
}.runUntilInputEntered {
onInputEntered { wantsToLearn = "yes".startsWith(input.lowercase()) }
}
}
```

*See also: [the game of life](examples/life), [snake](examples/snake), [sliding tiles](examples/sliding), [doom fire](examples/doomfire), and [Wordle](examples/wordle) implemented in Kotter!*
---
Kotter (a **KOT**lin **TER**minal library) aims to be a relatively thin, declarative, Kotlin-idiomatic API that provides
useful functionality for writing delightful console applications. It strives to keep things simple, providing a solution
a bit more opinionated than making raw `println` calls but way less featured than something like _Java Curses_.
Specifically, this library helps with:
* Setting colors and text decorations (e.g. underline, bold)
* Handling user input
* Creating timers and animations
* Seamlessly repainting terminal text when values change
## 🐘 Gradle
### 🎯 Dependency
```groovy
// build.gradle (groovy)
repositories {
mavenCentral()
}
dependencies {
implementation 'com.varabyte.kotter:kotter:1.0.0-rc1'
}
```
```kotlin
// build.gradle.kts (kotlin script)
repositories {
mavenCentral()
}
dependencies {
implementation("com.varabyte.kotter:kotter:1.0.0-rc1")
}
```
### 🚥 Running examples
If you've cloned this repository, examples are located under the [examples](examples) folder. To try one of them, you
can navigate into it on the command line and run it via Gradle.
```bash
$ cd examples/life
$ ../../gradlew run
```
However, because Gradle itself has taken over the terminal to do its own fancy command line magic, the example will
actually open up and run inside a virtual terminal.
If you want to run the program directly inside your system terminal, which is hopefully the way most users will see your
application, you should use the `installDist` task to accomplish this:
```bash
$ cd examples/life
$ ../../gradlew installDist
$ cd build/install/life/bin
$ ./life
```
***Note:** If your terminal does not support features needed by Kotter, then this still may end up running inside a
virtual terminal.*
## 📖 Usage
### 👶 Basics
The following is equivalent to `println("Hello, World")`. In this simple case, it's definitely overkill!
```kotlin
session {
section { textLine("Hello, World") }.run()
}
```
`section { ... }` defines a `Section` which, on its own, is inert. It needs to be run to output text to the
console. Above, we use the `run` method to trigger this. The method blocks until the render (i.e. text printing to the
console) is finished (which, in the above case, will be almost instant).
`session { ... }` sets the outer scope for your whole program. While we're just calling it with default arguments here,
you can also pass in parameters that apply to the entire application.
While the above simple case is a bit verbose for what it's doing, Kotter starts to show its strength when doing
background work (or other async tasks like waiting for user input) during which time the section block may render
several times. We'll see many examples throughout this document later.
A Kotter `session` can contain one or more `section`s. Your own app may only ever contain a single `section` and that's
fine! But if you have multiple `section`s, it will feel to the user like your app has a current, active area, following
a history of text paragraphs from previous interactions that no longer change.
### 🎨 Text Effects
You can call color methods directly, which remain in effect until the next color method is called:
```kotlin
section {
green(layer = BG)
red() // defaults to FG layer if no layer specified
textLine("Red on green")
blue()
textLine("Blue on green")
}.run()
```

If you only want the color effect to live for a limited time, you can use scoped helper versions that handle
clearing colors for you automatically at the end of their block:
```kotlin
section {
green(layer = BG) {
red {
textLine("Red on green")
}
textLine("Default on green")
blue {
textLine("Blue on green")
}
}
}.run()
```

If the user's terminal supports truecolor mode, you can specify rgb (or hsv) values directly:
```kotlin
section {
rgb(0xFFFF00) { textLine("Yellow!") }
hsv(35, 1.0f, 1.0f) { textLine("Orange!") }
}.run()
```

***Note:** If truecolor is not supported, terminals may attempt to emulate it by falling back to a nearby color, which
may look decent! However, to be safe, you may want to avoid subtle gradient tricks, as they may come out clumped for
some users.*
Various text effects (like bold) are also available:
```kotlin
section {
bold {
textLine("Title")
}
p {
textLine("A paragraph is content auto-surrounded by newlines")
}
p {
text("This paragraph has an ")
underline { text("underlined") }
textLine(" word in it")
}
}.run()
```

***Note:** Italics functionality is not currently exposed, as it is not a standard feature and is inconsistently
supported across terminals.*
### 🪆 State and scopedState
To reduce the chance of introducing unexpected bugs later, state changes (like colors) will be localized to the current
`section` block only:
```kotlin
section {
blue(BG)
red()
text("This text is red on blue")
}.run()
section {
text("This text is rendered using default colors")
}.run()
```

Within a section, you can also use the `scopedState` method. This creates a new scope within which any state will be
automatically discarded after it ends.
```kotlin
section {
scopedState {
red()
blue(BG)
underline()
textLine("Underlined red on blue")
}
text("Text without color or decorations")
}.run()
```

***Note:** This is what the scoped text effect methods (like `red { ... }`) are doing for you under the hood, actually.*
### 🎬 Rerendering sections
The `section` block is designed to be run one _or more_ times. That is, you can write logic inside it which may not get
executed on the first run but will be on a followup run.
Here, we pass in a callback to the `run` method which updates a value referenced by the `section` block (the `result`
integer). This example will run the section twice - once when `run` is first called and again when it calls
`rerender`:
```kotlin
var result: Int? = null
section {
text("Calculating... ")
if (result != null) {
text("Done! Result = $result")
}
}.run {
result = doNetworkFetchAndExpensiveCalculation()
rerender()
}
```

The `run` callback runs as a suspend function, so you can call other suspend methods from within it.
Unlike using `run` without a callback (i.e. simply `run()`), here your program will be blocked until the callback has
finished (or, if it has triggered a rerender, until the last rerender finishes after your callback is done).
#### LiveVar
In our example above, the `run` callback calls a `rerender` method, which you can call to request another render pass.
However, remembering to call `rerender` yourself is potentially fragile and could be a source of bugs in the future when
trying to figure out why your console isn't updating.
For this purpose, Kotter provides the `LiveVar` class, which, when modified, will automatically request a rerender.
An example will demonstrate this in action shortly.
To create a `LiveVar`, simply change a normal variable declaration line like:
```kotlin
session {
var result: Int? = null
/* ... */
}
```
to:
```kotlin
session {
var result by liveVarOf<Int?>(null)
/* ... */
}
```
***Note:** The `liveVarOf` method is actually scoped to the `session` block. For many remaining examples, we'll elide
the `session` boilerplate, but that doesn't mean you can omit it in your own program!*
Let's apply `liveVarOf` to our earlier example in order to remove the `rerender` call:
```kotlin
var result by liveVarOf<Int?>(null)
section {
/* ... no changes ... */
}.run {
result = doNetworkFetchAndExpensiveCalculation()
}
```
And done! Fewer lines and less error pone.
Here's another example, showing how you can use `run` and a `LiveVar` to render a progress bar:
```kotlin
// Prints something like: [****------]
val BAR_LENGTH = 10
var numFilledSegments by liveVarOf(0)
section {
text("[")
for (i in 0 until BAR_LENGTH) {
text(if (i < numFilledSegments) "*" else "-")
}
text("]")
}.run {
var percent = 0
while (percent < 100) {
delay(Random.nextLong(10, 100))
percent += Random.nextInt(1,5)
numFilledSegments = ((percent / 100f) * BAR_LENGTH).roundToInt()
}
}
```

#### LiveList
Similar to `LiveVar`, a `LiveList` is a reactive primitive which, when modified by having elements added to or
removed from it, causes a rerender to happen automatically. You don't need to use the `by` keyword with `LiveList`.
Instead, within a `session`, use the `liveListOf` method:
```kotlin
val fileWalker = FileWalker(".") // This class doesn't exist but just pretend for this example...
val fileMatches = liveListOf<String>()
section {
textLine("Matches found so far:")
if (fileMatches.isNotEmpty()) {
for (match in fileMatches) {
textLine(" - $match")
}
}
else {
textLine("No matches so far...")
}
}.run {
fileWalker.findFiles("*.txt") { file ->
fileMatches += file.name
}
}
```

The `LiveList` class is thread safe, but you can still run into trouble if you access multiple values on the list one
after the other, as a lock is released between each check. It's always possible that modifying the first property will
kick off a new render which will start before the additional values are set, in other words.
To handle this, you can use the `LiveList#withWriteLock` method:
```kotlin
val fileWalker = FileWalker(".")
val last10Matches = liveListOf<String>()
section {
...
}.run {
fileWalker.findFiles("*.txt") { file ->
last10Matches.withWriteLock {
add(file.name)
if (size > 10) { removeAt(0) }
}
}
}
```
The general rule of thumb is: use `withWriteLock` if you want to access or modify more than one property from the list
at the same time within your `run` block.
Note that you don't have to worry about locking within a `section { ... }` block. Data access is already locked for you
in that context.
#### Other Collections
In addition to `LiveList`, Kotter also provides `LiveMap` and `LiveSet`. There's no need to extensively document these
classes here as much of the earlier `LiveList` section applies to them as well.
You can create these classes using `liveMapOf(...)` and `liveSetOf(...)`, respectfully.
#### Signals and waiting
A common pattern is for the `run` block to wait for some sort of signal before finishing, e.g. in response to some
event. You could always use a general threading trick for this, such as a `CountDownLatch` or a
`CompletableDeffered<Unit>` to stop the block from finishing until you're ready:
```kotlin
val fileDownloader = FileDownloader("...")
section {
/* ... */
}.run {
val finished = CompletableDeffered<Unit>()
fileDownloader.onFinished += { finished.complete(Unit) }
fileDownloader.start()
finished.await()
}
```
but, for convenience, Kotter provides the `signal` and `waitForSignal` methods, which do this for you.
```kotlin
val fileDownloader = FileDownloader("...")
section {
/* ... */
}.run {
fileDownloader.onFinished += { signal() }
fileDownloader.start()
waitForSignal()
}
```
These methods are enough in most cases. Note that if you call `signal` before you reach `waitForSignal`, then
`waitForSignal` will just pass through without stopping.
There's also a convenience `runUntilSignal` method you can use, within which you don't need to call `waitForSignal`
yourself, since this case is so common:
```kotlin
val fileDownloader = FileDownloader("...")
section {
/* ... */
}.runUntilSignal {
fileDownloader.onFinished += { signal() }
fileDownloader.start()
}
```
### ⌨️ User input
#### Typed input
Kotter consumes keypresses, so as the user types into the console, nothing will show up unless you intentionally print
it. You can easily do this using the `input` method, which handles listening to kepresses and adding text into your
section at that location:
```kotlin
section {
// `input` is a method that appends the user's input typed so far in this
// Once your section references it, the block is automatically rerendered when its value changes.
text("Please enter your name: "); input()
}.run { /* ... */ }
```

The input method automatically adds a cursor for you. It also handles keys like LEFT/RIGHT and HOME/END, moving the
cursor back and forth between the bounds of the input string.
You can intercept input as it is typed using the `onInputChanged` event:
```kotlin
section {
text("Please enter your name: "); input()
}.run {
onInputChanged {
input = input.toUpperCase()
}
/* ... */
}
```
You can also use the `rejectInput` method to return your input to the previous (presumably valid) state.
```kotlin
section {
text("Please enter your name: "); input()
}.run {
onInputChanged {
if (input.any { !it.isLetter() }) { rejectInput() }
// Would also work: input = input.filter { it.isLetter() }
}
/* ... */
}
```
To handle when the user presses the _ENTER_ key, use the `onInputEntered` callback. You can use it in conjunction with
the `onInputChanged` callback we just discussed:
```kotlin
var name = ""
section {
text("Please enter your name: "); input()
}.runUntilSignal {
onInputChanged { input = input.filter { it.isLetter() } }
onInputEntered { name = input; signal() }
}
```
Above, we've indicated that we want to close the section when the user presses _ENTER_. Since this is actually a fairly
common case, Kotter provides `runUntilInputEntered` for your convenience. Using it, we can simplify the above example a
bit, typing fewer characters for identical behavior and expressing clearer intention:
```kotlin
var name = ""
section {
text("Please enter your name: "); input()
}.runUntilInputEntered {
onInputChanged { input = input.filter { it.isLetter() } }
onInputEntered { name = input }
}
```
#### Input Completions
You can pass in an `InputCompleter` implementation to `input` that can generate suggestions based on the current input.
The user can press RIGHT at any time to autocomplete any suggested shown to them.
Here's the interface (with some parts elided for simplicity):
```kotlin
interface InputCompleter {
fun complete(input: String): String?
}
input(object : InputCompleter {
override fun complete(input: String): String? { ... }
})
```
Perhaps you have a database of names in your program? You can use it to provide suggestions. If your implementation
returns null, that means no suggestion was found:
```kotlin
object : InputCompleter {
override fun complete(input: String): String? {
return names
.firstOrNull { it.startsWith(input) }
?.let { it.drop(input.length) }
// ^ Don't return the whole word; just the part that comes after the user's input so far.
}
}
```
Kotter provides a very useful implementation out of the box, called `Completions`, which lets you specify a list of
values that will be autocompleted as long as the user's input matches one of them.
```
section {
text("Continue? "); input(Completions("yes", "no"))
}.run()
```
Order matters! If nothing is typed, the first completion will be suggested. If multiple values match, the one earliest
in the list will be suggested.
#### Keypresses
If you're interested in specific keypresses and not simply input that's been typed in, you can register a listener to
the `onKeyPressed` callback:
```kotlin
section {
textLine("Press Q to quit")
/* ... */
}.run {
var quit = false
onKeyPressed {
when(key) {
Keys.Q -> quit = true
}
}
while (!quit) {
delay(16)
/* ... */
}
}
```
For convenience, there's also a `runUntilKeyPressed` method you can use to help with patterns like the above. It can be
nice, for example, to let the user press _Q_ to quit your application:
```kotlin
section {
textLine("Press Q to quit")
/* ... */
}.runUntilKeyPressed(Keys.Q) {
while (true) {
delay(16)
/* ... */
}
}
```
### ⏳ Timers
Kotter can manage a set of timers for you. Use the `addTimer` method in your `run` block to add some:
```kotlin
section {
/* ... */
}.runUntilSignal {
addTimer(Duration.ofMillis(500)) {
println("500ms passed!")
signal()
}
}
```
You can create a repeating timer by passing in `repeat = true` to the method. And if you want to stop it from repeating
at some point, set `repeat = false` inside the timer block when it is triggered:
```kotlin
val BLINK_TOTAL_LEN = Duration.ofSeconds(5)
val BLINK_LEN = Duration.ofMillis(250)
var blinkOn by liveVarOf(false)
section {
scopedState {
if (blinkOn) invert()
textLine("This line will blink for ${BLINK_TOTAL_LEN.toSeconds()} seconds")
}
}.run {
var blinkCount = BLINK_TOTAL_LEN.toMillis() / BLINK_LEN.toMillis()
addTimer(BLINK_LEN, repeat = true) {
blinkOn = !blinkOn
blinkCount--
if (blinkCount == 0L) {
repeat = false
}
}
/* ... */
}
```

With timers running, it's possible your `run` block will exit while things are in a state you didn't intend (e.g. in the
above example with the blink effect still on). You should use the `onFinishing` callback to handle this case:
```kotlin
var blinkOn by liveVarOf(false)
section {
/* ... */
}.onFinishing {
blinkOn = false // User might press Q while the blinking state was on
}.runUntilKeyPressed(Keys.Q) {
addTimer(Duration.ofMillis(250), repeat = true) { blinkOn = !blinkOn }
/* ... */
}
```
***Note:** Unlike all the other callbacks we discussed earlier, `onFinishing` is registered directly against the
underlying `section` and not inside the `run` block, because it is actually triggered AFTER the run pass is finished but
before the block is torn down.*
`onFinishing` will only run after all timers are stopped, so you don't have to worry about setting a value that an
errant timer will clobber later.
### 🎥 Animations
Animations make a huge difference for how the user experiences your application, so Kotter strives to make it trivial to
add them into your program.
#### Text Animation
You can easily create quick animations by calling `textAnimOf`:
```kotlin
var finished = false
val spinnerAnim = textAnimOf(listOf("\\", "|", "/", "-"), Duration.ofMillis(125))
val thinkingAnim = textAnimOf(listOf("", ".", "..", "..."), Duration.ofMillis(500))
section {
if (!finished) { text(spinnerAnim) } else { text("✓") }
text(" Searching for files")
if (!finished) { text(thinkingAnim) } else { text("... Done!") }
}.run {
doExpensiveFileSearching()
finished = true
}
```

When you reference an animation in a render for the first time, it kickstarts a timer automatically for you. In other
words, all you have to do is treat your animation instance as if it were a string, and Kotter takes care of the rest!
#### Text animation templates
If you have an animation that you want to share in a bunch of places, you can create a template for it and instantiate
instances from the template. `TextAnim.Template` takes exactly the same arguments as the `textAnimOf` method.
This may be useful if you have a single animation that you want to run in many places at the same time but all slightly
off from one another. For example, if you were processing 10 threads at a time, you may want the spinner for each thread
to start spinning whenever its thread activates:
```kotlin
val SPINNER_TEMPATE = TextAnim.Template(listOf("\\", "|", "/", "-"), Duration.ofMillis(250))
val spinners = (1..10).map { textAnimOf(SPINNER_TEMPLATE) }
/* ... */
```
#### Render animations
If you need a bit more power than text animations, you can use a render animation instead. You create one with a
callback that is given a frame index and access to the current render scope. You can interpret the frame index however
you want and use the render scope to call any of Kotter's text rendering methods that you need.
Declare a render animation using the `renderAnimOf` method and then invoke the result inside your render block:
```kotlin
session {
val exampleAnim = renderAnimOf(numFrames = 5, Duration.ofMillis(250)) { i -> ... }
section {
// RenderAnims act like a function which take a render scope as their first parameter
exampleAnim(this)
...
}
}
```
For example, let's say we want to rotate through a list of colors and apply those to some text. Text animations only
deal with raw text and don't have access to text effects like colors and styles, so we can't use them here, but we can
accomplish these easily using a render animation and the `color(Color)` method:
```kotlin
// Note: Color is a Kotter enum with the main colors it supports
session {
val colorAnim = renderAnimOf(Color.values().size, Duration.ofMillis(250)) { i ->
color(Color.values()[i])
}
section {
colorAnim(this) // Side-effect: sets the color for this section
text("RAINBOW")
}
}
```

### 📥 Offscreen
Occasionally, when you want to render some marked up text, you'll wish you could measure it first, for example allowing
you to pad both sides of each line with spaces to center everything, or putting the right count of "=" characters above
and below a block of text to give it a sort of header effect. But by the time you've rendered something out, then it's
too late!
`offscreen` to the rescue. You can think of `offscreen` as a temporary buffer to render to, after which you can both
query it and control when it actually renders to the screen.
`offscreen` returns a buffer, which is a read-only view of the content. You can query its raw text or line lengths,
for example. To render it, you need to call `offscreen.createRenderer` and then use `renderer.renderNextRow` to render
out each line at a time.
```kotlin
section {
// NOTE: This example doesn't really take advantage of the offscreen buffer,
// but it does showcase all the moving parts.
val buffer = offscreen { ... }
val renderer = buffer.createRenderer()
while (renderer.hasNextRow()) { renderer.renderNextRow() }
}
```
***Note:** Although you usually won't need to, you can create multiple renderers, each which manages its own state for
what row to render out next.*
One nice thing about the offscreen buffer is it manages its own local state, and while it originally inherits its parent
scope's state, any changes you make within the offscreen buffer will be remembered to its end.
This is easier seen than described. The following example:
```kotlin
section {
val buffer = offscreen {
textLine("Inherited color (red)")
cyan()
textLine("Local color (cyan)")
textLine("Still blue")
}
val renderer = buffer.createRenderer()
red()
while (renderer.hasNextRow()) {
text("red -- "); renderer.renderNextRow(); textLine(" -- red")
}
}
```
will render:

The driving motivation for adding offscreen buffers was to be able to easily add borders around any block of text, so
when this functionality went in, we also added the `bordered` method ([link to code](https://github.com/varabyte/kotter/blob/main/kotter/src/main/kotlin/com/varabyte/kotterx/decorations/BorderSupport.kt)).
You can check the implementation yourself to see how it delegates to `offscreen`, padding each row with the right
number of spaces so that the border sides all line up.
### 📤 Aside
You can actually make one-off render requests directly inside a `run` block:
```kotlin
section {
/* ... */
}.run {
aside {
textLine("Hello from an aside block")
}
}
```
which will output text directly before the active section.
In order to understand aside blocks, you should start to think of Kotter output as two parts -- some static history, and
a dynamic, active area at the bottom. The static history will never change, while the active area will be written and
cleared and rewritten over and over and over again as needed.
In general, a section is active *until* it is finished running, at which point it becomes static history, and the next
section becomes active. You can almost think about *consuming* an active section, which freezes it after one final
render, at which point it becomes static.
In fact, it's a common pattern to get static instructions out of the way first, in its own section, so we don't waste
time rerendering them over and over in the main block:
```kotlin
session {
section {
textLine("Press arrow keys to move")
textLine("Press R to restart")
textLine("Press Q to quit")
textLine();
}.run()
section {
... constantly rerendered lines ...
}.runUntilKeyPressed(Keys.Q) { ... }
}
```
Occasionally, however, you want to generate static history *while* a block is still active.
Let's revisit an example from above, our `FileWalker` demo which searched a list of files and added every matching
result to a list. We can, instead, put a spinner in the active section and use the `aside` block to output matches:
```kotlin
val fileWalker = FileWalker(".")
var isFinished by liveVarOf(false)
val searchingAnim = textAnimOf(listOf("", ".", "..", "..."), Duration.ofMillis(500))
section {
textLine()
if (!isFinished) {
textLine("Searching$searchingAnim")
}
else {
textLine("Finished searching")
}
}.run {
aside {
textLine("Matches found so far:")
textLine()
}
fileWalker.findFiles("*.txt") { file ->
aside { textLine(" - ${file.name}") }
}
isFinished = true
}
```

## 🎓 Advanced
### 🔨 "Extending" Kotter
Kotter aims to provide all the primitives you need to write dynamic, interactive console applications, such as
`textLine`, `input`, `offscreen`, `aside`, `onKeyPressed`, etc.
But we may have missed _your_ use case, or maybe you just want to refactor out some logic to share across `section`s.
This is totally doable, but it requires writing extension methods against the correct receiving classes. At this point,
we need to discuss the framework in a bit more detail than beginners need to know.
For reference, you should also look at the [extend](examples/extend) sample project, which was written to demonstrate
some of the concepts that will be discussed here.
#### Scopes
Before continuing, let's look at the overview of a Kotter application. The following may look a bit complex at first
glance, but don't worry as the remaining subsections will break it down:
```
┌───────── | session {
│ ┌─┬───── | section {
│ │ │ | ...
│ │ 3a┌─── | offscreen {
│ │ │ 3b | ...
│ │ │ └─── | }
│ │ └───── | }.onFinished {
1 2 | ...
│ │ ┌───── | }.run {
│ │ │ | ...
│ │ 4 ┌─── | aside {
│ │ │ 3c | ...
│ │ │ └─── | }
│ └─┴───── | }
└───────── | }
```
**1 - `Session`**
```
┌─ session {
│ section {
│ ...
│ }.run {
│ ...
│ }
└─ }
```
The top level of your whole application. `Session` owns a `data: ConcurrentScopedData` field which we'll talk more about
a little later. However, it's worth understanding that `data` lives inside a session, and every time you see some place
exposing a `data` field, it is really pointing back to this singular one.
`Session` is the scope you need when you want to call `liveVarOf` or `liveListOf`, or even to declare a `section`:
```kotlin
fun Session.firstSection() {}
var name by liveVarOf("")
var age by liveVarOf(18)
section { ... }.run { ... }
}
fun Session.secondSection() { ... }
fun Session.thirdSection() { ... }
... later ...
session {
firstSection()
secondSection()
thirdSection()
}
```
**2 - `Section`**
```
┌─ section {
│ ...
│ }.run {
│ ...
└─ }
```
Unlike `Session`, you shouldn't ever need to add an extension method on top of a `Section`, because a section is mainly
just a class for managing two sub-parts - the render logic (which runs on a render thread) and the run logic (which runs
on the main thread).
It is the render and run parts that are particularly interesting, those being the most likely ones that users will want
to extend in general. These are discussed next.
**3 - `RenderScope`**
```
3a
┌─ section {
│ ...
└─ }
3b
┌─ offscreen {
│ ...
└─ }
3c
┌─ aside {
│ ...
└─ }
```
This scope represents a render pass. This is the scope that owns `textLine`, `red`, `green`, `bold`, `underline`, and
other text rendering methods.
This can be a useful scope for extracting out a common text rendering pattern. For example, let's say you wanted to
display a bunch of terminal commands and their arguments, and you want to highlight the command a particular color,
e.g. cyan:
```kotlin
section {