A program can see a directory

Every file service took a name a program already knew - read it, save it,
rename it, delete it, ask how big it is - and none of them could find out
what names there are. dir could list only because it lives in the shell
and calls the filesystem directly. So a file manager, a backup, and the
package manager still to come were each unwritable for want of this.

osDirFirst and osDirNext. DP0 says where to put the name and B how much
room, the same bargain osArgument and osWhereAmI offer.

Q ANSWERS THE KIND rather than a yes or no, so one value says both
whether there is an entry and what it is: 0 a file, 1 a directory, 2 a
save that stopped before it committed, 0xFF nothing more. A caller that
only wants names tests for 0xFF and ignores the rest.

The size is deliberately not in it. A walk hands back a name, and a
program that wants the size asks osFileInfo about that name - the
alternative being a record in memory whose shape both sides have to agree
on, which services.asm went out of its way to avoid for file sizes.

Walk.asm is the first program that can see a directory, and it asks
osFileInfo about each entry BETWEEN two steps of the walk. That is the
hazard rather than decoration: where a walk has got to and where the last
file asked about lives are both held by the system, and two things
sharing one position would show as a listing that stopped early or said a
name twice. Then the same walk in /Apps, since one that only ever ran at
the root would not have proved it walks where you are.

A directory is not asked about at all. osFileInfo answers for one
perfectly well and says nought blocks, which is true and reads as a size
- and nought is a size a file can genuinely have.

The lint baseline moves by one. The kind is decided by a chain of bit
tests in the shape dir already uses five hundred lines away, and arms of
a comparison chain each loading the same variable are the case this
project's own rule says not to collapse: the repetition is what lets a
new arm be dropped in anywhere.

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 17:55:41 -04:00
co-authored by Claude Opus 5
parent b83ba5bf7a
commit 2ad8edf9bc
10 changed files with 375 additions and 2 deletions
+137
View File
@@ -0,0 +1,137 @@
; A program that says what is on the disk.
;
; Which no program could do until now. Everything the system offered took a name a program
; already knew - read it, save it, rename it, delete it, ask how big it is - and there was no
; way to ask what names there are. dir could list only because it lives in the shell and calls
; the filesystem directly. So a file manager, a backup, and the package manager still to come
; were all unwritable for want of two services.
;
; This is those two, used as simply as they can be: walk, say what each thing is, and ask
; about it.
;
; > Walk
; f Say.sbx 1
; d Apps 0
; u halfsaved.txt 3
;
; ---- And it asks about each one AS IT GOES, which is the point ----
;
; Where a walk has got to is the system's, and so is where the last file it was asked about
; lives - the same eight bytes of filesystem state. A program that walks a directory and asks
; osFileInfo about each entry is using both at once, which is the obvious thing to write and
; the thing that would quietly go wrong if the two shared a position.
;
; Calling osFileInfo between two steps of the walk is therefore not decoration here. It is the
; check, and a walk that came back wrong afterwards would show up as a short listing or a
; repeated name rather than as an error.
#Include services.asm
#Program
#Base 0x5000
start:
SETD.0 Name
INIB 0d24
SWI osDirFirst
BRI walkCheck
walkStep:
SETD.0 Name
INIB 0d24
SWI osDirNext
walkCheck:
; The kind is written down before anything else wants the registers: the comparison below
; is an ALU operation and Q is where its answer goes.
MVQA
SETD.0 Kind
STA.0
INIB 0xFF
CCF
SUB
BRQ walkDone
; f, d or u. A chain rather than a table, because three is not enough to index - and A is
; still the kind, since SUB writes Q and leaves it alone.
BRA walkFile
INIB 0d1
CCF
SUB
BRQ walkDirectory
SETD.0 Unfinished
BRI walkSay
walkDirectory:
SETD.0 Directory
BRI walkSay
walkFile:
SETD.0 File
walkSay:
SWI osPrintString
SETD.0 Name
SWI osPrintString
SETD.0 Space
SWI osPrintString
; ---- And how big it is, asked BETWEEN two steps of the walk ----
;
; The name is the only thing the walk hands over, which is what keeps it from being a record
; both sides have to agree the shape of. A program that wants more asks about the name, the
; same way anything else does.
;
; A directory is not asked about. osFileInfo answers for one perfectly well and says nought
; blocks, which is true and reads as a size - and nought is a size a file can genuinely
; have, so the two would be indistinguishable in the listing.
SETD.0 Kind
LDA.0
INIB 0d1
CCF
SUB
BRQ walkNoSize
SETD.0 Name
SWI osFileInfo
BNQ walkNoSize
PSHD.3
POPB
POPA
SWI osPrintNumber
BRI walkEnded
walkNoSize:
SETD.0 NoSize
SWI osPrintString
walkEnded:
SETD.0 NewLine
SWI osPrintString
BRI walkStep
walkDone:
RSTA
SWI osExit
#Data
#Base 0x3000
File:
"f "
Directory:
"d "
Unfinished:
"u "
Space:
" "
NoSize:
"-"
NewLine:
0x0A 0x00
Kind:
0x00
Name:
#Reserve 0d24
+38
View File
@@ -791,6 +791,7 @@ from every assembly file in it. Several are old programs written for the bare ma
| Keys | The console interrupting rather than being asked. The only one that brings a vector of its own, which is what the version two format exists for. |
| Play | Four voices on one clock, which is what music is and one channel cannot be. The timer keeps a tick and every voice keeps its own place in its own track and its own count of how much longer the note it is holding lasts, so the parts move at four different rates and share nothing but the beat. A track is pairs of bytes, what to play and how many ticks it lasts: 1 to 127 is a MIDI note, zero is a rest, and 255 ends it - MIDI stops at 127, so neither of those had to be invented. A note's duration is its whole life and the gate goes down when the count runs out, which means a gap between two notes is written as a rest rather than invented by the player out of some fraction it decided on. Each voice loads an instrument of its own before a note is played - an oboe for the melody, strings under it, a square wave for the bass and a kalimba for the arpeggio - out of the format SoundPatch writes, which the player reads as a count and that many parameter and value pairs and understands nothing else about. Four patches can be up at once because a patch belongs to its channel; Kalimba has an LFO switched off and the other three have one on, and under a device where the LFOs belonged to the whole machine the last patch loaded would have imposed its setting on every part. A voice does not play one long track: it walks an ORDER LIST of its own, a table of sequence addresses, and takes the next one when a sequence runs out. That is where repetition comes from and it costs no notation - the bass plays the same sequence in the first bar and the last, written once. The four columns are how it reads and how a tracker would show it; per voice is how it is stored, because a voice's order cursor is then a pointer it advances by itself. Sequence and patch names are INDICES through two tables, which are the only places an address lives - so a tune read from a file will need its base added to two arrays and nothing else, rather than a loader that walks every sequence looking for addresses to correct. A sequence can also carry commands, which take no time at all: 0x80 plays the rest of that voice on a different patch, which is how the melody's last bar becomes a swell rather than a reed. Nothing keeps the voices together except that their sequences add up to the same length, which is the first thing a compiler should check. The patch each voice starts on is declared rather than assumed, because a voice given no instrument would play on whatever the device woke up with. `Play <file>` reads a tune and plays that; `Play` on its own plays the one built into it. A tune file is "SBTU", a version, the tick in cycles, and offsets to a patch table, a sequence table and four order lists - everything in it an OFFSET from wherever it was put, so loading one is adding the base to two tables and pointing four voices at their order lists. No sequence is walked and nothing inside one is an address, which is what makes a malformed tune something that plays wrongly rather than something that takes the loader with it; the magic is checked first, because the loader follows what the offsets name. The patch each voice starts on is in the header, because a starting instrument is state. The engine that plays it is Libraries/player.asm rather than this program. It spends over ninety nine per cent of its time asleep, because a beat is something to be woken by rather than counted up to. |
| Say | Prints whatever it was told, which is the shortest thing that shows osArgument working. |
| Walk | Says what is in the working directory, one line each: what kind of thing it is, its name, and how many blocks. The shortest thing that shows osDirFirst and osDirNext working, and the first program that could see a directory at all. |
| Where | Says the path it was loaded from, and with a name after it, the path of that name beside it. The shortest thing that shows osWhereAmI and path.asm working, and the answer follows the program rather than whoever ran it. |
| Reboot | Starts the machine again, in 45 bytes. Writes a port rather than asking the system, because a reset has to work when the system does not. |
| Once | Asks the loader to start something else on the next start, and only that one, in 569 bytes. |
@@ -1350,6 +1351,8 @@ Those numbers are written down once, in `Programs/CosmOS/Source/services.asm`, w
| osLastStatus | Q answers what the last program exited with: 0 it did what it was asked, 1 it did not, 2 it was asked wrongly. A program may give its own meanings if it says so. |
| osTakeScreen | Says this program is about to use the whole screen and would like what is on it put back when it exits. Q is zero if that was arranged; anything else means it was not, which is the ordinary answer on a machine with no volatile drive. See Giving The Screen Back. |
| osWhereAmI | DP0 names somewhere to put the path the program was loaded from, B says how much room there is. See Where A Program Came From. |
| osDirFirst | DP0 names somewhere to put an entry's name, B says how much room there is. Q says what it is: 0 a file, 1 a directory, 2 an unfinished save, 0xFF nothing more. Starts a walk of the working directory. See Seeing What Is On The Disk. |
| osDirNext | The same, for the entry after it. |
| osBootState | Q answers how the last start went: 0 settled, 1 trying, 2 fell back. A machine with no disk answers settled, because there is nothing there to be unsettled about. |
| osBootSettle | Puts it back to settled, which is how a machine that fell back is told the situation has changed. Q is zero if the disk took it. **Settling is the only write a program gets** - marking a start as trying or fallen back is the loader's business, and a service that let a program claim either would let it lie about something the loader cannot check. |
@@ -1490,6 +1493,41 @@ directory stays the person's; this is how a program finds its own things.
`Programs/CosmOS/Apps/Where.asm` is the whole of it in one program, and exists to be run
rather than read.
## Seeing What Is On The Disk:
Every other file service takes a name a program already knows - read it, save it, rename it,
delete it, ask how big it is. **None of them could find out what names there are.** `dir`
could list only because it lives in the shell and calls the filesystem directly, so nothing
loaded could list anything at all: a file manager, a backup and a package manager were each
unwritable for want of two services.
```
SETD.0 Name
INIB 0d24
SWI osDirFirst
...
SWI osDirNext
```
**Q answers the kind rather than a yes or no**, so one value says both whether there is an
entry and what it is: 0 a file, 1 a directory, 2 a save that stopped before it committed,
0xFF nothing more. A caller that only wants names tests for 0xFF and ignores the rest.
**The size is not in it.** A walk hands back a name, and a program that wants the size asks
`osFileInfo` about that name. The alternative is a record in memory whose shape both sides
have to agree on, which is exactly what these services went out of their way to avoid for
file sizes - and most callers want names and nothing else.
**One walk at a time, and the system holds where it has got to**, the same bargain
`osFileStart` makes for writing. A program that starts a second walk before finishing the
first gets the second; there is one position, not a handle per caller. The state is small, so
a program that stops in the middle leaves nothing behind to clean up.
`Programs/CosmOS/Apps/Walk.asm` is the whole of it in one program. It asks `osFileInfo` about
each entry **between two steps of the walk**, which is not decoration: where a walk has got to
and where the last file asked about lives are both the system's, and two things sharing one
position would show as a listing that stopped early or said a name twice.
## An Application With Things Of Its Own:
A simple program is one file and lives in `/Apps`. One with **assets** gets a directory of
+78
View File
@@ -6184,6 +6184,82 @@ fileForget:
; Blocks, not bytes, and that is forced rather than chosen: a file on a sixteen megabyte
; disk is up to twenty four bits long, which does not fit in a pointer. Blocks do, and the
; bytes in the last one come back from osFileBlock when the reader gets there.
; ---- Walking a directory on a program's behalf ----
;
; The one thing the filesystem could do that no program could ask for. dir has always been
; able to list because it lives in here; a loaded program had no way to find out what names
; exist at all, which left a file manager, a backup, and the package manager still to come
; all unable to be written.
;
; Two entry points into one routine, because the difference between them is a single call.
;
; Q ANSWERS THE KIND, so one value says both whether there is an entry and what it is: 0 a
; file, 1 a directory, 2 a save that stopped before it committed, 0xFF nothing more. A
; caller that only wants names tests for 0xFF and ignores the rest.
handleDirFirst:
SETD.2 DiskReady
LDA.2
BRA dirWalkNoMore
CALL sbfsFirst
BRI dirWalkGot
handleDirNext:
SETD.2 DiskReady
LDA.2
BRA dirWalkNoMore
CALL sbfsNext
dirWalkGot:
BNQ dirWalkNoMore
; The name first, because working out the kind wants the registers. B is still the room
; the caller asked for: a CALL puts it back, and nothing above has taken it.
PSHD.0
POPD.1
SETD.0 SbfsName
CALL copyText
; ---- What kind of thing it is ----
;
; Asked in the same order dir asks it. An unfinished save is looked at FIRST because it is
; the one thing here that is not really a file yet, and it is SHOWN rather than hidden:
; the bytes are all there under that name, so a program that can see it is a program that
; can rename it back.
SETD.2 SbfsFoundFlags
LDA.2
INIB 0x04
AND
BNQ dirWalkUnfinished
SETD.2 SbfsFoundFlags
LDA.2
INIB 0x02
AND
BNQ dirWalkDirectory
RSTA
BRI dirWalkAnswer
dirWalkDirectory:
INIA 0d1
BRI dirWalkAnswer
dirWalkUnfinished:
INIA 0d2
BRI dirWalkAnswer
dirWalkNoMore:
; A machine with no disk has nothing to walk, and says the same thing as a walk that has
; run out. There is no third answer worth telling apart: a caller that cannot list is a
; caller that lists nothing either way.
INIA 0xFF
dirWalkAnswer:
RSTB
CCF
ADD ; A is the answer, so Q becomes it.
SRET
handleFileInfo:
SETD.2 DiskReady
LDA.2
@@ -8652,6 +8728,8 @@ CommandLine:
osLastStatus handleLastStatus
osTakeScreen handleTakeScreen
osWhereAmI handleWhereAmI
osDirFirst handleDirFirst
osDirNext handleDirNext
osBootState handleBootState
osBootSettle handleBootSettle
Device 0x20 diskDone
+34
View File
@@ -208,3 +208,37 @@
; own would mean "Play mytune.tune", typed by somebody in their own directory, looked in
; Play's. The working directory stays the person's; this is how a program finds its own.
osWhereAmI 0d37
; ---- Seeing what is on the disk ----
;
; Everything above takes a name a program already knows. NOTHING HERE COULD FIND OUT WHAT
; NAMES THERE ARE - a machine whose programs can read, write, rename and delete files and
; cannot ask what files exist. dir could only list because it lives in the shell and calls
; the filesystem directly; no loaded program could list anything at all.
;
; DP0 says where to put the name and B how much room there is, counting the zero, the same
; bargain osArgument and osWhereAmI offer.
;
; Q ANSWERS THE KIND RATHER THAN A YES OR NO, which is one value carrying both "is there
; one" and "what is it":
;
; 0 a file
; 1 a directory
; 2 a save that stopped before it committed
; 0xFF there are no more
;
; ---- And the size is not in it ----
;
; A walk hands back a NAME, and a program that wants the size of what it found asks
; osFileInfo about that name. The alternative is a record in memory whose shape both sides
; have to agree on, which is exactly what this file went out of its way to avoid for file
; sizes - and most callers of this want names and nothing else.
;
; ---- One walk at a time, and the system holds it ----
;
; Where a walk has got to is the system's, the way an open write is. A program that starts a
; second walk before finishing the first gets the second; there is one position, not a handle
; per caller. That is the same bargain osFileStart makes and for the same reason: the state
; is small, and a program that stops in the middle leaves nothing behind to clean up.
osDirFirst 0d38
osDirNext 0d39