Fennel's repl is part of its API. If
you want to put a repl into your program, it can be done as easily as
loading the fennel
module and calling (fennel.repl)
from your
code.
One potential problem with this is that it will call Lua's io.read
function in order to get input to evaluate. As you may know, Lua's IO
is synchronous. What this means is that when it goes to look for
input, it will wait until the enter key is pressed, and the rest of
your program is blocked until then. In some contexts this might be
fine, but often it's not going to cut it.
This is where coroutines come in. A coroutine is like a function which
can be suspended and resumed at will. We can collect input in our
program loop into a table and send it to the repl when the enter key
is pressed. Then we tell the repl to use coroutine.yield
as the
function which reads input; that is, when you go to look for input,
suspend your coroutine and wait for someone else to resume it with the
data you're waiting for.
Here's a example of how to do that using the LÖVE game framework:
(local fennel (require :fennel))
(local input []) ; store characters as they are typed
(local buffer []) ; output that has been printed
(var incomplete? false)
;; put things into the output buffer
(fn out [xs] (icollect [_ x (ipairs xs) :into buffer] x))
;; display errors in red (love2d-specific convention for colored text)
(fn err [_errtype msg]
(each [line (msg:gmatch "([^\n]+)")]
(table.insert buffer [[0.9 0.4 0.5] line])))
;; ensure repl only has access to limited environment
(local env {: love : string : table : math : type : pcall
: tostring : tonumber : pairs : ipairs
:print #(out (icollect [_ x (ipairs [$...])] (tostring x)))})
;; put the repl inside a coroutine
(local repl (coroutine.create (partial fennel.repl)))
;; start it using the options table
(coroutine.resume repl {:readChunk coroutine.yield
:onValues out
:onError err
:env env})
(fn enter []
;; send the input to the repl
(let [input-text (table.concat (doto input (table.insert "\n")))
(_ {: stack-size}) (coroutine.resume repl input-text)]
(set incomplete? (< 0 stack-size)))
;; clear the input table afterwards
(while (next input) (table.remove input)))
(fn love.keypressed [key]
(match key
:return (enter)
:backspace (table.remove input)
:escape (love.event.quit)))
(fn love.textinput [text]
(table.insert input text))
(fn love.draw []
(let [(w h) (love.window.getMode)
fh (: (love.graphics.getFont) :getHeight)]
;; draw every line in the buffer (TODO: don't draw *everything* in buffer)
(for [i (length buffer) 1 -1]
(match (. buffer i)
line (love.graphics.print line 2 (* i (+ fh 2)))))
;; draw the input text at the bottom
(love.graphics.line 0 (- h fh 4) w (- h fh 4))
(if incomplete? ; change the prompt character
(love.graphics.print "- " 2 (- h fh 2))
(love.graphics.print "> " 2 (- h fh 2)))
(love.graphics.print (table.concat input) 15 (- h fh 2))))
You can run this by putting the above in main.fnl
and creating a
main.lua
file in the same directory containing:
require("fennel").dofile("main.fnl")
Then run love .
in that directory.
Note that for a real game using LÖVE you should use the min-fennel-love2d starter repo which includes its own repl already.