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