; 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, something that will run in green, an unfinished save in red, ; and everything else plain. SETD.0 Kind LDA.0 BRA lsFileInk DECA BRA lsBlue INIA 0d1 ; An unfinished save, which is worth looking at. BRI lsInk lsBlue: INIA 0d4 BRI lsInk lsFileInk: ; ---- Whether it will run, asked by looking ---- ; ; This was priced once as needing a RUNNABLE BIT in the directory entry, set by the ; filesystem from a list of magic numbers it would have to be taught - which was declined, ; twice, for putting format knowledge in the filesystem and giving two implementations a ; registry to keep in step. ; ; It costs nothing of the sort now. The shell decides what to run by READING the first block ; of a file, so "will this run" is a question with an answer already, and asking it is the ; same question the shell would ask. Nothing new is written down and nothing has to agree ; about anything: if the shell learns a third kind of runnable file, this is wrong until ; somebody adds two lines, and wrong in the direction of a listing that is less colourful. ; ; The price is a block read per file, which is what the bit existed to avoid. It is paid ; here rather than in dir on purpose: dir is the listing you audit and is built into the ; shell, and this is the one you read and is a program that was loaded off the disk anyway. CALL runnable BNQ lsPlain INIA 0d2 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 ; ---- Will the shell start this? ---- ; ; Q is zero if it will. The same two things the shell itself looks for in a first block: SBEX ; for a program, "#!" for a script. A file too short to have a first block at all cannot be ; either, and osFileBlock saying no is that answer arriving for free. runnable: SETD.0 Name SETD.1 Block RSTA RSTB SWI osFileBlock BNQ runnableNo ; The shebang first, because two bytes settle it. SETD.0 Block LDA.0 INIB 0d35 ; '#' CCF SUB BNQ runnableExec INCD.0 LDA.0 INIB 0d33 ; '!' CCF SUB BRQ runnableYes runnableExec: SETD.0 Block SETD.1 ExecText INIA 0d4 SETD.2 Left STA.2 runnableSame: LDA.0 LDB.1 CCF SUB BNQ runnableNo INCD.0 INCD.1 SETD.2 Left LDA.2 DECA STA.2 BNA runnableSame runnableYes: RSTA RSTB CCF ADD RET runnableNo: INIA 0x01 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 ExecText: "SBEX" 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 Left: 0x00 Name: #Reserve 0d24 Given: #Reserve 0d64 ; A whole block, because osFileBlock writes one whether the file fills it or not. Block: #Reserve 0d256