Do not make a face for every pixel color.
[pico8.git] / pico8.el
1 ;;; pico8.el --- major mode for editing PICO-8 cartridges
2 ;;
3 ;; Author: Joe Wreschnig <joe.wreschnig@gmail.com>
4 ;; Package-Version: 20170620
5 ;; Package-Requires: ((emacs "25") (polymode "20170307") (lua-mode "20151025") (dash "2.12.0"))
6 ;; URL: https://git.korewanetadesu.com/pico8.git
7 ;; Keywords: convenience
8 ;;
9 ;; This program is free software; you can redistribute it and/or
10 ;; modify it under the terms of the GNU General Public License
11 ;; as published by the Free Software Foundation; either version 3
12 ;; of the License, or (at your option) any later version.
13
14 ;;; Commentary:
15 ;;
16 ;; This mode (ab)uses polymode to fit six modes into one buffer, one
17 ;; of which is "real Lua" text and the other five of which have
18 ;; diverse strict formatting requirements.
19 ;;
20 ;; It provides keybindings and commands for inter-mode actions.
21
22 ;;; Code:
23
24 (require 'polymode)
25 (require 'lua-mode)
26 (require 'thingatpt)
27 (require 'dash)
28
29
30 (defgroup pico8 nil
31 "Support for PICO-8 (.p8) cartridge files."
32 :tag "PICO-8"
33 :group 'languages)
34
35 (defcustom pico8-executable-paths
36 '("/Applications/PICO-8.app/Contents/MacOS/pico8" ; macOS
37 "/usr/lib/pico8/pico8" ; PocketCHIP
38 "pico8") ; Normal systems
39 "The locations to search for the PICO-8 executable."
40 :group 'pico8
41 :tag "PICO-8 Executable Paths"
42 :type '(repeat string))
43
44 (defcustom pico8-preserve-output-on-exit nil
45 "Whether to keep the output buffer when PICO-8 exits.
46
47 PICO-8 processes are long-lived with little surprising output, so
48 their output buffers are killed by default when they exit.
49 However, this is not usual behavior in Emacs, and can be
50 disabled by setting this to t."
51 :group 'pico8
52 :tag "Preserve PICO-8 Output On Exit"
53 :type 'boolean)
54
55 (defcustom pico8-lua-indent-level 1
56 "Default indentation for PICO-8 Lua mode.
57
58 This overrides `lua-indent-level' in `pico8-lua-mode'.
59 `lua-mode''s default indentation is 3, which is both
60 idiosyncratic and quite large when viewed in the PICO-8 editor,
61 where the convention is 1."
62 :group 'pico8
63 :tag "PICO-8 Lua Indent Level")
64
65 (defconst pico8-colors
66 (mapcar #'symbol-value
67 (list (defconst pico8-color-0 "#000000")
68 (defconst pico8-color-1 "#1D2B53")
69 (defconst pico8-color-2 "#7E2553")
70 (defconst pico8-color-3 "#008751")
71 (defconst pico8-color-4 "#AB5236")
72 (defconst pico8-color-5 "#5F574F")
73 (defconst pico8-color-6 "#C2C3C7")
74 (defconst pico8-color-7 "#FFF1E8")
75 (defconst pico8-color-8 "#FF004D")
76 (defconst pico8-color-9 "#FFA300")
77 (defconst pico8-color-a "#FFEC27")
78 (defconst pico8-color-b "#00E436")
79 (defconst pico8-color-c "#29ADFF")
80 (defconst pico8-color-d "#83769C")
81 (defconst pico8-color-e "#FF77A8")
82 (defconst pico8-color-f "#FFCCAA"))))
83
84 (defconst pico8-data-characters
85 (append "0123456789abcdef" nil))
86
87 (defface pico8-data
88 '((t (:inherit fixed-pitch)))
89 "Face for PICO-8 binary data.")
90
91 (define-derived-mode pico8-data-mode fundamental-mode
92 "PICO-8 Data"
93 "Major mode for non-text PICO-8 cartridge sections.
94
95 This is an 'abstract' mode and should not be used
96 as an actual major mode, only to derive new modes.")
97
98 (defun pico8-data-self-insert-command (n)
99 "Insert the data character you type N times.
100
101 PICO-8 cartridges represent binary data using fixed-length
102 strings of 0-f, one character per nybble. This command will only
103 insert the typed character if it is one of these characters,
104 overwriting one of these characters."
105 (interactive "P")
106 (when (memq (char-after) pico8-data-characters)
107 (let ((overwrite-mode 'overwrite-mode-textual))
108 (self-insert-command (prefix-numeric-value n)))))
109
110 (let ((map pico8-data-mode-map))
111 (suppress-keymap map)
112 (dolist (c (mapcar #'char-to-string pico8-data-characters))
113 (define-key map c 'pico8-data-self-insert-command)))
114
115 (defun pico8-goto-char (position)
116 "Set point to POSITION, a number.
117
118 The position is global to the cartridge, and the buffer is
119 widened if necessary to reach it."
120 (unless (<= (point-min) position (point-max))
121 (widen))
122 (goto-char position))
123 \f
124 (defun pico8-executable ()
125 "Look up the PICO-8 executable."
126 (or (car (delete nil (mapcar 'executable-find pico8-executable-paths)))
127 (error "The PICO-8 executable could not be found.
128 Make sure it is installed, and present in `pico8-executable-paths'")))
129
130 (defun pico8--create-output-buffer ()
131 "Create and return a buffer for PICO-8 output."
132 (let* ((output-name (generate-new-buffer-name "*PICO-8*"))
133 (output (get-buffer-create output-name)))
134 (display-buffer output)
135 (with-current-buffer output
136 (insert "(Use C-r within PICO-8 to reload changes from Emacs.)\n")
137 ;; Setting the point in the buffer doesn't have a lasting
138 ;; effect, we need to change it in the window it opened in.
139 ;; https://emacs.stackexchange.com/questions/21464/
140 (set-window-point (get-buffer-window output) (point-max))
141 (compilation-mode))
142 output))
143
144 (defun pico8--process-sentinel (process signal)
145 "Delete buffers and windows for PROCESS if SIGNAL is exit."
146 (when (and (not pico8-preserve-output-on-exit)
147 (process-buffer process)
148 (memq (process-status process) '(exit signal)))
149 (let ((buffer (process-buffer process)))
150 (dolist (window (get-buffer-window-list buffer))
151 (quit-restore-window window))
152 (kill-buffer buffer))))
153
154 (defun pico8--execute (&rest params)
155 "Run PICO-8 with the provided PARAMS after saving etc."
156 (let ((pico8 (pico8-executable))
157 (cartridge-file-name (buffer-file-name (buffer-base-buffer))))
158 (when (and (buffer-modified-p)
159 (y-or-n-p (format "Save %s? " cartridge-file-name)))
160 (save-buffer))
161 (let* ((output (pico8--create-output-buffer))
162 (process-args (append params (list cartridge-file-name)))
163 (process (apply #'start-file-process "PICO-8" output pico8
164 process-args)))
165 (set-process-sentinel process 'pico8--process-sentinel))))
166
167 (defun pico8-run-cartridge ()
168 "Run the current PICO-8 cartridge."
169 (interactive)
170 (pico8--execute "-run"))
171
172 (defun pico8-load-cartridge ()
173 "Load the current cartridge in PICO-8."
174 (interactive)
175 (pico8--execute))
176
177
178 (defun pico8-cartridge-section-header (name)
179 "Return the header string for cartridge section NAME."
180 (format "__%s__" name))
181
182 (defconst pico8-cartridge-sections
183 '("lua" "gfx" "gff" "map" "sfx" "music"))
184
185 (defconst pico8-cartridge-keywords
186 (mapcar #'pico8-cartridge-section-header pico8-cartridge-sections))
187
188 (defconst pico8-cartridge-header
189 "pico-8 cartridge // http://www.pico-8.com\nversion [0-9]+")
190
191 (define-derived-mode pico8-cartridge-mode fundamental-mode
192 "Cartridge"
193 "Major mode for showing PICO-8 cartridge structure."
194 (font-lock-add-keywords
195 nil (list (regexp-opt pico8-cartridge-keywords 'symbols)
196 (cons pico8-cartridge-header font-lock-comment-face)))
197 (suppress-keymap pico8-cartridge-mode-map))
198
199 (defun pico8-cartridge-point-of-section (name)
200 "Find the point where the data for section NAME begins."
201 (save-restriction
202 (widen)
203 (save-excursion
204 (goto-char (point-min))
205 (let ((token (format "^%s\n" (pico8-cartridge-section-header name))))
206 (if (not (or (re-search-forward token nil t)
207 (re-search-forward token nil t)))
208 (error "Unable to find a %s section in current buffer" name)))
209 (point))))
210
211 (defun pico8-cartridge-goto-section (name)
212 "Go to the beginning of section NAME."
213 (interactive "sGoto PICO-8 cartridge section: ")
214 (pico8-goto-char (pico8-cartridge-point-of-section name)))
215
216 (defmacro pico8-cartridge-with-section (section &rest body)
217 "Go to and narrow SECTION and evaluate BODY there."
218 `(save-restriction
219 (save-excursion
220 (pico8-cartridge-goto-section ,section)
221 (pm-narrow-to-span)
222 ,@body)))
223 \f
224 (defconst pico8-lua-builtins
225 (regexp-opt
226 '("abs" "add" "all" "atan2" "btn" "btnp" "camera"
227 "cartdata" "circ" "circfill" "clip" "cls" "cocreate"
228 "coresume" "costatus" "color" "cos" "cstore" "cursor"
229 "del" "dget" "dset" "fget" "flip" "flr" "folder" "foreach"
230 "fset" "info" "line" "load" "ls" "map" "max" "memcpy"
231 "memset" "menuitem" "mget" "mid" "min" "mset" "music"
232 "pairs" "pal" "palt" "peek" "pget" "poke" "print"
233 "printh" "pset" "reboot" "rect" "rectfill" "reload"
234 "resume" "rnd" "run" "save" "sfx" "sget" "sin" "spr"
235 "sqrt" "srand" "sset" "sspr" "stat") 'symbols))
236
237 (define-derived-mode pico8-lua-mode lua-mode
238 "Lua"
239 "Major mode for editing Lua code in PICO-8 cartridges."
240 (font-lock-add-keywords
241 nil `((,pico8-lua-builtins . font-lock-builtin-face)))
242 (set (make-local-variable 'lua-indent-level) pico8-lua-indent-level))
243
244 \f
245 (defun pico8-gff-current-position ()
246 "Calculate the flag position of the cursor."
247 (pm-with-narrowed-to-span (pm-get-innermost-span)
248 (let ((row (1- (line-number-at-pos)))
249 (col (min 255 (current-column))))
250 (+ (/ col 2) (* row 128)))))
251
252 (defun pico8-gff-lighter ()
253 "Calculate the flag under the cursor."
254 (pm-with-narrowed-to-span (pm-get-innermost-span)
255 (let ((row (1- (line-number-at-pos)))
256 (col (current-column)))
257 (+ (* 128 row) (/ col 2)))))
258
259 (define-derived-mode pico8-gff-mode pico8-data-mode
260 '(:eval (format "Flag[%d]" (pico8-gff-lighter)))
261 "Major mode for editing flags in PICO-8 cartridges.")
262
263 (defun pico8-gff-offset-of-flag (flag)
264 "Calculate the offset of of flag number FLAG."
265 (unless (<= 0 flag 255)
266 (error "Valid flag numbers are 0 to 255, inclusive"))
267 (+ (* 2 flag) (if (> flag 128) 1 0)))
268 \f
269 (defface pico8-pixel
270 '((t (:inherit pico8-data :height 100)))
271 "Face for PICO-8 sprite “pixels.”"
272 :group 'pico8)
273
274 (defconst pico8-gfx-font-lock-keywords
275 (cons
276 ;; If the \n isn't in the smaller face the line is taller to
277 ;; accommodate the full sized point at the end-of-line.
278 '("\n" . 'pico8-pixel)
279 (-map-indexed
280 (lambda (i c)
281 `(,(format "%x+" i)
282 0 '(:inherit pico8-pixel :foreground ,c)))
283 pico8-colors)))
284
285
286 (defun pico8-gfx-current-position ()
287 "Calculate the sprite and in-sprite position of the cursor."
288 ;; FIXME: Ensure the span we got was actually the gfx one.
289 (pm-with-narrowed-to-span (pm-get-innermost-span)
290 (let ((row (1- (line-number-at-pos)))
291 (col (min 127 (current-column))))
292 (list (+ (* 16 (/ row 8)) (/ col 8))
293 (% col 8) (% row 8)))))
294
295 (defun pico8-forward-sprite (n)
296 "Move the point N sprites forward (backward if N is negative)."
297 (interactive "P")
298 (let* ((n (prefix-numeric-value n))
299 (current (pico8-gfx-current-position))
300 (offset (pico8-gfx-offset-of-sprite
301 (% (+ 256 n (nth 0 current)) 256)
302 (nth 1 current)
303 (nth 2 current))))
304 (pm-with-narrowed-to-span (pm-get-innermost-span)
305 (goto-char (+ (point-min) offset)))))
306
307 (defun pico8-backward-sprite (n)
308 "Move the point N sprites backward (forward if N is negative)."
309 (interactive "P")
310 (pico8-forward-sprite (- (prefix-numeric-value n))))
311
312 (defun pico8-gfx-lighter ()
313 "Show a short description of the current sprite position."
314 (let ((current (pico8-gfx-current-position)))
315 (if current (apply #'format (cons "Sprite[%d:%d,%d]" current))
316 "Sprite[-]")))
317
318 (define-derived-mode pico8-gfx-mode pico8-data-mode
319 '(:eval (pico8-gfx-lighter))
320 "Major mode for editing sprites in PICO-8 cartridges."
321 (font-lock-add-keywords nil pico8-gfx-font-lock-keywords)
322 (read-only-mode t))
323
324 (defun pico8-gfx-offset-of-sprite (sprite &optional x y)
325 "Calculate the point of SPRITE's X,Y pixel (0,0 by default)."
326 (let ((x (or x 0))
327 (y (or y 0))
328 (line (* (/ sprite 16) 8))
329 (row (* (% sprite 16) 8)))
330 ;; A limit of 4x4 is kind of arbitrary but if you're using sprites
331 ;; larger than that you probably aren't going to be doing so in a
332 ;; way that this command is useful anyway.
333 (unless (and (<= 0 x 31) (<= 0 y 31))
334 (error "Valid sprite offsets are 0 to 31, inclusive"))
335 (unless (<= 0 sprite 255)
336 (error "Valid sprite numbers are 0 to 255, inclusive"))
337 (+ (* 129 (+ y line)) row x)))
338
339 (define-key pico8-gfx-mode-map "q" 'pico8-backward-sprite)
340 (define-key pico8-gfx-mode-map "w" 'pico8-forward-sprite)
341
342 (defface pico8-map-tile
343 '((t (:inherit pico8-data :height 100)))
344 "Face for PICO-8 map 'tiles'."
345 :group 'pico8-faces)
346
347 (defconst pico8-map-font-lock-keywords
348 '(("[0-9a-f]+" . 'pico8-map-tile)
349 ("\n" . 'pico8-map-tile)))
350
351 (defun pico8-map-lighter ()
352 "Calculate the map tile under the cursor."
353 (pm-with-narrowed-to-span (pm-get-innermost-span)
354 (let ((row (- (line-number-at-pos) 1))
355 (col (current-column)))
356 ;; TODO: Show sprite number and flags value
357 (format "%d,%d" (/ col 2) row))))
358
359 (define-derived-mode pico8-map-mode pico8-data-mode
360 '(:eval (format "Map[%s]" (pico8-map-lighter)))
361 "Major mode for editing map data in PICO-8 cartridges."
362 (setq font-lock-defaults '(pico8-map-font-lock-keywords)))
363 \f
364 (defun pico8-sfx-lighter ()
365 "Calculate the sound effect under the cursor."
366 (pm-with-narrowed-to-span (pm-get-innermost-span)
367 (let ((row (- (line-number-at-pos) 1)))
368 (format "%d" row))))
369
370 (define-derived-mode pico8-sfx-mode pico8-data-mode
371 '(:eval (format "Sound[%s]" (pico8-sfx-lighter)))
372 "Major mode for editing sound data in PICO-8 cartridges.")
373 \f
374 (defun pico8-music-lighter ()
375 "Calculate the map tile under the cursor."
376 (pm-with-narrowed-to-span (pm-get-innermost-span)
377 (let ((row (- (line-number-at-pos) 1)))
378 (format "%d" row))))
379
380 (define-derived-mode pico8-music-mode pico8-data-mode
381 '(:eval (format "Pattern[%s]" (pico8-music-lighter)))
382 "Major mode for editing music data in PICO-8 cartridges.")
383 \f
384 (defun pico8-goto-sprite (sprite &optional x y)
385 "Set point to the top-left pixel of SPRITE (or the X,Y pixel)."
386 (interactive "nGo to sprite [0-255]: ")
387 (let ((base (pico8-cartridge-point-of-section "gfx"))
388 (offset (pico8-gfx-offset-of-sprite sprite x y)))
389 (pico8-goto-char (+ base offset))))
390
391 (defun pico8-goto-flag (flag)
392 "Set point to the start of flag number FLAG."
393 (interactive "nGo to flag [0-255]: ")
394 (let ((base (pico8-cartridge-point-of-section "gff"))
395 (offset (pico8-gff-offset-of-flag flag)))
396 (pico8-goto-char (+ base offset))))
397
398 (defun pico8--string-to-number (string)
399 "Convert STRING to a number, guessing the base.
400
401 Returns nil, not 0, if the string was not converted."
402 (cond ((string-match-p "^0[xX][0-9a-fA-F]+$" string)
403 (string-to-number (substring string 2) 16))
404 ((string-match-p "^0[0-9]+$" string)
405 (string-to-number (substring string 1) 8))
406 ((string-match-p "^[0-9]+$" string)
407 (string-to-number string 10))
408 (t nil)))
409
410 (defun pico8-sprite-relevant-to-point ()
411 "Get the sprite number relevant to the point.
412
413 When editing a flag, this is the flag number. When editing a
414 map, this is the value at the map. When editing Lua code,
415 this is the numeric literal in the code."
416 (cond
417 ((derived-mode-p 'pico8-gff-mode) (pico8-gff-current-position))
418
419 ;; The sprite for a map is the data at the map location, which is
420 ;; to say, the hexadecimal interpretation of the two character
421 ;; string beginning at the previous even column.
422 ((derived-mode-p 'pico8-map-mode)
423 (let ((beg (- (point) (% (min 255 (current-column)) 2))))
424 (string-to-number (buffer-substring beg (+ beg 2)) 16)))
425
426 ;; In Lua, the sprite is a numeric literal.
427 ((derived-mode-p 'lua-mode)
428 (pico8--string-to-number (or (word-at-point) "")))
429
430 (t nil)))
431
432
433 (defun pico8-goto-sprite-relevant-to-point ()
434 "Go to the sprite number relevant to the text at the point.
435
436 When editing a flag, this is the flag number. When editing a
437 map, this is the value at the map. When editing Lua code,
438 this is the numeric literal in the code."
439 (interactive)
440 (let ((sprite (pico8-sprite-relevant-to-point)))
441 (if sprite (pico8-goto-sprite sprite)
442 (error "No sprite number was found at the point"))))
443
444
445 (defun pico8-goto-thing-relevant-to-point ()
446 "Go to the thing relevant to the text at the point.
447
448 When editing a flag or map, this is the corresponding sprite.
449 When editing a sprite, this is the corresponding flag. When
450 editing Lua code, how lucky are you feeling?
451
452 This function needs a lot of work.."
453 (interactive)
454 (cond ((derived-mode-p 'pico8-gfx-mode)
455 (pico8-goto-flag (car (pico8-gfx-current-position))))
456
457 ((or (derived-mode-p 'pico8-gff-mode)
458 (derived-mode-p 'pico8-map-mode))
459 (pico8-goto-sprite-relevant-to-point))
460
461 ((derived-mode-p 'lua-mode)
462 (let ((n (or (pico8--string-to-number (word-at-point)) 0))
463 (c (save-excursion
464 (and (re-search-backward "\\<f[gs]et\\|s?spr\\>"
465 (- (point) 30) t)
466 (char-after)))))
467 (cond ((= c ?f) (pico8-goto-flag n))
468 ((= c ?s) (pico8-goto-sprite n))
469 (t (error "There's nothing obvious to go to")))))
470
471 (t (error "There's no obvious thing to go to"))))
472 \f
473 ;;;
474 ;; Flycheck Integration
475 ;;
476 ;; This is more or less the same as the default Lua checkers, but we
477 ;; need to write out a temporary file so it doesn't check the non-Lua
478 ;; parts of the file.
479
480 (defun pico8--lua-only (f &rest args)
481 "If this is a PICO-8 buffer, run F(ARGS) on only the current section."
482 (if (derived-mode-p 'pico8-lua-mode)
483 (pico8-cartridge-with-section "lua"
484 (let ((mainbuf (current-buffer))
485 (from (point-min))
486 (to (point-max)))
487 (with-temp-buffer
488 (insert "\n\n\n") ;; match line number with stripped header
489 (insert-buffer-substring mainbuf from to)
490 (insert " -- start __gfx__") ;; otherwise last line is ignored
491 (apply f args))))
492 (apply f args)))
493
494 (defconst pico8--lua-luacheckrc
495 (expand-file-name
496 "pico8.luacheckrc"
497 (file-name-directory (or load-file-name buffer-file-name))))
498
499 (eval-when-compile
500 (require 'flycheck))
501
502 (with-eval-after-load 'flycheck
503 (advice-add 'flycheck-save-buffer-to-file :around #'pico8--lua-only)
504
505 (flycheck-define-checker pico8-lua
506 "A PICO-8 Lua syntax checker using the Lua compiler.
507 See URL `http://www.lua.org/'."
508 :command ("luac" "-p" source)
509 :standard-input nil
510 :error-patterns
511 ((error line-start
512 ;; Skip the name of the luac executable.
513 (minimal-match (zero-or-more not-newline))
514 ":" line ": " (message) line-end))
515 :modes pico8-lua-mode)
516
517 (flycheck-define-checker pico8-luacheck
518 "A PICO-8 Lua syntax checker using luacheck.
519 See URL `https://github.com/mpeterv/luacheck'."
520 :command ("luacheck"
521 "--formatter" "plain"
522 "--codes" ; Show warning codes
523 "--no-color"
524 (option "--config" pico8--lua-luacheckrc)
525 "--filename" source-original
526 source)
527 :error-patterns
528 ((warning line-start
529 (optional (file-name))
530 ":" line ":" column
531 ": (" (id "W" (one-or-more digit)) ") "
532 (message) line-end)
533 (error line-start
534 (optional (file-name))
535 ":" line ":" column ":"
536 ;; `luacheck' before 0.11.0 did not output codes for errors, hence
537 ;; the ID is optional here
538 (optional " (" (id "E" (one-or-more digit)) ") ")
539 (message) line-end))
540 :modes pico8-lua-mode)
541
542 (add-to-list 'flycheck-checkers 'pico8-lua)
543 (add-to-list 'flycheck-checkers 'pico8-luacheck))
544
545
546 ;;;
547 ;; Finally - pico8-mode!
548
549 (defmacro pico8--defchunkmode (name)
550 "Define a PICO-8 polymode chunk for section NAME."
551 `(defconst ,(intern (concat "pico8--pm-inner-" name))
552 (pm-hbtchunkmode :mode ',(intern (format "pico8-%s-mode" name))
553 :head-mode 'host
554 :head-reg ,(format "^__%s__\n" name)
555 :tail-reg "^__[a-z]\\{3,5\\}__\n\\|^\n\\'")))
556
557 (defconst pico8--pm-poly
558 (pm-polymode-multi
559 :hostmode
560 (defconst pico8--pm-host
561 (pm-bchunkmode :mode 'pico8-cartridge-mode))
562 :innermodes (list
563 (pico8--defchunkmode "lua")
564 (pico8--defchunkmode "gfx")
565 (pico8--defchunkmode "gff")
566 (pico8--defchunkmode "map")
567 (pico8--defchunkmode "sfx")
568 (pico8--defchunkmode "music"))))
569
570 (define-polymode pico8-mode pico8--pm-poly
571 :lighter "P8"
572 :keymap '(("\C-c\C-r" . pico8-run-cartridge)
573 ("\C-c\C-e" . pico8-load-cartridge)
574 ("\M-gs" . pico8-goto-sprite)
575 ("\M-gS" . pico8-goto-sprite-relevant-to-point)
576 ("\M-gf" . pico8-goto-flag)
577 ("\M-g." . pico8-goto-thing-relevant-to-point))
578 (toggle-truncate-lines 1))
579
580 (add-to-list 'auto-mode-alist '("\\.p8$" . pico8-mode))
581 (add-to-list 'magic-mode-alist '("pico-8 cartridge" . pico8-mode))
582
583
584 (provide 'pico8)
585 ;;; pico8.el ends here