Skip to content

Commit

Permalink
xterm.js keyboard input plus example
Browse files Browse the repository at this point in the history
  • Loading branch information
noelwelsh committed Jan 2, 2025
1 parent dbffdae commit 2f8edb1
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 4 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ 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 core/jvm/target project/target
run: mkdir -p unidocs/target core/native/target core/js/target examples/js/target core/jvm/target examples/jvm/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 core/jvm/target project/target
run: tar cf targets.tar unidocs/target core/native/target core/js/target examples/js/target core/jvm/target examples/jvm/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
Expand Down
26 changes: 24 additions & 2 deletions core/js/src/main/scala/terminus/Terminal.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@

package terminus

import org.scalajs.dom
import org.scalajs.dom.HTMLElement
import org.scalajs.dom.document

import scala.collection.mutable
import scala.concurrent.Future
import scala.concurrent.Promise

class Terminal(root: HTMLElement, options: XtermJsOptions)
extends effect.Color[Terminal],
Expand All @@ -26,8 +30,23 @@ class Terminal(root: HTMLElement, options: XtermJsOptions)
effect.Erase,
effect.Writer {

private val keyBuffer: mutable.ArrayDeque[Promise[String]] =
new mutable.ArrayDeque[Promise[String]](8)

private val terminal = new XtermJsTerminal(options)
terminal.open(root)
terminal.onKey { (event: XtermKeyEvent) =>
keyBuffer.removeHead().success(event.domEvent.key)
()
}

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

promise.future
}

def flush(): Unit = ()

Expand All @@ -40,9 +59,12 @@ class Terminal(root: HTMLElement, options: XtermJsOptions)
type Program[A] = Terminal ?=> A

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

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

def run[A](element: HTMLElement, options: XtermJsOptions)(
Expand Down
16 changes: 16 additions & 0 deletions core/js/src/main/scala/terminus/XtermJsOptions.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2024 Creative Scala
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package terminus

import scala.scalajs.js
Expand Down
8 changes: 8 additions & 0 deletions core/js/src/main/scala/terminus/XtermJsTerminal.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,19 @@ import org.scalajs.dom
import scala.annotation.unused
import scala.scalajs.js
import scala.scalajs.js.annotation.JSGlobal
import org.scalajs.dom.KeyboardEvent

@js.native
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 {
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
}
1 change: 1 addition & 0 deletions docs/src/pages/directory.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
laika.navigationOrder = [
README.md
examples.md
jvm.md
js.md
]
4 changes: 4 additions & 0 deletions docs/src/pages/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Examples

@:doodle("prompt", "Prompt.go")

74 changes: 74 additions & 0 deletions examples/js/src/main/scala/terminus/examples/Prompt.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package terminus.examples

import terminus.*
import scala.scalajs.js.annotation.*
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

@JSExportTopLevel("Prompt")
object Prompt {
enum KeyCode {
case Down
case Up
case Enter
}

// Clear the text we've written
def clear(): Program[Unit] = {
Terminal.cursor.move(1, -4)
Terminal.erase.down()
Terminal.cursor.column(1)
}

// Write an option the user can choose. The currently selected option is highlighted.
def writeChoice(description: String, selected: Boolean): Program[Unit] =
if selected then
Terminal.display.bold(Terminal.write(s"> ${description}\r\n"))
else Terminal.write(s" ${description}\r\n")

// Write the UI
def write(selected: Int): Program[Unit] = {
Terminal.write("How cool is this?\r\n")
writeChoice("Very cool", selected == 0)
writeChoice("Way cool", selected == 1)
writeChoice("So cool", selected == 2)
Terminal.flush()
}

// Convert Javascript key to KeyCode
// See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
def read(): Program[Future[KeyCode]] = {
Terminal.readKey().flatMap( keyCode =>
keyCode match {
case "Enter" => Future.successful(KeyCode.Enter)
case "ArrowDown" => Future.successful(KeyCode.Down)
case "ArrowUp" => Future.successful(KeyCode.Up)
case other => read()
}
)
}

def loop(idx: Int): Program[Future[Int]] = {
write(idx)
read().flatMap(keyCode =>
keyCode match {
case KeyCode.Up =>
clear()
loop(if idx == 0 then 2 else idx - 1)

case KeyCode.Down =>
clear()
loop(if idx == 2 then 0 else idx + 1)

case KeyCode.Enter => Future.successful(idx)
}
)
}
@JSExport
def go(id: String) =
Terminal.run(id, rows = 16) {
loop(0).map(idx =>
Terminal.write(s"You selected $idx")
)
}
}

0 comments on commit 2f8edb1

Please sign in to comment.