ls, a listing laid out to be read

> ls
  Copy.sbx   Say.sbx    Where.sbx  Walk.sbx   ls.sbx     Lander     where.sh

dir says what is there one line each, with sizes and a tally of the disk.
This says the names in columns and nothing else, which is what you want
ninety times in a hundred. Two programs rather than one with a switch,
because they answer different questions and neither answer is a worse
version of the other.

AND THIS IS A PROGRAM WHERE DIR IS BUILT IN, which is the difference that
matters when the disk is what you are doubting: dir is already in memory
and this has to be loaded off the disk it is about to list. The daily
driver and the diagnostic.

Two passes over the directory and no buffer at all. Columns need the
longest name before the first line can be printed, which usually means
holding every name - twenty four bytes each against a format that allows
1,024 entries. Walking twice costs a read of each directory block, into a
buffer that is already there.

Across and not down. Real listings go down the columns so that names next
to each other alphabetically are next to each other on the screen, and
that reason depends on sorting - which nothing here does. With the order
arbitrary, down-and-across buys nothing and costs a division.

Directories in blue and wearing a separator, unfinished saves in red,
everything else plain: whether a file is runnable cannot be known without
opening it, and opening every file in a directory to colour a listing is
a price nobody agreed to pay. The colour reaches a terminal as well as
the screen, so it is one mechanism and not two.

---- Three bugs, and one of them is this machine's oldest trap ----

nameLength answered in A, and a RET puts A back the way the caller had
it. So it answered with nothing, and every gap between the columns came
out the same width because the padding was subtracting whatever A
happened to hold. Q is the ALU's output and nothing puts it back, which
is why every answer here comes home in it.

The padding subtracted the other way round - the name from the cell -
which borrowed on every name that was not the longest, so all of them
took the "wider than its cell" path and the listing came out separated by
one space.

And padding after a name left trailing spaces on every line that did not
fill its last column. It goes before the next name now, so spaces only
ever fall between two things and a line ends on a name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-06 19:29:02 -04:00
co-authored by Claude Opus 5
parent 323d7a0330
commit a13bbeb4db
19 changed files with 486 additions and 12 deletions
+369
View File
@@ -0,0 +1,369 @@
; A listing laid out to be read.
;
; > ls
; Copy.sbx Say.sbx Where.sbx
; Walk.sbx Lander where.sh
;
; > ls /Apps
;
; dir says what is there, one line each, with sizes and a tally of the disk. This says the
; names and nothing else, in columns, which is what you want ninety times in a hundred - and
; the two are separate programs rather than one with a switch because they answer different
; questions and neither answer is a worse version of the other.
;
; ---- Why this is a program and dir is not ----
;
; dir is built into the shell, and that is what makes it the one to trust when the disk is
; the thing being doubted: it is already in memory. This has to be loaded FROM the disk, so it
; cannot list a disk too broken to load it. The daily driver and the diagnostic, and it is
; worth having both.
;
; ---- Two passes, and no buffer at all ----
;
; Columns need the longest name before the first line can be printed, which usually means
; holding every name in memory - twenty four bytes each, and this format allows 1,024 entries
; on a disk. So instead the directory is walked TWICE: once to find the longest name and count
; them, once to print. A walk costs a read of each directory block and the disk buffer is
; already there, where a buffer for the names would be memory this program has to have whether
; the directory is large or not.
;
; ---- Across and then down, rather than down and then across ----
;
; Real listings go down the columns, so that names next to each other alphabetically are next
; to each other on the screen. THAT IS A REASON THAT DEPENDS ON SORTING, and nothing here
; sorts: entries come back in the order the directory holds them, which is the order they were
; made. With the order arbitrary, down-and-across buys nothing and costs a division to work
; out how many rows there are - on a machine that cannot divide.
;
; So this goes across. If sorting ever arrives, that is the moment to change it, and this
; comment is the reason why.
#Include services.asm
#Program
#Base 0x5000
start:
; A directory to list, if one was named. The shell puts the working directory back when a
; program exits, so this can walk off somewhere and not tidy up after itself.
SETD.0 Given
INIB 0d64
SWI osArgument
SETD.0 Given
LDA.0
BRA lsHere
SWI osChangeDir
BNQ lsNoSuchPlace
lsHere:
; ---- Pass one: how wide the widest of them is ----
RSTA
SETD.0 Longest
STA.0
SETD.0 Seen
STA.0
SETD.0 Name
INIB 0d24
SWI osDirFirst
BRI lsMeasure
lsMeasureNext:
SETD.0 Name
INIB 0d24
SWI osDirNext
lsMeasure:
MVQA
SETD.0 Kind
STA.0
INIB 0xFF
CCF
SUB
BRQ lsMeasured
SETD.0 Seen
LDA.0
INCA
STA.0
CALL nameLength
MVQA
; A directory wears a separator, which is a character of width like any other.
SETD.0 Kind
LDB.0
DECB
BNB lsMeasureWidth
INCA
lsMeasureWidth:
SETD.0 Longest
LDB.0
CCF
SUB
BRC lsMeasureKeep ; Borrowed, so this one is not the longest.
STA.0
lsMeasureKeep:
BRI lsMeasureNext
lsMeasured:
; Nothing at all is said with nothing at all, which is what an empty directory looks like.
SETD.0 Seen
LDA.0
BRA lsDone
; ---- How many of them fit across ----
;
; Two spaces between columns, so that names never touch. Worked out by taking the cell width
; off the screen width until there is not room for another, because this machine cannot
; divide and the answer is never more than a handful.
SETD.0 Longest
LDA.0
INCA
INCA
SETD.0 Cell
STA.0
INA 0x32
SETD.1 Across
STA.1 ; What is left of the line, counted down.
RSTA
SETD.1 Columns
STA.1
lsFitting:
SETD.1 Across
LDA.1
SETD.1 Cell
LDB.1
CCF
SUB
BRC lsFitted ; Borrowed, so there is not room for another column.
MVQA
SETD.1 Across
STA.1
SETD.1 Columns
LDA.1
INCA
STA.1
BRI lsFitting
lsFitted:
; A name wider than the screen still gets a column of its own. One that runs over is better
; than one that is not shown.
SETD.0 Columns
LDA.0
BNA lsPrint
INIA 0d1
STA.0
lsPrint:
RSTA
SETD.0 At
STA.0
SETD.0 Name
INIB 0d24
SWI osDirFirst
BRI lsSay
lsSayNext:
SETD.0 Name
INIB 0d24
SWI osDirNext
lsSay:
MVQA
SETD.0 Kind
STA.0
INIB 0xFF
CCF
SUB
BRQ lsLastLine
; ---- The cell the last name started, finished now rather than then ----
;
; Padding AFTER a name puts spaces at the end of every line that does not fill its last
; column, and trailing whitespace on a line is the kind of thing that is invisible until
; something else reads the output. Padding before the next name instead means the spaces
; only ever go between two things, and a line ends on the last character of a name.
SETD.0 At
LDA.0
BRA lsColour ; First on its line, so there is nothing to finish.
CALL padOut
lsColour:
; ---- What it is, said in colour ----
;
; The attribute reaches a terminal as well as the screen now, so this is one mechanism and
; not two. A directory in blue and an unfinished save in red; everything else plain, because
; whether a file is runnable cannot be known without opening it, and opening every file in a
; directory to colour a listing is a price nobody agreed to pay.
SETD.0 Kind
LDA.0
BRA lsPlain
DECA
BRA lsBlue
INIA 0d1 ; An unfinished save, which is worth looking at.
BRI lsInk
lsBlue:
INIA 0d4
BRI lsInk
lsPlain:
RSTA
lsInk:
OUTA 0x06
SETD.0 Name
SWI osPrintString
CALL nameLength
MVQA
SETD.0 Kind
LDB.0
DECB
BNB lsWidth
PSHA
SETD.0 Slash
SWI osPrintString
POPA
INCA
lsWidth:
; How much of the cell this name took, for whoever finishes it.
SETD.0 Used
STA.0
; Back to plain before anything else is written, so that a scheme with paper in it would
; not paint the gap between the columns.
RSTA
OUTA 0x06
SETD.0 At
LDA.0
INCA
STA.0
SETD.0 Columns
LDB.0
CCF
SUB
BNQ lsSayNext ; Room for another on this line.
RSTA
SETD.0 At
STA.0
CALL newLine
BRI lsSayNext
lsLastLine:
; A line with something on it needs ending. One that ended on its last column does not, and
; a blank line under a listing looks like a listing with a gap in it.
SETD.0 At
LDA.0
BRA lsDone
CALL newLine
lsDone:
RSTA
SWI osExit
lsNoSuchPlace:
SETD.0 NoSuchPlace
SWI osPrintString
SETD.0 Given
SWI osPrintString
CALL newLine
INIA 0x01
SWI osExit
; ---- How wide the name in the buffer is ----
;
; Q COMES BACK THE COUNT, not A. A RET puts A back the way the caller had it, so a subroutine
; that answers in A answers with nothing - and this one did, which made every gap between the
; columns the same width because the padding was subtracting whatever A happened to hold.
; Q is the ALU's output and nothing puts it back, which is why every answer on this machine
; comes home in it.
nameLength:
SETD.0 Name
RSTA
lengthNext:
LDB.0
BRB lengthDone
INCA
INCD.0
BRI lengthNext
lengthDone:
RSTB
CCF
ADD
RET
; A is how much of the cell the name took. Fills the rest of it with spaces.
padOut:
; The cell less what the name took, and IN THAT ORDER. Written the other way round first,
; which borrowed on every name that was not the longest - so every one of them took the
; "wider than its cell" path below and the whole listing came out separated by one space.
SETD.0 Cell
LDA.0
SETD.0 Used
LDB.0
CCF
SUB
BRC padDone ; Wider than its cell, so one space keeps the names apart.
MVQA
BRA padOne
padLoop:
PSHA
SETD.0 Space
SWI osPrintString
POPA
DECA
BNA padLoop
RET
padOne:
padDone:
SETD.0 Space
SWI osPrintString
RET
newLine:
SETD.0 NewLine
SWI osPrintString
RET
#Data
#Base 0x3000
Slash:
"/"
Space:
" "
NoSuchPlace:
"there is no such directory: "
NewLine:
0x0A 0x00
Kind:
0x00
Longest:
0x00
Cell:
0x00
Columns:
0x00
Across:
0x00
At:
0x00
Seen:
0x00
Used:
0x00
Name:
#Reserve 0d24
Given:
#Reserve 0d64