Printing Code Like It's 1989

Which made me laugh, the 2016-2021 dialogue and still not implemented feature in that bug report is everything you love about Microsoft-controlled "open source but not really".

I've only ever printed code from BBEdit in the last couple decades, let's see how other editors fare:

  • BBEdit: Perfect. Print command in menu (it's a native Mac app!), prints headers, page & line numbers, nice margins. Looks perfect, like an old-timey print job. I should probably have switched from dark to light theme, but it'd be fine. ★★★★★
  • Geany: Print command, several settings in Preferences, prints headers, page & line numbers (in settings, choose either headers or page numbers at bottom, or it'll be duplicated). Hideous non-native GTK app and dialogs, but does the job perfectly well. ★★★★½
  • MacVim: Print command, produces a postscript file which immediately opens in Preview, spins a while, converts to PDF. Default has no line numbers, add set popt=header:2,number:y to your .vimrc. ★★★½☆
  • Atom: No print command in the menu, but there's multiple packages 5-6 years old, they work fine. Only shows line numbers, bad margins, it's literally a web page being printed. ★★★☆☆
  • Sublime Text: Print command, but no settings (in the horrible JSON text editor where you do settings) to make it nice. Literally dumps a web page, no page or line numbers. What's the least you could do and check off the feature? That's how subl does everything. ★★☆☆☆
  • Xcode: Takes forever to start. Prints in grayscale, which is nice, but no headers or line numbers. Fail. ★★☆☆☆
  • Panic Nova: … My demo expired, and cleaning the configs out doesn't let me try again, so who knows. Can't even give them a second try without paying $100. Trashed.

I'd be interested in seeing how others fare, and on non-Mac platforms if they have any consistent print command.

Here's a zip of PDF prints - maze2.py is a silly Python program, but at just 3 pages it's a good test case.

Mac Icons for PDFs

I have a great many folders of PDFs, mostly grabbed from archive.org magazine_rack, ataribooks, etc. The trouble is when I open a folder of these, Finder makes preview icons for a few of them, then gives up and they all show a generic "PDF" icon. What I want is a persistent icon for the first page!

First, you need osxutils:

% sudo port install osxutils
% man seticon

And my icontool.

sips (Scriptable Image Processing System) is a built-in tool on the Mac, incredibly powerful image converter. I'm not gonna do anything fancy, just use it to get an image.

Now create pdficonset.zsh:

#!/bin/zsh
export CG_PDF_VERBOSE=1

iconify () {
    echo $1
    sips -s format png -z 1024 768 -p 1024 1024 $1 -o thumb.png && \
    icontool.zsh thumb.png thumb.icns && \
    seticon -d thumb.icns $1
    rm -f thumb.png thumb.icns
}

if [[ $# -eq 0 ]]; then
    ls -1 *.pdf |while read -r f; do
        iconify $f
    done
elif [[ "$1" == "-r" ]]; then
    find . -type f -d -iname "*.pdf" |while read -r f; do
        iconify $f
    done
else
    while [[ $# -gt 0 ]]; do
        iconify $1
        shift $1
    done
fi

Now run it:

% pdficonset.zsh
# iconifies pdfs in current dir
% pdficonset.zsh -r
# iconifies pdfs in all subdirs
% pdficonset.zsh foo.pdf
# iconifies named pdfs

Boom! All nice icons.

[update: added a little better error-safety. CG_PDF_VERBOSE just gives better but still not useful error messages.]

[update 2023-12-21: finally made it generate proper aspect ratio icons, and multiple file commands.]

I don't have the problem as bad with CBZ/CBR comics; they'd be trivial to extract the first page from, since they're just ZIP/BZIP files.

folder full of pdfs with nice icons instead of placeholders

Formatting Strings in Scheme

Most of the time I use primitive display, or print functions:

;; displays a series of args to stdout
(define (print . args) (for-each display args) (flush-output-port) )

;; displays a series of args to stdout, then newline
(define (println . args) (for-each display args) (newline) (flush-output-port) )

;; displays a series of args to given port
(define (fprint port . args) (for-each (λ (x) (display x port)) args) (flush-output-port port) )

;; displays a series of args to given port, then newline
(define (fprintln port . args) (for-each (λ (x) (display x port)) args) (newline port) (flush-output-port port) )

;; displays a series of args to stderr, then newline
(define (errprintln . args)  (let [ (port (current-error-port)) ]
    (for-each (λ (x) (display x port)) args) (newline port) (flush-output-port port)
))

but sometimes I actually need to format things:

(import (prefix (srfi s19 time) tm: ))

(format #f "~8a ~10:d ~20a" name score
    (tm:date->string (tm:current-date) "~Y-~m-~d ~H:~M:~S") )

Common Lisp format works as described in Chez Scheme, using /#f for destination, and some other Schemes as well; but most Schemes only have the nearly-useless SRFI 28. I'm aware of cat/fox/etc combinatorial formatters, but they're very verbose.

Chez also has date/time functions, but no formatter, so using SRFI 19 - nicely, SRFI 19 mostly does sane things, it's not like C's strftime.

Gambit hits a mark

(see, the X-Men Gambit has perfect aim and a stupid accent, which still makes him more interesting than Hawkeye; and of course I'm Mark and so is Marc)

With much appreciated help from Marc Feeley, got maintest running.

A couple of lessons: I very much think include paths should include the path of the main source doing the including. Chibi's default is correct, Gambit's default is wrong and requires fixing in every user program. It's "more secure", but if you're running source code from a directory, you can probably trust whatever else is in that dir.

main was frustrating: Gambit manual 2.6 (highlighting mine)

After the script is loaded the procedure main is called with the command line arguments. The way this is done depends on the language specifying token. For scheme-r4rs, scheme-r5rs, scheme-ieee-1178-1990, and scheme-srfi-0, the main procedure is called with the equivalent of (main (cdr (command-line))) and main is expected to return a process exit status code in the range 0 to 255. This conforms to the “Running Scheme Scripts on Unix SRFI” (SRFI 22). For gsi-script and six-script the main procedure is called with the equivalent of (apply main (cdr (command-line))) and the process exit status code is 0 (main’s result is ignored). The Gambit system has a predefined main procedure which accepts any number of arguments and returns 0, so it is perfectly valid for a script to not define main and to do all its processing with top-level expressions (examples are given in the next section).

So your code that looks fine with 1 arg will break with 2, depending on the version. (main . argv) works. I'm in the process of making sure every one of my maintests parses args consistently, and every Scheme disagrees.

Gambit's compiler worked very simply once I got the library on the command line; it doesn't seek out & include them the way Chez does, even though it takes what looks like a search path.

The upside of all this is at least now there's one maintained, fast, R7-compatible Scheme compiler. I'm sticking with Chez (R6) for my code, but it's nice having something 100x faster (gut feeling, not benchmarked) than Chibi to test R7 code on.

Gambit is a risky scheme

(puns about "scheme" names are mandatory)

Neat: New version 4.9.4 of Gambit Scheme is out and they have a web site again after like 3 years.

OK: So I start adapting my little module/how do you run example: here

Bad: Not only does the R7 library system not work, their version of this hello example
will load code from fucking github at runtime! NPM viruses & sabotage are baked into the system. See Modules in Gambit at 30

SIGH.

FreePascal Building

I went to do a little code maintenance on a couple utils I'd written in FreePascal, and they wouldn't build. For a couple years, FPC just didn't work on 64-bit Mac OS, but they finally fixed that. Current fpc 3.2.0 is in MacPorts, I'm not sure what the state of Lazarus is, I quit using it. But the core Pascal is fine for many tasks, and I may do some things in CP/M Pascal when I get my SpecNext (longest wait ever until fall/whenever).

Anyway, the error I got was ld: library not found for -lc and nobody on the Internet has ever posted with that error, that I can find.

And eventually I tracked it down to the SDK not being linked at all. So here's an updated fpc.cfg which goes in your $PPC_CONFIG_PATH, note the DARWIN section. Also that's x86_64 only, I need to add an ARM64 branch at some point.

#IFDEF DEBUG
    #WRITE DEBUG
    -gl
    -Crtoi
#ELSE
    # Strip debuginfo
    -O2
    -Xs
#ENDIF

#IFDEF DARWIN
    #WRITE DARWIN
    # use pipes instead of temporary files for assembling
    -ap
    #DEFINE CPUX86_64
    -Px86_64
    -FD/Library/Developer/CommandLineTools/usr/bin
    -XR/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
#ENDIF

#IFDEF OBJFPC
    #WRITE OBJFPC
    -Mobjfpc
#ELSE
    #WRITE FPC
    -Mfpc
#ENDIF

# use ansistrings
-Sh

# stop after warnings
-Sew

# don't show Hint: (5023) Unit "X" not used in Y
-vm5023
# don't show Hint: (5024) Parameter "name" not used
-vm5024

#-Fl/usr/X11/lib
#-Fl/usr/local/lib

-Fu/opt/local/libexec/fpc/lib/fpc/$fpcversion/units/$fpctarget
-Fu/opt/local/libexec/fpc/lib/fpc/$fpcversion/units/$fpctarget/*

And a quick hello world/Unicode tester:

{ hello.pas
    Copyright ©2017 by Mark Damon Hughes. Do what thou wilt.
}

program hello;

uses crt, sysutils;

var
    name: utf8string;
    c: widechar;
    i: integer;
begin
    write('What is your name? ');
    readln(name);
    writeln('Hello, ', name, ' from code page ', stringCodePage(name), '!');
    for i := 1 to length(name) do begin
        c := name[i];
        writeln(i, ': ', c, '(', ord(c), ')');
    end;
    readkey();
end.

fpc -dDEBUG hello.pas produces a couple dozen warnings like: ld: warning: object file (/opt/local/libexec/fpc/lib/fpc/3.2.0/units/x86_64-darwin/rtl/baseunix.o) was built for newer macOS version (11.0) than being linked (10.8) and I'd love it if someone could tell me how to get FPC to tell ld to make that STFU.

But it works:

% ./hello
What is your name? Mark
Hello, Mark from code page 65001!
1: M(77)
2: a(97)
3: r(114)
4: k(107)
5: �(239)
6: �(163)
7: �(191)

Script the Scheme REPL with Expect

Routinely I want to open the Chez Scheme REPL in my code dir and load my standard libraries; I've been copy-pasting from a Stickies to get my setup each time, because you can't easily set a common prelude from command line. Finally solved that.

In ~/bin/scheme-repl, I put:

#!/usr/bin/env expect -f
log_user 0
cd "$env(HOME)/Code/CodeChez"
spawn scheme
expect "> "
send -- "(import (chezscheme) (marklib) (marklib-os)"
send -- "  (only (srfi s1 lists) delete drop-right last)"
send -- "  (only (srfi s13 strings) string-delete string-index string-index-right string-join string-tokenize) )\n"
log_user 1
interact

(make sure to change the path to wherever you keep your Scheme scripts, and whatever imports you like)

Added alias s=scheme-repl to my .zshrc

Now I can just hit one letter for shortcut:

% s
(import (chezscheme) (marklib) (marklib-os)  (only (srfi s1 lists) delete drop
-right last)  (only (srfi s13 strings) string-delete string-index string-index-r
ight string-join string-tokenize) )
> (os-path 'pwd)
"/Users/mdh/Code/CodeChez"
>

You can sort of do the same thing with a Chez boot file, but I wasn't able to get it to load libraries from thunderchez, even with --libdirs flag, so screw it.

I'd forgotten everything I ever knew about expect, and the only resources online are exact copies of the same "log into ssh with expect!" (which you should never do! Set up SSH keys for Cthulhu's sake!) tutorial over and over again, so had to read the man page to make even this trivial thing work.

Still Fixing Sublime

Minor notes on customization, all files are in Preferences > Browse Packages.

I fixed Dracula to be a little more pit of utter darkness:

UI: Customize Theme, file is User/Default Dark.sublime-theme:

// Documentation at https://www.sublimetext.com/docs/themes.html
{
    "variables": {
        "base_hue": "black",
        "base_tint": "#99ddff",
        "ui_bg": "#222222",
        "text_fg": "white",
        "sidebar_bg": "#111111",
        "sidebar_row_selected": "#666600",
    },
    "rules": [
    ]
}

UI: Customize Color Scheme, file is User/Dracula.sublime-color-scheme:

// Documentation at https://www.sublimetext.com/docs/color_schemes.html
{
    "variables": {
    },
    "globals": {
        "background": "black",
    },
    "rules": [
    ]
}

And some keys I use, file is User/Default (OSX).sublime-keymap: ^O as Open above is incredibly useful if you write "paren code enter paren open-above tab" over and over. Cmd-Y is lambda λ in a bunch of my editors and iTerm2; using the actual lambda symbol is awkward but cool, so of course I do it.

[
    { "keys": ["ctrl+o"], "command": "run_macro_file", "args": {"file": "res://Packages/Default/Add Line Before.sublime-macro"} },
    { "keys": ["shift+ctrl+o"], "command": "run_macro_file", "args": {"file": "res://Packages/Default/Add Line.sublime-macro"} },
    { "keys": ["super+y"], "command": "insert", "args": {"characters": "λ"} },
]

Obvious missing feature is piping thru shell. I'm going to tackle that fully later, but for now I just needed to number lines, so added a package Number, containing:

Number/Number.py:

import sublime
import sublime_plugin

def numberLines(s):
    out = []
    i = 0
    for line in s.splitlines():
        i += 1
        out.append("%d. %s" % (i, line))
    return "\n".join(out) + "\n"

class NumberCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        for region in self.view.sel():
            if not region.empty():
                s = self.view.substr(region)
                out = numberLines(s)
                self.view.replace(edit, region, out)

and Number/Default (OSX).sublime-keymap:

[
    { "keys": ["shift+ctrl+n"], "command": "number" }
]

This makes a nice template for doing additional filters. Of course there's no damned examples for the current version, the old samples are missing arguments.

When developing a plugin, you can open the console with ^~ and type view.run_command("number") and see some print statement debugging, which helps.

It's very much more of a "developer's toolkit" than a real editor sometimes. It reminds me a lot of hacking on E/PM in REXX on OS/2, where the entire editor was just a big REXX script and it was trivial to make it circle all search results, or some such. But it's been better for doing my Scheme hacking than anything else so far.

Sublime or Sub-prime?

Because I'm endlessly evading responsibility^W^W looking for more efficient ways to work at nothin' all day^A^K look, I play with new editors.

Ugly themes, right away, install Package Control, and then the theme Dracula, and it looks like something I can stand. Not as good as Hackerman for Atom.

It's still ludicrously using stacks of JSON files for settings; 700+ lines in the default, OS overrides that, then your user prefs override that. Read lines, figure out if the default is useful, copy & modify in your own.

It does have the advantage it works reasonably fast, and runs on multiple platforms. It has a Lisp syntax highlighter that doesn't completely suck on Scheme.

But it has this terrible minimap, and no setting for it! You have to disable it from the menu every time you make a new window. OR, you can hack on it. Prefs, Browse Packages, make a Python file User/minimap.py (found on StackOverflow, ugh):

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sublime
import sublime_plugin

class MinimapSetting(sublime_plugin.EventListener):
    def on_activated(self, view):
        show_minimap = view.settings().get("show_minimap")
        view.window().set_minimap_visible(show_minimap)

Then add "show_minimap": false, to the prefs file, and the stupid minimap never shows back up.

Problem 2, I wanted to insert timestamps in my headers. No datetime function or snippet or whatever.

Make a folder Datetime next to User, file Datetime/Datetime.py:

import sublime
import sublime_plugin
import datetime

class DatetimeCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        self.view.insert(edit, self.view.sel()[0].b, datetime.datetime.now().isoformat())

Note all the name cases matter. See that horrible self.view.sel()[0].b? That's how you get the current cursor position. WTF!

Next to it, make Default (OSX).sublime-keymap:

[
    { "keys": ["shift+ctrl+t"], "command": "datetime" }
]

And now Sh-^T will insert a chunky ISO8601 datetime. Good enough. (originally I did Sh-Cmd-D, but turns out that's duplicate line which I use sometimes)

So, I'm giving Sublime 4 a ★★☆☆☆ out of the box, the API is ugly as hell and they no longer have example code up so ★★★☆☆, but it's pretty hackable if you don't mind getting your hands covered in code guts, so maybe ★★★★☆

I'll give it another week and see if it's worth using longer term. BBEdit's infinitely better for giant text files, but I do have needs other than that.


Man, that Bachman & Turner concert… they're super old, and that was 2012. But Paul Schaeffer, minus the World's Most Dangerous Band, jamming out on the keyboard! The audience are too old to dance. But they still rock out pretty good for guys who are even older than me.

Reinventing the Wheel

Sure, there's existing code. Somebody Else's Code. It works fine, maybe not as fast as you'd like, or the interface isn't quite right. That's how it often is with me and SRFI-13. Olin Shivers is a skilled Schemer, back when that wasn't cool (OK, it's still not cool), but some of his APIs enshrined in early SRFIs drive me a little nuts, and the implementation is slow because it's so generalized.

So after a few false starts and failed tests, I now have these pretty things: (updated 2020-11-10, enforced hstart, hend boundaries)

;; Returns index of `needle` in `haystack`, or #f if not found.
;; `cmp`: Comparator. Default `char=?`, `char-ci=?` is the most useful alternate comparator.
;; `hstart`: Starting index, default 0.
;; `hend`: Ending index, default (- haystack-length needle-length)
(define string-find (case-lambda
    [(haystack needle)  (string-find haystack needle char=? 0 #f)]
    [(haystack needle cmp)  (string-find haystack needle cmp 0 #f)]
    [(haystack needle cmp hstart)  (string-find haystack needle cmp hstart #f) ]
    [(haystack needle cmp hstart hend)
        (let* [ (hlen (string-length haystack))  (nlen (string-length needle)) ]
            (set! hstart (max 0 (min hstart (sub1 hlen))))
            (unless hend (set! hend (fx- hlen nlen)))
            (set! hend (max 0 (min hend hlen)) )
            (if (or (fxzero? hlen) (fxzero? nlen))
                #f
                (let loop [ (hi hstart)  (ni 0) ]
                    ;; assume (< ni nlen)
                    ;(errprintln "hi=" hi ", ni=" ni ", hsub=" (substr haystack hi hlen) ", bsub=" (substr needle ni nlen))
                    (cond
                        [(cmp (string-ref haystack (fx+ hi ni)) (string-ref needle ni))  (set! ni (fx+ ni 1))
                            ;; end of needle?
                            (if (fx>=? ni nlen)  hi  (loop hi ni) )
                        ]
                        [else  (set! hi (fx+ hi 1))
                            ;; end of haystack?
                            (if (fx>? hi hend)  #f  (loop hi 0) )
                        ]
        ))))
    ]
))

;; Test whether 'haystack' starts with 'needle'.
(define (string-has-prefix? haystack needle)
    (let [ (i (string-find haystack needle char=? 0 0)) ]
        (and i (fxzero? i))
))

;; Test whether 'haystack' ends with 'needle'.
(define (string-has-suffix? haystack needle)
    (let* [ (hlen (string-length haystack))  (nlen (string-length needle))
            (i (string-find haystack needle char=? (fx- hlen nlen)))
        ]
        (and i (fx=? i (fx- hlen nlen)))
))

Written for Chez Scheme, caveat implementor. BSD license, do what thou wilt. If you find a bug, send me a failing test case.

I don't normally bother with fx (fixnum) operations, but in tight loops it makes a difference over generic numeric tower +, etc.