ref: refs/heads/master polywell/manual.md -rw-r--r-- 7.8 KiB View raw
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# 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](https://gitlab.com/technomancy/bussard/blob/master/data/src/ssh) or
[platformer level editor](https://gitlab.com/technomancy/liquid-runner/blob/master/level_edit.lua).

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`.

## Usage

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

```lisp
(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:

```lisp
(fn love.keypressed [key]
  (if (= key "escape")
    (do-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:

```clojure
(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:

```lisp
(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.

### Properties

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.

```lisp
(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.