ref: d45d783ea0203b12ec0e840f480a6cc67bef741f polywell/manual.md -rw-r--r-- 7.8 KiB View raw
d45d783ePhil Hagelberg Remove CI; love2d appimage doesn't work on the CI images. 7 months ago

Polywell Manual

Polywell is at its core a library for creating programmable textual and graphical interfaces where keystrokes are bound to functions and operate on buffers.

It also ships with some code which uses this functionality to construct a text editor and repl, (in the config dir) but you can just as well replace those bits with your own code or define a new mode which defines a simulated SSH client or platformer level editor.

Polywell is in the middle of being ported from Lua to Fennel, which is why some identifiers use snake_case and some use kebab-case.


Polywell has a bunch of handlers which need to be wired into the love table; usually in your love.load function:

(set love.keyreleased polywell.handle_key_up)
(set love.keypressed polywell.handle_key)
(set love.textinput polywell.handle_textinput)

-- if you want polywell to handle mouse events too:
(set love.wheelmoved polywell.handle_wheel)
(set love.mousepressed polywell.handle_mouse_pressed)
(set love.mousereleased polywell.handle_mouse_released)
(set love.mousemoved polywell.handle_mouse_moved)
(set love.mousefocus polywell.handle_mouse_focus)

But of course you can set the love handlers to your own functions which can intercept events before they are sent to the Polywell handlers too:

(fn love.keypressed [key]
  (if (= key "escape")
    (polywell.handle_key key)))

You also will need to either set love.draw to polywell.draw or call polywell.draw from within your own drawing function.

Your love.load function will probably also want to call love.graphics.setFont and love.keyboard.setKeyRepeat, and you will also want to load the config to create modes and key bindings: (require :config)

Buffers and Modes

Every file you open in Polywell has a buffer associated with it, and you can also have buffers like *repl* which aren't backed by a file at all. You can use the polywell.open function to open a new buffer; it takes an fs argument which is used to look up buffers. This makes it easy to use Polywell in-game with a simulated filesystem, but if you're not doing that, the polywell.fs module will give you a table which works for the real filesystem. (TODO: this might deserve rethinking; Polywell was extracted from a game which assumes a virtual in-game filesystem, but this is unlikely to be used much outside that context.) The polywell.open function also takes the path as the second argument. If you are making a buffer like *console* which doesn't correspond to a file, pass nil as the first argument, and use a path which has * around the name.

Every buffer has a mode associated with it which determines how keystrokes will be interpreted. It's recommended that each mode should be its own module. In that case you can create a mode like so:

(fn complete []

{:name "clam-mode"
 :parent "edit"
 :map {"tab" complete}
 :ctrl {"n" (fn [] (editor.print "you pressed ctrl-n!"))}
 :props {:activate (fn [] (global clam true))
         :deactivate (fn [] (global clam false))}}

And then add it with this code:

(local editor (require :polywell))

(editor.add-mode (require :config.clam-mode))

The :map table contains functions to handle various key presses. The handlers for ctrl/alt combinations are kept in the :ctrl and :alt entries. Setting :parent in a mode means that if a key binding isn't defined in the key maps you've provided, it will delegate to another mode and look it up in those key maps. You probably want your :parent set to "edit" if you're making a textual mode.


Each mode has a table of properties that can contain functions or other values which override its behaviour. Buffers can also have properties set which take precedence over mode properties.

Currently these properties are used:

  • on_change: a function which is called every time a change is made.
  • read_only: a boolean indicating whether edits should be allowed; this can be sidestepped by the polywell.suppress_read_only function, which takes a function as an arg to run with read_only checks disabled.
  • wrap: every time a command is run, it is run inside the wrap function which handles bookkeeping for undo; providing your own wrap allows you to implement your own undo.
  • render_lines: a table of lines in LÖVE's print format (color, string, color, string, etc) which is used instead of the raw lines table during rendering if present; used by syntax highlighting.
  • activate: (mode property only) a function called when a mode activates.
  • deactivate: (mode property only) a function called when a mode deactivates.
  • draw: a function to render the buffer instead of the default textual rendering.
  • update: a function which is run on every tick with a dt parameter, assuming polywell.update is called from love.update.

You can get and set properties on the current buffer with polywell.get_prop and polywell.set_prop, while mode properties are defined by the :prop key in the mode module.

Reading Input

The polywell.read_line function allows you to prompt the user for a line of input. It takes a prompt and a callback function. The callback function takes the string value which was input as its first argument and a boolean indicating whether the user canceled the input as its second.

(fn go-to-line []
  (editor.read_line "Go to line: "
                    (fn [line cancel]
                      (when (not cancel)
                        (editor.go_to (tonumber line))))))

When you read input, you can also offer live feedback as you type, like when you invoke the buffer switcher with ctrl-alt-b. The read_line function takes as a third argument a properties table, and if you provide a completer function, it will be called with the input string whenever the input changes. It must return a table of possible completion matches. You can use the polywell.completion.for function to calculate completion candidates; it takes a table of possible candidates and an input string.

See editor.find-file in polywell/commands.fnl for an example of how this can be used.

Syntax Highlighting

Currently Polywell ships with syntax highlighting for both Fennel code and Lua code. You can add support for new languages, but new language syntax highlighting support is limited to defining a new set of keywords and to-EOL comment markers for the language; the colorization of strings and numbers cannot be changed, nor can new constructs be added. See config/fennel-mode.fnl for an example.

Useful Functions

Most text editing commands should be fairly self-explanatory; see config/edit-mode.fnl for a listing.

The polywell.print and polywell.write functions print to the *console* buffer unless it is run within a call to with_output_to, which accepts an alternate buffer to which to redirect output and a function to run with the redirection active. For messages that do not need to stick around, use polywell.echo which just shows at the bottom of the screen until the next command.

The polywell.point function returns the point and line number. The line number obviously tells you which line you're on, and the point tells you how far over the cursor is in that line. You can use polywell.get_line to get a specific line as a string.

Use polywell.activate_mode to change the mode for the current buffer. The polywell.change_buffer function will switch to an existing buffer if it exists and do nothing if not, while polywell.open will switch or create if it's not found. The polywell.with_current_buffer function takes a buffer name and and function to run with that buffer active; it switches back to the original buffer when that function returns.