Skip to content

Commit

Permalink
Merge pull request #1 from lukewilliamboswell/add-tui
Browse files Browse the repository at this point in the history
Updates and add TUI
  • Loading branch information
lukewilliamboswell authored Jan 28, 2024
2 parents 29437fe + 2698af0 commit d50db17
Show file tree
Hide file tree
Showing 12 changed files with 943 additions and 135 deletions.
8 changes: 1 addition & 7 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,4 @@ jobs:

- run: ./roc_nightly/roc version

- run: sudo apt install -y expect
# expect for testing

- run: expect -v

# Run all tests
- run: ./ci/all_tests.sh
- run: ROC=./roc_nightly/roc ./ci/all_tests.sh
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
examples/colors
examples/animals
examples/tui-menu

generated-docs/
generated-docs/

format.sh
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,20 @@

Helpers for working with terminal escapes

## Examples
## Example - Colors

Run with `roc run examples/colors.roc`

![example output showing colors](example.png)
![example output showing colors](examples/colors.png)

## Example - TUI Menu

Run with `roc run examples/tui-menu.roc`

![example output showing colors](examples/tui-menu.gif)

## Documentation

See [https://lukewilliamboswell.github.io/roc-ansi-escapes/](https://lukewilliamboswell.github.io/roc-ansi-escapes/)

To generate locally use `roc docs package/main.roc`
30 changes: 16 additions & 14 deletions ci/all_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,28 @@
# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
set -euxo pipefail

roc='./roc_nightly/roc'
if [ -z "${ROC}" ]; then
echo "ERROR: The ROC environment variable is not set.
Set it to something like:
/home/username/Downloads/roc_nightly-linux_x86_64-2023-10-30-cb00cfb/roc
or
/home/username/gitrepos/roc/target/build/release/roc" >&2

examples_dir='./examples/'
exit 1
fi

EXAMPLES_DIR='./examples'
PACKAGE_DIR='./package'

# roc check
for roc_file in $examples_dir*.roc; do
$roc check $roc_file
for ROC_FILE in $EXAMPLES_DIR/*.roc; do
$ROC check $ROC_FILE
done

# roc build
for roc_file in $examples_dir*.roc; do
$roc build $roc_file --linker=legacy
for ROC_FILE in $EXAMPLES_DIR/*.roc; do
$ROC build $ROC_FILE --linker=legacy
done

# check output
# for roc_file in $examples_dir*.roc; do
# roc_file_only="$(basename "$roc_file")"
# no_ext_name=${roc_file_only%.*}
# expect ci/expect_scripts/$no_ext_name.exp
# done

# test building docs website
$roc docs package/main.roc
$ROC docs $PACKAGE_DIR/main.roc
12 changes: 6 additions & 6 deletions examples/animals.roc
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
app "example"
packages {
cli: "https://github.com/roc-lang/basic-cli/releases/download/0.5.0/Cufzl36_SnJ4QbOoEmiJ5dIpUxBvdB3NEySvuH82Wio.tar.br",
pkg: "https://github.com/lukewilliamboswell/roc-ansi-escapes/releases/download/0.1.1/cPHdNPNh8bjOrlOgfSaGBJDz6VleQwsPdW0LJK6dbGQ.tar.br",
cli: "https://github.com/roc-lang/basic-cli/releases/download/0.8.1/x8URkvfyi9I0QhmVG98roKBUs_AZRkLFwFJVJ3942YA.tar.br",
ansi: "../package/main.roc",
}
imports [cli.Stdout, pkg.Color.{ fg }]
imports [cli.Stdout, ansi.Core]
provides [main] to cli

main =
[
"The ",
"GREEN" |> Color.fg Green,
"GREEN" |> Core.withFg Green,
" frog, the ",
"BLUE" |> Color.fg Blue,
"BLUE" |> Core.withFg Blue,
" bird, and the ",
"RED" |> Color.fg Red,
"RED" |> Core.withFg Red,
" ant shared a leaf.",
]
|> Str.joinWith ""
Expand Down
File renamed without changes
39 changes: 18 additions & 21 deletions examples/colors.roc
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
app "example"
packages {
cli: "https://github.com/roc-lang/basic-cli/releases/download/0.5.0/Cufzl36_SnJ4QbOoEmiJ5dIpUxBvdB3NEySvuH82Wio.tar.br",
pkg: "../package/main.roc",
cli: "https://github.com/roc-lang/basic-cli/releases/download/0.8.1/x8URkvfyi9I0QhmVG98roKBUs_AZRkLFwFJVJ3942YA.tar.br",
ansi: "../package/main.roc",
}
imports [
cli.Stdout,
pkg.Color,
]
imports [cli.Stdout, ansi.Core]
provides [main] to cli

main =
[
"Red fg" |> Color.fg Red,
"Green fg" |> Color.fg Green,
"Blue fg" |> Color.fg Blue,
"Red bg" |> Color.bg Red,
"Green bg" |> Color.bg Green,
"Blue bg" |> Color.bg Blue,
"{ fg: BrightRed, bg: Black}" |> Color.with { fg: BrightRed, bg: Black},
"{ fg: Green, bg: Red}" |> Color.with { fg: Green, bg: Red},
"{ fg: BrightYellow, bg: Green}" |> Color.with { fg: BrightYellow, bg: Green},
"{ fg: BrightBlue, bg: BrightYellow}" |> Color.with { fg: BrightBlue, bg: BrightYellow},
"{ fg: BrightMagenta, bg: BrightBlue}" |> Color.with { fg: BrightMagenta, bg: BrightBlue},
"{ fg: Cyan, bg: BrightMagenta}" |> Color.with { fg: Cyan, bg: BrightMagenta},
"{ fg: BrightWhite, bg: Cyan}" |> Color.with { fg: BrightWhite, bg: Cyan},
"{ fg: Default, bg: Default}" |> Color.with { fg: Default, bg: Default},
"Red fg" |> Core.withFg Red,
"Green fg" |> Core.withFg Green,
"Blue fg" |> Core.withFg Blue,
"Red bg" |> Core.withBg Red,
"Green bg" |> Core.withBg Green,
"Blue bg" |> Core.withBg Blue,
"{ fg: BrightRed, bg: Black}" |> Core.withColor { fg: BrightRed, bg: Black },
"{ fg: Green, bg: Red}" |> Core.withColor { fg: Green, bg: Red },
"{ fg: BrightYellow, bg: Green}" |> Core.withColor { fg: BrightYellow, bg: Green },
"{ fg: BrightBlue, bg: BrightYellow}" |> Core.withColor { fg: BrightBlue, bg: BrightYellow },
"{ fg: BrightMagenta, bg: BrightBlue}" |> Core.withColor { fg: BrightMagenta, bg: BrightBlue },
"{ fg: Cyan, bg: BrightMagenta}" |> Core.withColor { fg: Cyan, bg: BrightMagenta },
"{ fg: BrightWhite, bg: Cyan}" |> Core.withColor { fg: BrightWhite, bg: Cyan },
"{ fg: Default, bg: Default}" |> Core.withColor { fg: Default, bg: Default },
]
|> Str.joinWith "\n"
|> Stdout.line
|> Stdout.line
Binary file added examples/tui-menu.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
214 changes: 214 additions & 0 deletions examples/tui-menu.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
app "tui-menu"
packages {
pf: "https://github.com/roc-lang/basic-cli/releases/download/0.7.0/bkGby8jb0tmZYsy2hg1E_B2QrCgcSTxdUlHtETwm5m4.tar.br",
ansi: "../package/main.roc",
}
imports [
pf.Stdout,
pf.Stdin,
pf.Tty,
pf.Task.{ Task },
ansi.Core.{ Color, Input, ScreenSize, Position, DrawFn },
pf.Utc.{ Utc },
]
provides [main] to pf

Model : {
screen : ScreenSize,
cursor : Position,
prevDraw : Utc,
currDraw : Utc,
things : List Str,
inputs : List Input,
debug : Bool,
state : [HomePage, ConfirmPage Str, DoSomething Str, UserExited],
}

init : Model
init = {
cursor: { row: 3, col: 3 },
screen: { width: 0, height: 0 },
prevDraw: Utc.fromMillisSinceEpoch 0,
currDraw: Utc.fromMillisSinceEpoch 0,
things: ["Foo", "Bar", "Baz"],
inputs: List.withCapacity 1000,
debug: Bool.false,
state: HomePage,
}

render : Model -> List DrawFn
render = \state ->
# PRESS 'd' to toggle debug screen
debug = if state.debug then debugScreen state else []

when state.state is
ConfirmPage _ ->
List.join [
confirmScreen state,
debug,
]

_ ->
List.join [
homeScreen state,
debug,
]

main : Task {} *
main = runTask |> Task.onErr \_ -> Stdout.line "ERROR Something went wrong"

runTask : Task {} []
runTask =

# TUI Dashboard
{} <- Tty.enableRawMode |> Task.await
model <- Task.loop init runUILoop |> Task.await

# Restore terminal
{} <- Stdout.write (Core.toStr Reset) |> Task.await
{} <- Tty.disableRawMode |> Task.await

# EXIT or RUN selected solution
when model.state is
DoSomething selected ->
Stdout.line "Doing something with $(selected)... now exiting..."

_ ->
Stdout.line "Exiting..."

runUILoop : Model -> Task [Step Model, Done Model] []
runUILoop = \prevModel ->

# Get the time of this draw
now <- Utc.now |> Task.await

# Update screen size (in case it was resized since the last draw)
terminalSize <- getTerminalSize |> Task.await

# Update the model with screen size and time of this draw
model = { prevModel & screen: terminalSize, prevDraw: prevModel.currDraw, currDraw: now }

# Draw the screen
drawFns = render model
{} <- Core.drawScreen model drawFns |> Stdout.write |> Task.await

# Get user input
input <- Stdin.bytes |> Task.map Core.parseRawStdin |> Task.await

# Parse user input into a command
command =
when (input, model.state) is
(KeyPress Up, _) -> MoveCursor Up
(KeyPress Down, _) -> MoveCursor Down
(KeyPress Left, _) -> MoveCursor Left
(KeyPress Right, _) -> MoveCursor Right
(KeyPress LowerD, _) -> ToggleDebug
(KeyPress Enter, HomePage) -> UserToggledScreen
(KeyPress Enter, ConfirmPage s) -> UserWantToDoSomthing s
(KeyPress Escape, ConfirmPage _) -> UserToggledScreen
(KeyPress Escape, _) -> Exit
(KeyPress _, _) -> Nothing
(Unsupported _, _) -> Nothing
(CtrlC, _) -> Exit

# Update model so we can keep a history of user input
modelWithInput = { model & inputs: List.append model.inputs input }

# Action command
when command is
Nothing -> Task.ok (Step modelWithInput)
Exit -> Task.ok (Done { modelWithInput & state: UserExited })
ToggleDebug -> Task.ok (Step { modelWithInput & debug: !modelWithInput.debug })
MoveCursor direction -> Task.ok (Step (Core.updateCursor modelWithInput direction))
UserWantToDoSomthing s -> Task.ok (Done { modelWithInput & state: DoSomething s })
UserToggledScreen ->
when modelWithInput.state is
HomePage ->
result = getSelected modelWithInput

when result is
Ok selected -> Task.ok (Step { modelWithInput & state: ConfirmPage selected })
Err NothingSelected -> Task.ok (Step modelWithInput)

_ -> Task.ok (Step { modelWithInput & state: HomePage })

mapSelected : Model -> List { selected : Bool, s : Str, row : I32 }
mapSelected = \model ->
s, idx <- List.mapWithIndex model.things

row = 3 + (Num.toI32 idx)

{ selected: model.cursor.row == row, s, row }

getSelected : Model -> Result Str [NothingSelected]
getSelected = \model ->
mapSelected model
|> List.keepOks \{ selected, s } -> if selected then Ok s else Err {}
|> List.first
|> Result.mapErr \_ -> NothingSelected

getTerminalSize : Task ScreenSize []
getTerminalSize =

# Move the cursor to bottom right corner of terminal
cmd = [SetCursor { row: 999, col: 999 }, GetCursor] |> List.map Core.toStr |> Str.joinWith ""
{} <- Stdout.write cmd |> Task.await

# Read the cursor position
Stdin.bytes
|> Task.map Core.parseCursor
|> Task.map \{ row, col } -> { width: col, height: row }

homeScreen : Model -> List DrawFn
homeScreen = \model ->
[
[
Core.drawCursor { bg: Green },
Core.drawText " Choose your Thing" { r: 1, c: 1, fg: Green },
Core.drawText "RUN" { r: 2, c: 11, fg: Blue },
Core.drawText "QUIT" { r: 2, c: 26, fg: Red },
Core.drawText " ENTER TO RUN, ESCAPE TO QUIT" { r: 2, c: 1, fg: Gray },
Core.drawBox { r: 0, c: 0, w: model.screen.width, h: model.screen.height },
],
{ selected, s, row } <- model |> mapSelected |> List.map

if selected then
Core.drawText " > $(s)" { r: row, c: 2, fg: Green }
else
Core.drawText " - $(s)" { r: row, c: 2, fg: Black },
]
|> List.join

confirmScreen : Model -> List DrawFn
confirmScreen = \state -> [
Core.drawCursor { bg: Green },
Core.drawText " Would you like to do something?" { r: 1, c: 1, fg: Yellow },
Core.drawText "CONFIRM" { r: 2, c: 11, fg: Blue },
Core.drawText "RETURN" { r: 2, c: 30, fg: Red },
Core.drawText " ENTER TO CONFIRM, ESCAPE TO RETURN" { r: 2, c: 1, fg: Gray },
Core.drawText " count: TBC" { r: 3, c: 1 },
Core.drawText " speed: TBC" { r: 4, c: 1 },
Core.drawText " size: TBC" { r: 5, c: 1 },
Core.drawBox { r: 0, c: 0, w: state.screen.width, h: state.screen.height },
]

debugScreen : Model -> List DrawFn
debugScreen = \state ->
cursorStr = "CURSOR R$(Num.toStr state.cursor.row), C$(Num.toStr state.cursor.col)"
screenStr = "SCREEN H$(Num.toStr state.screen.height), W$(Num.toStr state.screen.width)"
inputDelatStr = "DELTA $(Num.toStr (Utc.deltaAsMillis state.prevDraw state.currDraw)) millis"
lastInput =
state.inputs
|> List.last
|> Result.map Core.inputToStr
|> Result.map \str -> "INPUT $(str)"
|> Result.withDefault "NO INPUT YET"

[
Core.drawText lastInput { r: state.screen.height - 5, c: 1, fg: Magenta },
Core.drawText inputDelatStr { r: state.screen.height - 4, c: 1, fg: Magenta },
Core.drawText cursorStr { r: state.screen.height - 3, c: 1, fg: Magenta },
Core.drawText screenStr { r: state.screen.height - 2, c: 1, fg: Magenta },
Core.drawVLine { r: 1, c: state.screen.width // 2, len: state.screen.height, fg: Gray },
Core.drawHLine { c: 1, r: state.screen.height // 2, len: state.screen.width, fg: Gray },
]
Loading

0 comments on commit d50db17

Please sign in to comment.