Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5b374b9
Simplify types on Color and Format
noelwelsh Apr 5, 2026
cacf17e
Don't clear terminal input queue in read
noelwelsh Apr 13, 2026
b54691e
Update dependencies
noelwelsh Apr 13, 2026
f8eb5ed
Accept newline as enter
noelwelsh Apr 13, 2026
894ea26
Remove unused import
noelwelsh Apr 14, 2026
091b84c
Initial sketch of UI framework
noelwelsh Apr 15, 2026
2745055
Update plugins
noelwelsh Apr 15, 2026
56b4014
Rebuild workflows
noelwelsh Apr 15, 2026
922048f
Reformat to braceless style
noelwelsh Apr 15, 2026
e1423d9
Move dependencies into build.sbt
noelwelsh Apr 15, 2026
e0c427a
UI example running in Scala Native
noelwelsh Apr 15, 2026
0e536b3
Increase heap size in sbt
noelwelsh Apr 15, 2026
6e3f6f2
Ignore .scala-build
noelwelsh Apr 15, 2026
14b92ad
Extend support styles
noelwelsh Apr 15, 2026
a1a684a
Support wide characters
noelwelsh Apr 15, 2026
9f4bff9
Add buffer differencing
noelwelsh Apr 15, 2026
81a5a5d
Add Column layout component
noelwelsh Apr 15, 2026
4a010d0
Handle newlines in Buffer.putString
noelwelsh Apr 15, 2026
a4f2b91
Add CLAUDE.md and design notes
noelwelsh Apr 15, 2026
526be59
Add component-level styling
noelwelsh Apr 15, 2026
cdbdb99
Contexts recalculate size on each call
noelwelsh Apr 15, 2026
0529a7d
Remove empty file
noelwelsh Apr 16, 2026
97d4167
Bump base version
noelwelsh Apr 16, 2026
5b98196
Add reactive event loop and signal model
noelwelsh Apr 17, 2026
93cace5
Add effects to show and hide the cursor
noelwelsh Apr 17, 2026
9890d2f
Change quit key to q
noelwelsh Apr 17, 2026
f25964d
Update architecture notes and fix example quit key
noelwelsh Apr 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout current branch (full)
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0

Expand All @@ -45,7 +45,7 @@ jobs:
- name: Setup Java (temurin@11)
id: setup-java-temurin-11
if: matrix.java == 'temurin@11'
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 11
Expand Down Expand Up @@ -83,15 +83,15 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: mkdir -p unidocs/target core/native/target core/js/target examples/js/target core/jvm/target examples/jvm/target project/target
run: mkdir -p ui/js/target unidocs/target core/native/target core/js/target ui/jvm/target examples/js/target core/jvm/target examples/jvm/target ui/native/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: tar cf targets.tar unidocs/target core/native/target core/js/target examples/js/target core/jvm/target examples/jvm/target project/target
run: tar cf targets.tar ui/js/target unidocs/target core/native/target core/js/target ui/jvm/target examples/js/target core/jvm/target examples/jvm/target ui/native/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: target-${{ matrix.os }}-${{ matrix.java }}-${{ matrix.scala }}-${{ matrix.project }}
path: targets.tar
Expand All @@ -107,7 +107,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout current branch (full)
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0

Expand All @@ -117,7 +117,7 @@ jobs:
- name: Setup Java (temurin@11)
id: setup-java-temurin-11
if: matrix.java == 'temurin@11'
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 11
Expand All @@ -128,7 +128,7 @@ jobs:
run: sbt +update

- name: Download target directories (3, rootJS)
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootJS

Expand All @@ -138,7 +138,7 @@ jobs:
rm targets.tar

- name: Download target directories (3, rootJVM)
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootJVM

Expand All @@ -148,7 +148,7 @@ jobs:
rm targets.tar

- name: Download target directories (3, rootNative)
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootNative

Expand Down Expand Up @@ -191,7 +191,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout current branch (full)
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0

Expand All @@ -201,7 +201,7 @@ jobs:
- name: Setup Java (temurin@11)
id: setup-java-temurin-11
if: matrix.java == 'temurin@11'
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 11
Expand All @@ -226,7 +226,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout current branch (full)
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0

Expand All @@ -236,7 +236,7 @@ jobs:
- name: Setup Java (temurin@11)
id: setup-java-temurin-11
if: matrix.java == 'temurin@11'
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 11
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -144,5 +144,6 @@ local.properties
.bloop
project/metals.sbt
.vscode
.scala-build

.sbt-hydra-history
1 change: 1 addition & 0 deletions .sbtopts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-J-Xmx4G
3 changes: 2 additions & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
version = "3.8.1"
version = "3.11.0"
runner.dialect = scala3
rewrite.scala3.convertToNewSyntax = true
rewrite.scala3.optionalBraces.enabled = true
21 changes: 21 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Terminus

Terminus is a Scala terminal toolkit, cross-platform across JVM, JS, and Scala Native. It is split into two main modules:

- **`core/`** — low-level effect traits and ANSI escape code primitives
- **`ui/`** — cell buffer–based terminal UI toolkit built on top of core

## Notes

Design notes and architectural decisions are kept in `notes/`:

- [`notes/ui-architecture.md`](notes/ui-architecture.md) — cell buffer model, component/layout system, effect layer, wide character support
- [`notes/styling-design.md`](notes/styling-design.md) — agreed design direction for styling (style split, no cascade, CSS box model, separators)

## Build

```
sbt compile
sbt test
sbt 'uiNative/runMain terminus.ui.demo'
```
41 changes: 30 additions & 11 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import laika.config.ApiLinks
import laika.theme.Theme
import laika.helium.config.TextLink

ThisBuild / tlBaseVersion := "0.4" // your current series x.y
ThisBuild / tlBaseVersion := "0.5" // your current series x.y

Global / onChangedBuildSource := ReloadOnSourceChanges

Expand All @@ -33,8 +33,6 @@ ThisBuild / developers := List(
tlGitHubDev("noelwelsh", "Noel Welsh")
)

ThisBuild / sonatypeCredentialHost := xerial.sbt.Sonatype.sonatypeLegacy

lazy val scala3 = "3.3.4"

ThisBuild / crossScalaVersions := List(scala3)
Expand All @@ -61,30 +59,51 @@ commands += Command.command("build") { state =>
state
}

// Dependencies

val catsCore = Def.setting("org.typelevel" %%% "cats-core" % "2.13.0")

val jline = Def.setting("org.jline" % "jline" % "4.0.12")

val scalajsDom = Def.setting("org.scala-js" %%% "scalajs-dom" % "2.8.1")

val munitVersion = "1.3.0"
val munit = Def.setting("org.scalameta" %%% "munit" % munitVersion % "test")
val munitScalaCheck =
Def.setting("org.scalameta" %%% "munit-scalacheck" % munitVersion % "test")

// Projects and Settings

lazy val commonSettings = Seq(
libraryDependencies ++= Seq(
Dependencies.munit.value,
Dependencies.munitScalaCheck.value
munit.value,
munitScalaCheck.value
),
startYear := Some(2024),
licenses := List(
"Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0.txt")
)
)

lazy val root = tlCrossRootProject.aggregate(core, unidocs)
lazy val root = tlCrossRootProject.aggregate(core, ui, unidocs)

lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.in(file("core"))
.settings(
commonSettings,
libraryDependencies ++= Seq(
Dependencies.catsCore.value
),
libraryDependencies ++= Seq(catsCore.value),
name := "terminus-core"
)
.jvmSettings(libraryDependencies += Dependencies.jline.value)
.jsSettings(libraryDependencies += Dependencies.scalajsDom.value)
.jvmSettings(libraryDependencies += jline.value)
.jsSettings(libraryDependencies += scalajsDom.value)

lazy val ui = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.in(file("ui"))
.settings(
name := "terminus-ui",
commonSettings
)
.dependsOn(core)

lazy val docs =
project
Expand Down
19 changes: 7 additions & 12 deletions core/js/src/main/scala/terminus/Terminal.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ import scala.concurrent.Future
import scala.concurrent.Promise

class Terminal(root: HTMLElement, options: XtermJsOptions)
extends effect.Color[Terminal],
extends effect.Color,
effect.Cursor,
effect.Format[Terminal],
effect.Format,
effect.Erase,
effect.Dimensions,
effect.Writer {
effect.Writer:

private val keyBuffer: mutable.ArrayDeque[Promise[String]] =
new mutable.ArrayDeque[Promise[String]](8)
Expand All @@ -42,12 +42,11 @@ class Terminal(root: HTMLElement, options: XtermJsOptions)
}

/** Block reading a Javascript keycode */
def readKey(): Future[String] = {
def readKey(): Future[String] =
val promise = Promise[String]()
keyBuffer.append(promise)

promise.future
}

def flush(): Unit = ()

Expand All @@ -62,22 +61,18 @@ class Terminal(root: HTMLElement, options: XtermJsOptions)

def setDimensions(dimensions: effect.TerminalDimensions): Unit =
terminal.resize(dimensions.columns, dimensions.rows)
}
type Program[A] = Terminal ?=> A

object Terminal extends Color, Cursor, Format, Erase, Dimensions, Writer {
object Terminal extends Color, Cursor, Format, Erase, Dimensions, Writer:
def readKey(): Program[Future[String]] =
terminal ?=> terminal.readKey()

def run[A](id: String, cols: Int = 80, rows: Int = 24)(f: Program[A]): A = {
def run[A](id: String, cols: Int = 80, rows: Int = 24)(f: Program[A]): A =
val options = XtermJsOptions(cols, rows)
run(dom.document.getElementById(id).asInstanceOf[HTMLElement], options)(f)
}

def run[A](element: HTMLElement, options: XtermJsOptions)(
f: Program[A]
): A = {
): A =
val terminal = Terminal(element, options)
f(using terminal)
}
}
6 changes: 2 additions & 4 deletions core/js/src/main/scala/terminus/XtermJsOptions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@ package terminus
import scala.scalajs.js

@js.native
trait XtermJsOptions extends js.Object {
trait XtermJsOptions extends js.Object:
val cols: Int = js.native
val rows: Int = js.native
}
object XtermJsOptions {
object XtermJsOptions:
def apply(cols: Int = 80, rows: Int = 24): XtermJsOptions =
js.Dynamic.literal(cols = cols, rows = rows).asInstanceOf[XtermJsOptions]
}
6 changes: 2 additions & 4 deletions core/js/src/main/scala/terminus/XtermJsTerminal.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,14 @@ import scala.scalajs.js
import scala.scalajs.js.annotation.JSGlobal

@js.native
trait XtermKeyEvent extends js.Object {
trait XtermKeyEvent extends js.Object:
val key: String = js.native
val domEvent: KeyboardEvent = js.native
}

@js.native
@JSGlobal("Terminal")
/** Minimal definition of the Terminal type from xterm.js */
class XtermJsTerminal(@unused options: XtermJsOptions) extends js.Object {
class XtermJsTerminal(@unused options: XtermJsOptions) extends js.Object:
val onKey: js.Function1[js.Function1[XtermKeyEvent, Unit], Unit] = js.native
def open(element: dom.HTMLElement): Unit = js.native
def write(data: String): Unit = js.native
Expand All @@ -41,4 +40,3 @@ class XtermJsTerminal(@unused options: XtermJsOptions) extends js.Object {
def cols: Int = js.native
def rows: Int = js.native
def resize(cols: Int, rows: Int): Unit = js.native
}
Loading
Loading