A declarative, Kotlin-idiomatic API for writing dynamic command line applications.

Related tags

Kotlin konsole
Overview

version: 0.9.0 Varabyte Discord

Konsole

"); input(Completions("yes", "no")) if (wantsToLearn) { yellow(isBright = true) { p { textLine("""\(^o^)/""") } } } }.runUntilInputEntered { onInputEntered { wantsToLearn = "yes".startsWith(input.lowercase()) } } } ">
konsoleApp {
  var wantsToLearn by konsoleVarOf(false)
  konsole {
    text("Would you like to learn "); cyan { text("Konsole") }; textLine("? (Y/n)")
    text("> "); input(Completions("yes", "no"))

    if (wantsToLearn) {
      yellow(isBright = true) { p { textLine("""\(^o^)/""") } }
    }
  }.runUntilInputEntered {
    onInputEntered { wantsToLearn = "yes".startsWith(input.lowercase()) }
  }
}

Code sample in action

See also: the game of life, snake, and doom fire implemented in Konsole!


Konsole aims to be a relatively thin, declarative, Kotlin-idiomatic API that provides useful functionality for writing delightful command line 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

The artifact for this project is hosted in our own artifact repository (*), so to include Konsole in your project, modify your Gradle build file as follows:

repositories {
  /* ... */
  maven { url 'https://us-central1-maven.pkg.dev/varabyte-repos/public' }
}

dependencies {
  /* ... */
  implementation 'com.varabyte.konsole:konsole:0.9.0'
}

(* To be hosted in mavenCentral eventually)

Running examples

If you've cloned this repository, examples are located under the examples folder. To try one of them, you can navigate into it on the command line and run it via Gradle.

$ 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:

$ cd examples/life
$ ../../gradlew installDist
$ cd build/install/life/bin
$ ./life

Note: If your terminal does not support features needed by Konsole, then this still may end up running inside a virtual terminal.

Usage

Basic

The following is equivalent to println("Hello, World"). In this simple case, it's definitely overkill!

konsoleApp {
  konsole {
    textLine("Hello, World")
  }.run()
}

konsole { ... } defines a KonsoleBlock which, on its own, is inert. It needs to be run to output text to the console. Above, we use the run method above to trigger this. The method blocks until the render (i.e. text printing to the console) is finished (which, for console text, probably won't be very long).

While the above simple case is a bit verbose for what it's doing, Konsole starts to show its strength when doing background work (or other async tasks like waiting for user input) during which time the block may update several times. We'll see many examples throughout this document later.

Text Effects

You can call color methods directly, which remain in effect until the next color method is called:

konsole {
  green(layer = BG)
  red() // defaults to FG layer if no layer specified
  textLine("Red on green")
  blue()
  textLine("Blue on green")
}.run()

or, 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:

konsole {
  green(layer = BG) {
    red {
      textLine("Red on green")
    }
    textLine("Default on green")
    blue {
      textLine("Blue on green")
    }
  }
}.run()

Various text effects are also available:

konsole {
  bold {
    textLine("Title")
  }

  p {
    textLine("This is the first paragraph of text")
  }

  p {
    text("This paragraph has an ")
    underline { text("underlined") }
    textLine(" word in it")
  }
}.run()

Konsole block state and scopedState

To reduce the chance of introducing unexpected bugs later, state changes (like colors) will be localized to the current konsole block only:

konsole {
  blue(BG)
  red()
  text("This text is red on blue")
}.run()

konsole {
  text("This text is rendered using default colors")
}.run()

Within a Konsole block, you can also use the scopedState method. This creates a new scope within which any state will be automatically discarded after it ends.

konsole {
  scopedState {
    red()
    blue(BG)
    underline()
    text("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.

Dynamic Konsole block

The konsole 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 konsole block (the result integer). This example will run the Konsole block twice - once when run is first called and again when it calls rerender:

var result: Int? = null
konsole {
  text("Calculating... ")
  if (result != null) {
    text("Done! Result = $result")
  }
}.run {
  result = doNetworkFetchAndExpensiveCalculation()
  rerender()
}

The run callback automatically runs on a background thread for you (as a suspend function, so you can call other suspend methods from within it).

Unlike using run without a callback, 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).

KonsoleVar

As you can see above, the run callback uses 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, Konsole provides the KonsoleVar class, which, when modified, will automatically request a rerender. An example will demonstrate this in action shortly.

To create a KonsoleVar, simply change a normal variable declaration line like:

konsoleApp {
  var result: Int? = null
  /* ... */
}

to:

konsoleApp {
  var result by konsoleVarOf<Int?>(null)
  /* ... */
}

Note: The konsoleVarOf method is actually part of the konsoleApp block. For many remaining examples, we'll elide the konsoleApp boilerplate, but that doesn't mean you can omit it in your own program!

Let's apply konsoleVarOf to our earlier example in order to remove the rerender call:

var result by konsoleVarOf<Int?>(null)
konsole {
  /* ... no changes ... */
}.run {
  result = doNetworkFetchAndExpensiveCalculation()
}

And done! Fewer lines and less error pone.

Here's another example, showing how you can use run for something like a progress bar:

// Prints something like: [****------]
val BAR_LENGTH = 10
var numFilledSegments by konsoleVarOf(0)
konsole {
  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()
  }
}

KonsoleList

Similar to KonsoleVar, a KonsoleList 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 KonsoleList. Instead, in a konsoleApp block, use the konsoleListOf method:

() konsole { 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 } /* ... */ } ">
val fileWalker = FileWalker(".") // This class doesn't exist but just pretend for this example...
val fileMatches = konsoleListOf<String>()
konsole {
  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 KonsoleList 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 KonsoleList#withWriteLock method:

() konsole { ... }.run { fileWalker.findFiles("*.txt") { file -> last10Matches.withWriteLock { add(file.name) if (size > 10) { removeAt(0) } } } /* ... */ } ">
val fileWalker = FileWalker(".")
val last10Matches = konsoleListOf<String>()
konsole {
  ...
}.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 konsole { ... } block. Data access is already locked for you in that context.

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 to stop the block from finishing until you're ready:

() fileDownloader.onFinished += { finished.complete(Unit) } finished.await() } ">
val fileDownloader = FileDownloader("...")
fileDownloader.start()
konsole {
  /* ... */
}.run {
  val finished = CompletableDeffered<Unit>()
  fileDownloader.onFinished += { finished.complete(Unit) }
  finished.await()
}

but, for convenience, Konsole provides the signal and waitForSignal methods, which do this for you.

val fileDownloader = FileDownloader("...")
konsole {
  /* ... */
}.run {
  fileDownloader.onFinished += { signal() }
  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.

Alternately, there's a runUntilSignal you can use, within which you don't need to call waitForSignal yourself:

val fileDownloader = FileDownloader("...")
konsole {
  /* ... */
}.runUntilSignal {
  fileDownloader.onFinished += { signal() }
}

User input

Typed input

Konsole 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 Konsole block at that location:

konsole {
  // `input` is a method that appends the user's input typed so far in this
  // konsole block. If your block uses it, the block is automatically
  // rerendered when it changes.
  text("Please enter your name: "); input()
}.run { /* ... */ }

Note that the input method automatically adds a cursor for you. This 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:

konsole {
  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.

konsole {
  text("Please enter your name: "); input()
}.run {
  onInputChanged {
    if (input.any { !it.isLetter() }) { rejectInput() }
    // Would also work: input = input.filter { it.isLetter() }
  }
  /* ... */
}

You can also use onInputEntered. This will be triggered whenever the user presses the ENTER key.

var name = ""
konsole {
  text("Please enter your name: "); input()
}.runUntilSignal {
  onInputChanged { input = input.filter { it.isLetter() } }
  onInputEntered { name = input; signal() }
}

There's actually a shortcut for cases like the above, since they're pretty common: runUntilInputEntered. Using it, we can slightly simplify the above example, typing fewer characters for identical behavior:

var name = ""
konsole {
  text("Please enter your name: "); input()
}.runUntilInputEntered {
  onInputChanged { input = input.filter { it.isLetter() } }
  onInputEntered { name = input }
}

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 event:

quit = true } } while (!quit) { delay(16) /* ... */ } } ">
konsole {
  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.

konsole {
  textLine("Press Q to quit")
  /* ... */
}.runUntilKeyPressed(Keys.Q) {
  while (true) {
    delay(16)
    /* ... */
  }
}

Timers

A Konsole block can manage a set of timers for you. Use the addTimer method in your run block to add some:

konsole {
  /* ... */
}.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:

val BLINK_TOTAL_LEN = Duration.ofSeconds(5)
val BLINK_LEN = Duration.ofMillis(250)
var blinkOn by konsoleVarOf(false)
konsole {
  scopedState {
    if (blinkOn) invert()
    textLine("This line will blink for $BLINK_LEN")
  }

}.run {
  var blinkCount = BLINK_TOTAL_LEN.toMillis() / BLINK_LEN.toMillis()
  addTimer(BLINK_LEN, repeat = true) {
    blinkOn = !blinkOn
    blinkCount--
    if (blinkCount == 0) {
      repeat = false
    }
  }
  /* ... */
}

It's possible your block will exit while things are in a bad state due to running timers, so you can use the onFinishing callback to handle this:

var blinkOn by konsoleVarOf(false)
konsole {
  /* ... */
}.onFinishing {
  blinkOn = false
}.run {
  addTimer(Duration.ofMillis(250), repeat = true) { blinkOn = !blinkOn }
  /* ... */
}

Note: Unlike other callbacks, onFinishing is registered directly against the underlying konsole 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

You can easily create custom animations, by calling konsoleAnimOf:

var finished = false
val spinnerAnim = konsoleAnimOf(listOf("\\", "|", "/", "-"), Duration.ofMillis(125))
val thinkingAnim = konsoleAnimOf(listOf(".", "..", "..."), Duration.ofMillis(500))
konsole {
  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 Konsole takes care of the rest!

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. KonsoleAnim.Template takes exactly the same arguments as the konsoleAnimOf 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:

val SPINNER_TEMPATE = KotlinAnim.Template(listOf("\\", "|", "/", "-"), Duration.ofMillis(250))

val spinners = (1..10).map { konsoleAnimOf(SPINNER_TEMPLATE) }
/* ... */

Advanced

Thread Affinity

Setting aside the fact that the run block runs in a background thread, Konsole blocks themselves are rendered sequentionally on a single thread. Anytime you make a call to run a Konsole block, no matter which thread it is called from, a single thread ultimately handles it. At the same time, if you attempt to run one konsole block while another block is already running, an exception is thrown.

I made this decision so that:

  • I don't have to worry about multiple Konsole blocks printlning at the same time - who likes clobbered text?
  • Konsole handles repainting by moving the terminal cursor around, which would fail horribly if multiple Konsole blocks tried doing this at the same time.
  • Konsole embraces the idea of a dynamic, active block trailed by a bunch of static history. If two dynamic blocks wanted to be active at the same time, what would that even mean?

In practice, I expect this decision won't be an issue for most users. Command line apps are expected to have a main flow anyway -- ask the user a question, do some work, then ask another question, etc. It is expected that a user won't ever even need to call konsole from more than one thread. It is hoped that the konsole { ... }.run { ... } pattern is powerful enough for most (all?) cases.

Virtual Terminal

It's not guaranteed that every user's command line setup supports ANSI codes. For example, debugging this project with IntelliJ as well as running within Gradle are two such environments where functionality isn't available! According to many online reports, Windows is also a big offender here.

Konsole will attempt to detect if your console does not support the features it uses, and if not, it will open up a virtual terminal. This fallback gives your application better cross-platform support.

To modify the logic to ALWAYS open the virtual terminal, you can construct the virtual terminal directly and pass it into the app:

konsoleApp(terminal = VirtualTerminal.create()) {
  konsole { /* ... */ }
  /* ... */
}

or if you want to keep the same behavior where you try to run a system terminal first and fall back to a virtual terminal later, but perhaps you want to customize the virtual terminal with different parameters, you can use:

konsoleApp(terminal = SystemTerminal.or {
  VirtualTerminal.create(title = "My App", terminalSize = Dimension(30, 30))
}) {
  /* ... */
}

Why Not Compose / Mosaic?

Konsole's API is inspired by Compose, which astute readers may have already noticed -- it has a core block which gets rerun for you automatically as necessary without you having to worry about it, and special state variables which, when modified, automatically "recompose" the current console block. Why not just use Compose directly?

In fact, this is exactly what Jake Wharton's Mosaic is doing. Actually, I tried using it first but ultimately decided against it before deciding to write Konsole, for the following reasons:

  • Compose is tightly tied to the current Kotlin compiler version, which means if you are targeting a particular version of the Kotlin language, you can easily see the dreaded error message: This version (x.y.z) of the Compose Compiler requires Kotlin version a.b.c but you appear to be using Kotlin version d.e.f which is not known to be compatible.

    • Using Kotlin v1.3 or older for some reason? You're out of luck.
    • I suspect this issue with Compose will improve over time, but for the present, it still seems like a non-Compose approach could be useful to many.
  • Compose is great for rendering a whole, interactive UI, but console printing is often two parts: the active part that the user is interacting with, and the history, which is static. To support this with Compose, you'd need to manage the history list yourself and keep appending to it, and it was while thinking about an API that addressed this limitation that I envisioned Konsole.

  • Compose encourages using a set of powerful layout primitives, namely Box, Column, and Row, with margins and shapes and layers. Command line apps don't really need this level of power, however.

  • Compose has a lot of strengths built around, well, composing methods! And to enable this, it makes heavy use of features like remember blocks, which you can call inside a composable method and it gets treated in a special way. But for a simple CLI library, being able to focus on render blocks that don't nest too deeply and not worrying as much about performance allowed a more pared down API to emerge.

  • Compose does a lot of nice tricks due to the fact it is ultimately a compiler plugin, but it is nice to see what the API would kind of look like with no magic at all (although, admittedly, with some features sacrificed).

Mosaic comparison

// Mosaic
runMosaic {
  var count by remember { mutableStateOf(0) }
  Text("The count is: $count")

  LaunchedEffect(null) {
    for (i in 1..20) {
      delay(250)
      count++
    }
  }
}

// Konsole
konsoleApp {
  var count by konsoleVarOf(0)
  konsole {
    textLine("The count is: $count")
  }.run {
    for (i in 1..20) {
      delay(250)
      count++
    }
  }
}

Comparisons with Mosaic are included in the examples/mosaic folder.

Comments
  • Add handler for exception that occurs within run?

    Add handler for exception that occurs within run?

    Right now, if an exception occurs within run, the program just exits. A user can themselves wrap everything inside a try / catch block (within the run block), but it might be nice if we had a method, onFailure maybe, similar to onFinished

    Edit: Added "within the run block" for clarity

    enhancement 
    opened by bitspittle 19
  • Timing issues with aside sections

    Timing issues with aside sections

    First: Great library, I like it a lot!

    Describe the bug

    That very simple demo below always only yields

    Do something...
    

    swallowing all the asides.

    session {
      section {
        textLine("Do something...")
      }.run {
        aside { textLine(" - a") }
        aside { textLine(" - b") }
        aside { textLine(" - b") }
        aside { textLine(" - f") }
        // to be replaced
      }
    }
    

    If I replace // to be replaced at the end of the run block with something like Thread.sleep(1) (yes 1 ms is enough) or a "heavy" computation that takes some ms everything's printed out. I am suspecting a timing issue.

    Do you have any thoughts?

    bug 
    opened by cedriessen 8
  • Add OSC support, add OSC 8 (anchor) support to the public api, add a …

    Add OSC support, add OSC 8 (anchor) support to the public api, add a …

    …simple implementation in VirtualTerminal, and use in 'input' example. Fixes #84.

    Hope this is useful for you.

    Running in iTerm...

    image

    Also see the definitive reference https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda

    opened by cbcmg 6
  • A simple game using Kotter

    A simple game using Kotter

    First off, I like Kotter a lot, thank you for your work!

    I wanted to see how versatile it is, so I'm making a small game with a main game loop, continuous rendering and multiple screens all in Kotter. I mostly do Android development so I wrote a very simple MVVM wrapper around Kotter session and render scopes and it seems to work well.

    https://github.com/LeafyLappa/kcooking/tree/dev

    Although it's more of a proof-of-concept thing, the game runs and there's basic gameplay!

    opened by LeafyLappa 6
  • Add support for OSC hyperlinks

    Add support for OSC hyperlinks

    See also: https://en.wikipedia.org/wiki/ANSI_escape_code#OSC_(Operating_System_Command)_sequences See also: https://github.com/varabyte/kotter/issues/12#issuecomment-1144444459 See also: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda

    I'd really like to do this assuming support for it is standard across all of the main terminals.

    @cbcmg FYI

    enhancement maybe 
    opened by bitspittle 4
  • Handle control C

    Handle control C

    We should restore the cursor when the user presses Ctrl+C, and probably add handler methods to KonsoleBlock and KonsoleApp that get triggered as well so users can clean up if it happens.

    enhancement 
    opened by bitspittle 4
  • Add an API for drawing a box

    Add an API for drawing a box

    Something like

    box(WIDTH, HEIGHT, x, y, "TEXT")
    
    where
    
    box(10, 10, 3, 5, "Hello")
    
    would render (assume X's are spaces):
    
    XXXXXXXXXX
    XXXXXXXXXX
    XXXXXXXXXX
    XXXXXXXXXX
    XXXXXXXXXX
    XXXHelloXX
    XXXXXXXXXX
    XXXXXXXXXX
    XXXXXXXXXX
    XXXXXXXXXX
    

    This would be useful in the robot example but also for something like, say, a "screensaver", like a text string that bounces around the console.

    But this is low priority, and honestly may be a very niche case.

    enhancement maybe 
    opened by bitspittle 3
  • MAJOR CHANGE: Rename

    MAJOR CHANGE: Rename "konsole" to something else

    The name Konsole sits too closely to KDE Konsole, which is confusing.

    So, this bug is a few parts...

    1. Remove all references of the name "konsole" from class, method, and variable names, e.g. konsoleApp -> session and konsoleVarOf to liveVarOf
      • Once this is done, the code itself will no longer care about large refactorings of name changes in the future.
    2. Come up with a new name for the library. I'm leaning on "Kotter" for "KOTlin TERminal" but I'm open to other ideas
    3. Update the package / artifacts / README / screencasts / this repo name / our Discord channel etc. to match

    This is a pretty big change for this library. This may inconvenience users who are using it, so I'll give this bug a day or two just in case anyone wants to respond. But so far, I haven't seen much of an indication that people are actually using it yet, so it seems like a good time to make such a drastic change.

    enhancement process 
    opened by bitspittle 2
  • Allow executing other programs while Konsole is running

    Allow executing other programs while Konsole is running

    For example, maybe an app will want to call git to sync, or some other function that returns output.

    Right now, Konsole has taken over System.in, System.out, and System.err - so we may need an escape hatch for when the user wants to say "I'm not in a Konsole block right now, let someone else print stuff for a whiel"

    Maybe

    konsoleApp {
       konsole { ... }.run()
       konsoleExec("git clone")
       konsole { ... }.run()
    }
    

    ?

    enhancement maybe 
    opened by bitspittle 2
  • Allow addTimer to take a key value for uniqueness

    Allow addTimer to take a key value for uniqueness

    Currently in Snake I'm doing something like:

    var firstMove = true
    level.snake.onMoved = {
       if (firstMove) {
          firstMove = false
          addTimer(KonsoleAnim.ONE_FRAME_60FPS, repeat = true) {
             ...
          }
       }
    }
    

    which could possibly be simplified to:

    level.snake.onMoved = {
       addTimer(KonsoleAnim.ONE_FRAME_60FPS, repeat = true, key = level.snake) {
          ...
       }
    }
    

    In other words, if a timer with the particular key is already found, don't add it again.

    enhancement maybe 
    opened by bitspittle 2
  • Add

    Add "transform" callback to input() method

    Imagine you're typing a password. It could be nice to do something like this:

    input(transform { ch -> '*' })
    

    If you do this, the underlying value is still valid. It's only what's printed to the screen that is changed.

    Question: Should the transformation work per character? Or on the whole input?

    e.g. password could be this:

    input(transform { str -> "*".repeat(str.length) })
    
    enhancement 
    opened by bitspittle 1
  • Can't use Kotter without virtual terminal

    Can't use Kotter without virtual terminal

    Describe the bug Hello Kotter authors,

    First of all I really love the library! And I feel like your post about Compose dependencies was spot on. I arrived at Kotter the same way you arrived at building it, incidentally.

    To Reproduce

    1. Download Kotter as part of a Gradle build
    2. Hook it up to a Picocli terminal app
    3. From zsh on a mac, try spawning a terminal (forced to be system terminal, non-virtual)
    4. Observe failures

    I have also observed some strange compiler errors, and errors in IDEA, when the app breaks in this manner. Other dependencies aren't found, for some reason, even though they were found in the previous compile run. This could simply be an error inside IDEA, but I'm sharing here in case it helps diagnose.

    Expected behavior I was expecting my case to be rather vanilla for the library, so I was surprised that it didn't work.

    Screenshots I am happy to provide a screenshot, but, in essence, a virtual terminal opens when the system terminal should be used instead. Since I'm on zsh on a Unix-like machine, ANSI color support should work and it should be able to access the system terminal. I'm not supporting Windows at the moment, so there is no code related to Windows/Jansi that might be interfering here.

    Desktop (please complete the following information):

    • OS: macOS Ventura
    • Browser: N/A, it's CLI
    • Version Latest

    Additional context I am going to try to capture more error information so I can make this ticket more useful, but I'm filing ahead of that because it has halted our ability to use Kotter :(

    As a library author myself I'd definitely want to know about any such issues. Thank you again for all of your hard work on this tool!

    bug 
    opened by sgammon 0
  • Consider opening up `link(uri, block)`

    Consider opening up `link(uri, block)`

    Currently, we have implemented a method for creating OSC links with the following signature:

    fun RenderScope.link(uri: URI, block: RenderScope.() -> Unit) { ... }
    

    which you could call like:

    link("https://example.com") {
       text("click here")
    }
    

    but opening up this generic API feels like room for problems to sneak in:

    link(...) {
       link(...) { uh oh }
    }
    

    and does it makes sense to add underlines, clear underlines, change colors, etc. inside links?

    So, for now, we've marked it an non-public.

    We're only exposing the more constrained version:

    fun RenderScope.link(uri: URI, displayText: String)
    

    which you can call like:

    link("https://example.com", "click here")
    

    even if it, internally, delegates to the more general API.

    I think the constrained version is all that people will ever really need, but I'll keep this bug open in case users have good reasons to want to use the general one.

    enhancement maybe 
    opened by bitspittle 0
  • Fix busted unicode (emoji?) support

    Fix busted unicode (emoji?) support

    Well, nuts. String.length miscounts unicode characters and we might be using it everywhere.

    Something like this should render incorrectly

    bordered {
       textLine("Hello")
       textLine("Oh my stars ⭐⭐⭐ this just screws it up")
       textLine("Goodbye") 
    }
    

    Add stars to test cases, and probably use text.codePoints.count() instead of text.length. Audit all the code...

    bug 
    opened by bitspittle 1
  • Virtual terminal: Fix underline showing on newline

    Virtual terminal: Fix underline showing on newline

    The following

    underline { textLine("Test") }

    currently adds a weird tiny piece of underline on the next line.

    This only happens in the virtual terminal, not in an actual terminal.

    bug 
    opened by bitspittle 0
  • Investigate / add Kotlin native support

    Investigate / add Kotlin native support

    Context: https://www.reddit.com/r/Kotlin/comments/q17zjq/introducing_konsole_a_kotlinidiomatic_library_for/hfggbsv/

    Currently, this library is JVM only. Adding native support would allow users to use this library in a Kotlin native context, which would:

    • result in smaller file sizes / let people run on systems that don't have Java installed(?)
    • have better performance(?)
    • make this library available to Kotlin native projects

    Current challenges:

    • I'm currently leaning on the JVM library Jline for system terminal support, although there is almost certainly a C library that we can hook into instead
    • I'm using Swing to create a virtual terminal, but we can exclude Virtual Terminal support for native targets.
    • I've never written a Kotlin Native app before. I may be relying on JVM-only features without realizing it, like my threading logic or who knows what else?

    This bug isn't a priority for me at the moment because it's not clear to me how many people actually want this support, but hey, if you do, please indicate so by adding a thumbs up reaction to it (and consider leaving a comment on why you don't want to just use the JVM, I'd be curious!)

    enhancement maybe 
    opened by bitspittle 4
Releases(v1.0.0)
  • v1.0.0(Oct 29, 2022)

    Announcing: the first stable release of Kotter :tada: :tada: :tada:

    At this point, Kotter provides a core API for writing dynamic CLIs that is:


    Outside of documentation changes, there were a handful of API changes that snuck in since v1.0.0-rc3, gaps I noticed when poring over the API docs:

    • TextAnims no longer implement CharSequence.
      • The original decision was causing its API to expose additional properties that were unnecessarily confusing (e.g. length means the length of the text for that frame, not the length of the animation.)
      • If, after upgrading, you get a compile error that text(myTextAnim) in your project, simply right-click on it and import the new extension method that fixes it. Or, change it to text("$myTextAnim") as another alternative.
    • Added a new shiftRight { ... } method, which takes any text rendered in the block and prepends all lines with spaces.
    • Added the minWidth property to the justified method
      • So you can now do justified(CENTER, minWidth = 10) { text("Hi") }
    • Animation changes
      • Allow pausing animations via the paused property
      • Provide the looping parameter when creating animations, so that you can define one-shot animations.
      • Added a few other properties for querying the state of animations: totalDuration, isRunning, and lastFrame
    • Fixed a crash in the virtual terminal you could get clicking on a blank line.
    Source code(tar.gz)
    Source code(zip)
  • v1.0.0-rc3(Oct 21, 2022)

    • Code coverage is now above 90% and very healthy
    • Introduced the new link(...) method for adding text links
      • For example: link("https://github.com/varabyte/kotter", "Learn Kotter!")
      • See also: OSC hyperlink support
      • Huge thanks to @cbcmg for their support with this feature! :bow:

    With the current level of code coverage and the fact that link was the last feature I was planning on squeezing into 1.0, it's hoped that this 1.0-rc3 release will become 1.0, assuming no bugs or other issues are discovered by users before then. I might also make a pass at reviewing / updating code docs, but if all goes well, no more code changes will need to be made :crossed_fingers:

    Source code(tar.gz)
    Source code(zip)
  • v1.0.0-rc2(Oct 20, 2022)

    • Tons of unit tests added, bringing code coverage up to > 70%, and fixing a slew of subtle bugs that were uncovered by them.
    • Added a new viewMap parameter to the input method, which allows you to map characters in your input text before they show up on the screen.
      • For example, the feature allows password fields like so: input(viewMap = { '*' })
    • Created (and published) a kotter-test-support library, as a home for code that I not only use in my own Kotter tests but could be useful to anyone who wants to test their own programs backed by Kotter.
    Source code(tar.gz)
    Source code(zip)
  • v1.0.0-rc1(Oct 8, 2022)

    First 1.0.0 release candidate! :tada:

    At this point, all v1.0 milestone bugs have been resolved except for a final code coverage requirement.

    This seems like a good time to start advertising we're getting close to a 1.0 release. If no high priority community feature requests or bugs come in, we'll just keep adding more tests over time, and lock things down once code coverage hits a high enough percent.

    Now, onto the fixes added in this release:

    • Added new LiveMap and LiveSet classes, in a similar vein to the existing LiveList class.
    • Any exceptions thrown during the run block are no longer silently swallowed but will now crash the calling thread. This makes it so you no longer have programs that might fail silently, and also you can wrap a whole session in a single try/catch block now to handle all possible errors.
    • Fixed a bug where asides in a run block could get lost if their parent section block finished too quickly.
    • Exceptions thrown in a section block no longer prevent internal state from getting cleaned up

    In addition to the bug fixes, code coverage was added and is now being tracked. As we've added more and more unit tests for this goal, it has helped surface subtle inefficiencies with extra rendering that have since been fixed.

    Source code(tar.gz)
    Source code(zip)
  • v0.9.9(May 16, 2022)

    • Added ways to query and set input() values outside of onInputChanged / onInputEntered methods.
      • You shouldn't need this in most cases but it's an important escape hatch when available.
    • Created a new VirtualTerminal implementation backed by Compose. It works but is still considered experimental.
      • It is in a separate kotterx.compose module.
      • Unlike the Swing version, you can't simply deploy it effortlessly, but you need to package it differently on Windows, MacOS, and Linux. It may only ever be good for the rare application that intentionally wants to launch ONLY uses a virtual terminal and never using an actual command line.
    • Added a clearTerminal argument that you can set to true when creating a session. Useful if you're making a "full screen" terminal application, such as a game.
    • Added a handful of missing color methods. Thanks @LeafyLappa!
    • Fixed a hard lock that could happen with subtle timing if you pressed ESC very quickly followed by a control key.
    Source code(tar.gz)
    Source code(zip)
  • v0.9.8(Apr 18, 2022)

    • README updated, with animated gifs or at least screenshots for most examples.
    • Fixed a bug with how p worked, which was buggy if you had two p blocks in a row.
    • Fixed a bug where setting addTimer { repeat = false } wasn't actually working behind the scenes
    • Major change to input enabling advanced use-cases:
      • You can now call input() more than once in a render block. However, only one input block can be active at a time, and each input block must have a different ID.
      • Added onInputActivated and onInputDeactivated callbacks for intercepting focus change events.
      • Updated the input example to demonstrate this new behavior.
      • Simple code written as described in the README should still work.
    Source code(tar.gz)
    Source code(zip)
  • v0.9.7(Apr 14, 2022)

    Misc bug fixes / minor feature additions.

    • Fixed a bug with border rendering caused by setting both left/right AND top/bottom padding
    • Fixed an occasional crash that could be caused due to a threading issue around how the ESC key was handled
    • Added a try/catch around code that calls into user space and previously could break system assumptions from within if it threw an exception
    • Added the ability to call clearInput in onInputEntered, ensuring the input() field would be empty on the next render pass.
    Source code(tar.gz)
    Source code(zip)
  • v0.9.6(Jan 6, 2022)

    Patch fix.

    • BREAKING CHANGE animOf is now textAnimOf (and Anim is now TextAnim)
    • Introduce renderAnimOf and RenderAnim
    • Update Kotlin compilation version (which doesn't affect the end user but eliminates warnings when compiling Kotter)
    Source code(tar.gz)
    Source code(zip)
  • v0.9.5(Jan 3, 2022)

    Patch fix.

    • BREAKING CHANGE RunScope is now a top-level class.
      • Change imports of Section.RunScope to just RunScope
      • Made this change for API consistency with RenderScope
    • Added hasNextLine to the offscreen buffer renderer class.
    • Fixed a crash when ConcurrentScopedData value initialization register dependent values as part of its logic.
    • Added a new sliding tile sample.
    • Added a new "extend Kotter" sample.
    • Added a bunch of information to the README about extending Kotter.
    Source code(tar.gz)
    Source code(zip)
  • v0.9.4(Dec 29, 2021)

    Patch fix.

    This release was focused on some input(...) improvements.

    Major changes since last release include:

    • Fixed a bug with p { ... } adding an extra leading newline in some cases
    • Lifecycles can now be linked hierarchically, so that you can group some keys together as a subset of a greater lifecycle
      • Use the new lifecycle hierarchy feature to clean up input() code
    • Add initialText to input(...) which can be used to set, um, initial text (shocking, I know), when input is called for the first time.
    • Add id to input(...), which can be used if you have multiple input(...) blocks in a single section and want to make sure text is reset when bouncing between them
    • Release resources allocated by input(...) are now released if input(...) is not called on a subsequent render.
    Source code(tar.gz)
    Source code(zip)
  • v0.9.3(Dec 28, 2021)

    Patch fix. Major changes since last release include:

    • New offscreen and bordered blocks
      • You can now "render" commands offscreen so you can query results for width / height before actually rendering them
      • This allows you to make smart layout decisions, e.g. wrap your text with a border, or center text, etc.
      • New sample: "Border" to showcase bordered
    • Added a parameter for setting max num lines on the virtual terminal
    • Added support for clicking on URLs inside the virtual terminal
    Source code(tar.gz)
    Source code(zip)
  • v0.9.2(Dec 28, 2021)

    Patch fix. Major changes since last release include:

    • BREAKING CHANGE Konsole is now renamed to Kotter
      • And various classes with "Konsole" in the name have been renamed
      • See #74 for more details
    • Changed how you specify a chain of Terminal factory methods
    • Richer color support
      • rgb / hsv methods (24-bit color)
      • color by index (8-bit color)
    • New sample: "color picker"
    Source code(tar.gz)
    Source code(zip)
  • v0.9.1(Oct 28, 2021)

    Patch fix. Major changes since last release include:

    • Windows is now supported
    • Fixed an issue with the Virtual Terminal swallowing TAB
    • New samples: "doom fire" and "keys"
    • Misc. internal fixes and documentation cleanup
    Source code(tar.gz)
    Source code(zip)
  • v0.9.0(Oct 2, 2021)

    This is the first public release of Konsole. There are no guarantees of API stability between now and 1.0, but it's hoped that at this point there are no fundamental changes that will be required. The library is functional and can already allow one to write powerful, dynamic CLI applications with it.

    The goal of releasing this version to the community is to learn if people are interested in it, get feedback, handle bug reports, close API gaps, and learn about any development environments where this framework fails to work.

    Source code(tar.gz)
    Source code(zip)
Owner
Varabyte
Varabyte
Utility - The cross-platform native Kotlin command line tool template

Utility The cross-platform native Kotlin command line tool template. Usage Make

null 0 Jan 3, 2022
kinstall is an easy way to install gradle-based command-line kotlin projects that use the application plugin.

kinstall kinstall is an easy way to install gradle-based command-line kotlin projects that use the application plugin. use First, install kinstall its

david kilmer 0 Apr 24, 2022
Run Minecraft on the command line

HeadlessForge While headless Minecraft Clients aren't anything new, they come with a drawback. The Minecraft API is missing and you need to add all fu

null 28 Oct 17, 2022
A fast-prototyping command line system

Kotlin-er CLI A different take on making command line programs, emphasizing development speed over customization Status Auto-deployed to Maven Central

Lightning Kite 22 Jan 22, 2022
{ } Declarative Kotlin DSL for choreographing Android transitions

Transition X Kotlin DSL for choreographing Android Transitions TransitionManager makes it easy to animate simple changes to layout without needing to

Arunkumar 520 Dec 16, 2022
Kotlin Multiplatform Mobile + Mobile Declarative UI Framework (Jetpack Compose and SwiftUI)

Kotlin Multiplatform Mobile + Mobile Declarative UI Framework (Jetpack Compose and SwiftUI)

Kotchaphan Muangsan 3 Nov 15, 2022
fusion4j - declarative rendering language for the JVM based on Neos.Fusion

fusion4j - declarative rendering language for the JVM based on Neos.Fusion Supports the Neos Fusion syntax/semantic as described in the official Neos

sandstorm 2 May 3, 2022
Ivy FRP is a Functional Reactive Programming framework for declarative-style programming for Android

FRP (Functional Reactive Programming) framework for declarative-style programming for Andorid. :rocket: (compatible with Jetpack Compose)

null 8 Nov 24, 2022
A framework for writing composable parsers based on Kotlin Coroutines.

Parsus A framework for writing composable parsers based on Kotlin Coroutines. val booleanGrammar = object : Grammar<BooleanExpression>() { val ws

Aleksei Semin 28 Nov 1, 2022
Extension functions over Android's callback-based APIs which allows writing them in a sequential way within coroutines or observe multiple callbacks through kotlin flow.

callback-ktx A lightweight Android library that wraps Android's callback-based APIs into suspending extension functions which allow writing them in a

Sagar Viradiya 171 Oct 31, 2022
Utility for developers and QAs what helps minimize time wasting on writing the same data for testing over and over again. Made by Stfalcon

Stfalcon Fixturer A Utility for developers and QAs which helps minimize time wasting on writing the same data for testing over and over again. You can

Stfalcon LLC 31 Nov 29, 2021
Same as an Outlined text fields presented in Material Design page but with some dynamic changes

README SSCustomEditTextOutlineBorder Getting Started SSCustomEditTextOutLineBorder is a small kotlin library for android to support outlined (stroked)

Simform Solutions 194 Dec 30, 2022
A library for creating dynamic skeleton view

Skeleton Placeholder View Overview A Library designed to draw a Skeleton by "skinning" the view from a provided layout. Skeleton is composed of Bone w

Ferry Irawan 25 Jul 20, 2021
Modern Calendar View Supporting Both Hijri and Gregorian Calendars but in highly dynamic way

KCalendar-View Modern calendar view supporting both Hijri and Gregorian calendar

Ahmed Ibrahim 8 Oct 29, 2022
Example Multi module architecture Android project using MVVM, Dynamic Features, Dagger-Hilt, Coroutines and Navigation Components

ModularDynamicFeatureHilt An Android template project following a multi module approach with clean architecture. It has been built following Clean Arc

Mbuodile Obiosio 25 Nov 23, 2022
An app to demonstrate Apple's new feature dynamic island in Android with Jetpack Compose

Jet Island Dynamic Island in Android with Jetpack Compose An app to demonstrate Apple's new feature dynamic island in Android with Jetpack Compose. Th

Cengiz TORU 101 Nov 29, 2022
Simple cron command runner for Bukkit 1.17+

Cron Runner Description / 설명 Simple cron command runner for Bukkit 1.17+ Bukkit 1.17 이상을 위한 간단한 cron 명령어 실행기 입니다. Configuration Guide (config.yml) deb

Patrick 3 Sep 24, 2021
Clay is an Android library project that provides image trimming which is originally an UI component of LINE Creators Studio

Clay Clay is an Android library project that provides image trimming. Fully written in Kotlin, Clay is originally a UI component of LINE Creators Stud

LINE 119 Dec 27, 2022