|
- """make-guide.py - build vim-user-guide.pdf (needs: pip install reportlab).
-
- python make-guide.py [out.pdf] [screenshot.png]
-
- Defaults: vim-user-guide.pdf and shots/run.png next to this script.
- The command reference is hand-written: update it when the editor's keys change.
- """
- import os, sys
- from reportlab.lib.pagesizes import letter
- from reportlab.lib import colors
- from reportlab.lib.units import inch
- from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
- from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Table,
- TableStyle, Image, PageBreak, Preformatted, KeepTogether)
-
- HERE = os.path.dirname(os.path.abspath(__file__))
- OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "vim-user-guide.pdf")
- SHOT = sys.argv[2] if len(sys.argv) > 2 else os.path.join(HERE, "shots", "run.png")
-
- ss = getSampleStyleSheet()
- BLUE = colors.HexColor("#352879") # C64 dark blue
- LIGHT = colors.HexColor("#6c5eb5") # C64 light blue
- H1 = ParagraphStyle("H1", parent=ss["Heading1"], textColor=BLUE, spaceBefore=14, spaceAfter=6)
- H2 = ParagraphStyle("H2", parent=ss["Heading2"], textColor=BLUE, spaceBefore=10, spaceAfter=4)
- B = ParagraphStyle("B", parent=ss["BodyText"], fontSize=10, leading=13.5, spaceAfter=6)
- CELL = ParagraphStyle("C", parent=B, fontSize=9, leading=11.5, spaceAfter=0)
- CELLB = ParagraphStyle("CB", parent=CELL, fontName="Helvetica-Bold", textColor=colors.white)
- CODE = ParagraphStyle("Code", fontName="Courier", fontSize=8.8, leading=11,
- backColor=colors.HexColor("#f2f1f8"), borderPadding=6,
- leftIndent=6, rightIndent=6, spaceBefore=4, spaceAfter=10)
- NOTE = ParagraphStyle("N", parent=B, backColor=colors.HexColor("#fff6dc"),
- borderColor=colors.HexColor("#e0b84a"), borderWidth=0.6,
- borderPadding=6, leftIndent=6, rightIndent=6, spaceBefore=4, spaceAfter=10)
-
- def P(t, s=B): return Paragraph(t, s)
- def k(t): return f'<font face="Courier-Bold">{t}</font>'
- def code(t): return Preformatted(t, CODE)
-
- def table(rows, widths, head=True):
- data = [[P(c, CELLB if (head and i == 0) else CELL) for c in r] for i, r in enumerate(rows)]
- t = Table(data, colWidths=widths, repeatRows=1 if head else 0)
- st = [("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#b8b4d6")),
- ("VALIGN", (0, 0), (-1, -1), "TOP"),
- ("TOPPADDING", (0, 0), (-1, -1), 3), ("BOTTOMPADDING", (0, 0), (-1, -1), 3)]
- if head:
- st.append(("BACKGROUND", (0, 0), (-1, 0), BLUE))
- for i in range(1 if head else 0, len(rows)):
- if i % 2 == 0:
- st.append(("BACKGROUND", (0, i), (-1, i), colors.HexColor("#f4f3fa")))
- t.setStyle(TableStyle(st))
- return t
-
- def footer(c, d):
- c.saveState()
- c.setFont("Helvetica", 8)
- c.setFillColor(colors.grey)
- c.drawString(inch * 0.75, inch * 0.5, "vim for the Commodore 64 - User Guide")
- c.drawRightString(letter[0] - inch * 0.75, inch * 0.5, f"Page {d.page}")
- c.restoreState()
-
- W = letter[0] - 1.5 * inch
- s = []
-
- # ---------------------------------------------------------------- title
- s += [Spacer(1, 0.6 * inch),
- P("vim for the Commodore 64", ParagraphStyle("T", parent=ss["Title"], fontSize=28, textColor=BLUE, leading=34)),
- P("User Guide and Command Reference", ParagraphStyle("ST", parent=ss["Title"], fontSize=15, textColor=LIGHT)),
- Spacer(1, 6),
- P("A small modal, vi-style text editor written in Power C (K&R C) with machine-language "
- "helpers, running on a stock C64 with a 1541/1581-style disk drive on device 8.",
- ParagraphStyle("Sub", parent=B, alignment=1, fontSize=10.5, textColor=colors.HexColor("#444444"))),
- Spacer(1, 14)]
- if os.path.exists(SHOT):
- img = Image(SHOT, width=3.84 * inch * 1.05, height=2.72 * inch * 1.05)
- s += [img, P("<i>The editor running in VICE: 24 text rows, the reverse-video cursor block "
- "and the status row (file name on the left, line number / total on the right).</i>",
- ParagraphStyle("cap", parent=B, alignment=1, fontSize=8.5, textColor=colors.grey))]
- else:
- print("note: no screenshot at", SHOT, "- title page built without it")
-
- s += [P("Contents", H2)]
- for line in ["1. Overview and limits", "2. Building and running", "3. Starting the editor",
- "4. The screen", "5. Normal mode commands", "6. Insert mode",
- "7. Colon (:) commands", "8. Status messages", "9. Differences from real vi/vim",
- "10. Worked examples", "11. Source layout"]:
- s.append(P(line, ParagraphStyle("toc", parent=B, spaceAfter=1, leftIndent=12)))
- s.append(PageBreak())
-
- # ---------------------------------------------------------------- 1
- s += [P("1. Overview and limits", H1),
- P("The editor loads a PETSCII sequential (SEQ) file from disk into one memory buffer, "
- "shows it on the 40-column screen, lets you move around and edit it with vi-style "
- "keys, and writes it back. It is a deliberately small subset of vi: the commands "
- "listed in this guide are the complete set."),
- table([["Item", "Limit / behaviour"],
- ["Text buffer", "19,000 bytes (one <font face='Courier'>malloc</font> block). "
- "A file larger than about 18,999 bytes is truncated on load without warning."],
- ["Lines", "Up to 599 lines. Opening a new line beyond that is refused."],
- ["Line length", "80 characters. Longer lines are split at 80 on load (you are told "
- "<i>long lines split at 80</i>); typing past 80 shows <i>line full (80)</i>."],
- ["Screen", "24 text rows of 40 columns plus a status row. The view scrolls vertically "
- "and horizontally to follow the cursor."],
- ["File format", "PETSCII, lines separated by carriage return ($0D). CR LF and bare LF "
- "are converted to CR on load; a Ctrl-Z ($1A) byte ends the file. A final CR is "
- "always added on save."],
- ["Disk", "Device 8, drive 0. Files are read as <font face='Courier'>name,S,R</font> and "
- "written as <font face='Courier'>@0:name,S,W</font> (replace)."],
- ["File names", "Up to 20 characters, stored in lower case."],
- ["Undo / search / counts", "Not supported (see section 9)."]],
- [1.4 * inch, W - 1.4 * inch])]
-
- # ---------------------------------------------------------------- 2
- s += [P("2. Building and running", H1),
- P("Everything lives in the <font face='Courier'>vim/</font> folder of the repository. "
- "The build needs VICE 3.10 installed at "
- "<font face='Courier'>C:\\Program Files\\GTK3VICE-3.10-win64</font> and the Power C system "
- "disk at <font face='Courier'>reference\\POWERc.d81</font> (never modified; the build works "
- "on a copy called <font face='Courier'>work.d81</font>)."),
- P("Build", H2),
- code("build.bat build vim on work.d81\n"
- "build.bat vim -Full fresh disk, recompile every unit\n"
- "build.bat vim -Run -Keys \"vim.c\\njjj\" build, then run and type keys"),
- P("The build runs one VICE session that compiles each changed unit "
- "(<font face='Courier'>vim, vscreen, vview, vedit, vml</font>) with "
- "<font face='Courier'>cc</font>, links them with <font face='Courier'>link -s</font>, and "
- "produces the program <font face='Courier'>vim</font> on <font face='Courier'>work.d81</font>. "
- "Only units whose source changed since the last build (tracked in "
- "<font face='Courier'>work.stamp</font>) are recompiled. Screenshots of each step go to "
- "<font face='Courier'>shots\\</font>."),
- table([["build.ps1 option", "Meaning"],
- [k("-Prog name"), "Program to build: <font face='Courier'>vim</font> (default) or <font face='Courier'>mltest</font>."],
- [k("-Full"), "Ignore the stamps: start from a fresh work disk and recompile everything."],
- [k("-Run"), "After linking, leave the Power C shell, load the program and run it."],
- [k("-Keys text"), "Keystrokes typed into the running program (<font face='Courier'>\\n</font> = RETURN)."],
- [k("-Shell text"), "Extra commands for the Power C shell instead of running."],
- [k("-Delay ms"), "Wall-clock milliseconds per keystroke (default 900)."],
- [k("-Pad / -Tail n"), "Spare RETURNs while a compile / the linker is working (20 / 8)."],
- [k("-Settle s"), "Extra seconds to keep running after the last key."]],
- [1.4 * inch, W - 1.4 * inch]),
- P("Run", H2),
- P("Double-click or run <font face='Courier'>run.bat</font>: it autostarts "
- "<font face='Courier'>vim</font> from <font face='Courier'>work.d81</font> in "
- "<font face='Courier'>x64sc</font>. On a real C64 (or any emulator) with the disk in device 8:"),
- code("LOAD\"VIM\",8,1\nRUN"),
- P("<b>Keyboard tip for VICE:</b> with the default keymap the PC <b>Esc</b> key is the C64 "
- "<b>RUN/STOP</b> key and <b>Backspace</b> is <b>INST/DEL</b>, so the editor feels close to "
- "vim on a PC keyboard. Upper-case letters such as G, A, I and O are typed with Shift.", NOTE)]
-
- # ---------------------------------------------------------------- 3
- s += [P("3. Starting the editor", H1),
- P("The screen clears to white text on blue and the status row asks:"),
- code("file: _"),
- table([["You type", "What happens"],
- ["A name, then RETURN", "The file is loaded from device 8 and you are in normal mode at line 1."],
- ["Just RETURN (or RUN/STOP)", "You get an empty buffer named <i>[no name]</i>. Give it a name later with "
- + k(":w name") + "."],
- ["A name that is not on the disk", "You are asked <font face='Courier'>not found - create it? (y/n)</font>. "
- + k("y") + " starts an empty buffer that keeps the name (the status row says <i>new file</i>; "
- "the file is created on the first " + k(":w") + "). " + k("n") + " or RUN/STOP asks for a name again."]],
- [1.9 * inch, W - 1.9 * inch]),
- P("While typing at the <i>file:</i> prompt (and on the colon command line) DEL erases the last "
- "character and RUN/STOP cancels. Shifted letters are folded to lower case.")]
-
- # ---------------------------------------------------------------- 4
- s += [P("4. The screen", H1),
- P("Rows 0-23 show the text. The character under the cursor is drawn in reverse video. "
- "Row 24 is the status row, also in reverse video:"),
- code("left part (26 chars) + line/total\n"
- "vim.c + 12/240"),
- table([["Part", "Shows"],
- ["Left", "A one-shot message if there is one (section 8); otherwise "
- "<font face='Courier'>-- insert --</font> in insert mode; otherwise the file name, or "
- "<font face='Courier'>[no name]</font>."],
- ["Column 28", "<font face='Courier'>+</font> when the buffer has unsaved changes."],
- ["Right", "Current line number / total lines."]],
- [1.2 * inch, W - 1.2 * inch]),
- P("The editor switches the VIC-II to the upper/lower-case character set, so unshifted "
- "letters show as lower case and shifted letters as capitals.")]
-
- # ---------------------------------------------------------------- 5
- s += [P("5. Normal mode commands", H1),
- P("Normal mode is where the editor starts and where RUN/STOP returns you. Commands are "
- "single keys (or the two-key " + k("dd") + " and " + k("gg") + "); they take no count prefix.")]
- s += [KeepTogether([P("Moving", H2), table([["Key", "Action"],
- [k("h") + " or CRSR left", "Left one character."],
- [k("l") + " or CRSR right", "Right one character (stops at end of line)."],
- [k("j") + " or CRSR down", "Down one line."],
- [k("k") + " or CRSR up", "Up one line."],
- [k("0"), "Start of the line."],
- [k("$") + " (Shift+4)", "End of the line."],
- [k("gg"), "First line of the file."],
- [k("G") + " (Shift+g)", "Last line of the file."]],
- [1.6 * inch, W - 1.6 * inch])]),
- P("When moving up or down onto a shorter line the cursor is pulled back to that line's end. "
- "The screen scrolls so the cursor is always visible, including sideways for lines "
- "longer than 40 columns.")]
- s += [P("Entering insert mode", H2),
- table([["Key", "Inserts"],
- [k("i"), "Before the cursor."],
- [k("a"), "After the cursor."],
- [k("I") + " (Shift+i)", "At the start of the line."],
- [k("A") + " (Shift+a)", "At the end of the line."],
- [k("o"), "On a new, empty line opened below the current one."],
- [k("O") + " (Shift+o)", "On a new, empty line opened above the current one."]],
- [1.6 * inch, W - 1.6 * inch])]
- s += [P("Changing text", H2),
- table([["Key", "Action"],
- [k("x"), "Delete the character under the cursor."],
- [k("dd"), "Delete the current line and keep a copy of it (up to 80 characters) in the yank buffer. "
- "Pressing " + k("d") + " followed by any other key cancels."],
- [k("p"), "Put the last line deleted with " + k("dd") + " on a new line below the cursor. "
- "The copy stays, so " + k("p") + " can be repeated; " + k("dd") + " then " + k("p") + " moves a line down."],
- [k(":"), "Open the colon command line (section 7)."]],
- [1.6 * inch, W - 1.6 * inch]),
- P("Any other key in normal mode is ignored.")]
-
- # ---------------------------------------------------------------- 6
- s += [P("6. Insert mode", H1),
- P("The status row shows <font face='Courier'>-- insert --</font>. Every printable key is "
- "inserted at the cursor, including shifted letters and the C64 graphics characters."),
- table([["Key", "Action"],
- ["RUN/STOP (Esc in VICE)", "Back to normal mode; the cursor steps one left, as in vi."],
- ["RETURN", "Split the line at the cursor; the rest moves to a new line below."],
- ["INST/DEL (Backspace in VICE)", "Delete the character before the cursor. At the start of a line, "
- "join it onto the previous line (refused with <i>joined line would pass 80</i> if the "
- "result would be too long)."],
- ["Cursor keys", "Move without leaving insert mode."]],
- [1.9 * inch, W - 1.9 * inch]),
- P("A line cannot grow past 80 characters: further typing is refused with <i>line full (80)</i>.")]
-
- # ---------------------------------------------------------------- 7
- s += [P("7. Colon (:) commands", H1),
- P("Press " + k(":") + " in normal mode, type the command on the status row and press RETURN. "
- "DEL edits; RUN/STOP or an empty line cancels."),
- table([["Command", "Action"],
- [k(":w"), "Write the buffer to its file name, replacing the old copy."],
- [k(":w name"), "Write to <i>name</i>; from now on the buffer is called <i>name</i>."],
- [k(":wq") + " / " + k(":wq name"), "Write, then quit."],
- [k(":x"), "Write (always, even if nothing changed), then quit."],
- [k(":q"), "Quit. Refused with <i>unsaved - :q! quits</i> if there are unsaved changes."],
- [k(":q!"), "Quit and throw away unsaved changes."],
- [k(":dir"), "List the disk directory (device 8) over the editor, one page at a time. "
- "Any key shows the next page, RUN/STOP stops; the editing screen then comes back unchanged."]],
- [1.6 * inch, W - 1.6 * inch]),
- P("<b>Quitting restarts the C64.</b> Power C uses zero-page locations BASIC depends on, so "
- "instead of returning to a damaged BASIC the editor jumps to the KERNAL reset routine "
- "($FCE2). You see the normal power-on banner and READY. Anything in memory is gone; only "
- "what you wrote to disk survives.", NOTE),
- P("Commands are matched on their letters only, so " + k(":Q") + " works like " + k(":q") +
- ". Anything else gives <i>unknown command</i>.")]
-
- # ---------------------------------------------------------------- 8
- s += [P("8. Status messages", H1),
- P("Messages appear on the left of the status row and disappear at the next key."),
- table([["Message", "Meaning"],
- ["new file", "The name you gave is not on the disk yet; " + k(":w") + " will create it."],
- ["long lines split at 80", "The file had lines over 80 characters; they were broken into several lines."],
- ["written", "The save succeeded."],
- ["cannot write", "The file could not be opened for writing (no disk, drive not present, ...)."],
- ["no file name", k(":w") + " on a buffer with no name: use " + k(":w name") + "."],
- ["unsaved - :q! quits", k(":q") + " with unsaved changes."],
- ["unknown command", "The colon command was not one of w, wq, x, q, q!, dir."],
- ["cannot read directory", k(":dir") + " could not open the drive's directory."],
- ["line full (80)", "Insert refused: the line is already 80 characters long."],
- ["joined line would pass 80", "Backspace at the start of a line refused: the joined line would be too long."],
- ["out of memory", "The 19,000-byte buffer could not be allocated (shown at start-up)."]],
- [1.9 * inch, W - 1.9 * inch])]
-
- # ---------------------------------------------------------------- 9
- s += [P("9. Differences from real vi/vim", H1),
- table([["Area", "This editor"],
- ["Escape", "RUN/STOP (the C64 has no Esc key; VICE maps PC Esc to it)."],
- ["Counts", "No numeric prefixes: " + k("3j") + " or " + k("5dd") + " do not work (digits other than "
- + k("0") + " are ignored)."],
- ["Undo / redo", "None - save often."],
- ["Yank / registers", "One unnamed register filled only by " + k("dd") + "; no " + k("yy") + ", "
- "no named registers, no " + k("P") + "."],
- ["Motions", "No word motions (w, b, e), no search (/, ?, n), no " + k(":number") + "."],
- ["Other", "No visual mode, no " + k(".") + " repeat, no " + k("r") + ", " + k("c") + ", " + k("J") +
- ", no " + k(":e") + " (quit and run again to open another file)."]],
- [1.4 * inch, W - 1.4 * inch])]
-
- # ---------------------------------------------------------------- 10
- s += [P("10. Worked examples", H1),
- P("Create a new file", H2),
- code("RUN start the editor\n"
- "notes RETURN file: notes\n"
- "y not found - create it? (y/n)\n"
- "i Hello, C64! RETURN type some text\n"
- " second line\n"
- "RUN/STOP back to normal mode\n"
- ":wq RETURN write and quit (the C64 restarts)"),
- P("Fix a typo and move a line", H2),
- code("j j j down to the line\n"
- "l l l l x move right, delete one character\n"
- "dd cut the line\n"
- "p put it back one line lower\n"
- ":w RETURN save; status row says \"written\""),
- P("Save under a new name and abandon edits", H2),
- code(":w backup RETURN write a copy as \"backup\"\n"
- "x x x more edits ...\n"
- ":q RETURN \"unsaved - :q! quits\"\n"
- ":q! RETURN quit without saving"),
- P("Automated test run in VICE", H2),
- code("build.bat vim -Run -Keys \"vim.c\\njjj\""),
- P("Builds, starts the editor, opens <font face='Courier'>vim.c</font> from the work disk and "
- "moves down three lines; the final screen is saved to <font face='Courier'>shots\\run.png</font>.")]
-
- # ---------------------------------------------------------------- 11
- s += [P("11. Source layout", H1),
- table([["File", "Contents"],
- ["vim.c", "<font face='Courier'>main()</font>, the key dispatcher for both modes, the file prompt, loading and the colon commands."],
- ["vedit.c", "Buffer editing: insert, delete, newline, backspace/join, " + k("dd") + "/" + k("p") + ", cursor keys, saving."],
- ["vview.c", "Scrolling, partial/full repaint and the status row."],
- ["vscreen.c", "Screen set-up and tear-down, low-level drawing, keyboard read from the KERNAL buffer ($C6/$0277)."],
- ["vml.s / vmlwrap.c", "6510 helpers copied to $C000: full-screen paint, row paint, block move, line-index adjust, key pop. "
- "<font face='Courier'>build-ml.ps1</font> assembles them into the generated <font face='Courier'>vml.c</font>."],
- ["*.pet", "PETSCII copies of the sources made by the build; the <font face='Courier'>.c</font> files are authoritative."],
- ["build.bat / build.ps1", "One-session build on <font face='Courier'>work.d81</font>."],
- ["run.bat", "Start the built editor in VICE."],
- ["NOTES.md", "Hard-won notes on Power C, sys(), the keyboard and VICE automation."]],
- [1.5 * inch, W - 1.5 * inch]),
- Spacer(1, 6),
- P("Memory use: Power C program and heap below $C000 (the text buffer is a 19,000-byte heap "
- "block), machine code at $C000, its parameter block at $C400, zero-page save area at $C410, "
- "and the C stack just below $D000. Screen at $0400, colour RAM at $D800.")]
-
- doc = SimpleDocTemplate(OUT, pagesize=letter, leftMargin=0.75 * inch, rightMargin=0.75 * inch,
- topMargin=0.75 * inch, bottomMargin=0.8 * inch,
- title="vim for the Commodore 64 - User Guide", author="Daniel Covington")
- doc.build(s, onFirstPage=footer, onLaterPages=footer)
- print("ok", OUT)
|