 a nice new brother WP1 word processor. I could connect the Raspberry Pi up to the UART like so. Okay, that's interesting. You can see that I'm successfully interacting with the Raspberry Pi sort of, and I seem to have mostly made it work. There are some problems. Linux is really keen on sending and see escape sequences. I am actually going to take the time to write a whole new terminal emulator. I am going to cut this video short here because the next one is going to be long and is going to be me writing Z80 code on my desktop. Let's write a VT102 terminal emulator, or at least most of one. So what you're looking at here is a hacked up version of Pterm. I've taken away all the code that I previously had and replaced it with a simple test routine that just prints a 16 bit value to the console and some code that actually does it. Now we can compile this and then we can run it in the CPM emulator and that gives a nice way to actually test things to make sure it works. This is eventually going to turn into a standalone program for the brother that doesn't use CPM, but I also want to be able to reuse the VT102 state machine for CPM-ish. So we're going to start with this for now. So this routine just uses the CPM entry point for writing to the console. So there's nothing really of interest here. This is the code to turn a value into hex using the digital adjust instruction on the Z80. So what's our emulator actually going to look like? Well, we're actually going to try and do this properly, which means it's going to be a gigantic state machine. Each time a character arrives, it's going to process that one character and store its state ready for the next character. This is annoying to write, but is the best way of doing it. Now the CPM-ish TTY state machine, the user interface for it is pretty straightforward. We have a, where is it? Somewhere here, we have the instruction, the routine that actually does it. Here we go, TTY put C, which is the thing that actually does the work. Now, our state, we could just use a single byte for them, but I think it's going to be simpler to use the address of the handler routine. So we're going to do, I'll try to remember the syntax of the, here we go, it's DW, DW zero. This is going to be the routine of the handler. So this is just going to be, and that, so does that compile? It does, and this actually won't do anything yet. And I'm just going to take a quick look at the hex to make sure that it did what I expected. Yeah, I'm just making sure that dseg, meaning place what's here in the data segment, is actually doing the right thing rather than just emitting it inline. Actually, is it doing that correctly? So LDHL89AB call put hex 16, ret 00, it is not doing it correctly. Okay, yeah, that's actually probably because I have got this set to absolute mode rather than relocatable. That's fine, I'll fix that later. We want to define states, so we're going to use a macro to do that. And we have state macro to the macro syntax. I have some macros to steal here somewhere. Common, that's interesting. I have some macros for the hd64180 extended instructions. So did I put them in there? Yes, no, they're here. z180.lib, there it is. Okay, so this will define a new state. A state consists of a label and a routine to set to the current state. So that will be hopefully this, gel comma, hopefully that's not going to work. Okay, so that should be the core of our state machine. So let's define a state and it does not like that. What I'm trying to do here is glue together the macro parameter with this string in order to produce a constant label. I'm using ZMAC, which is a kinder old fashioned, but rather good, z80 assembler. So check the documentation. Macro call expands to text to the macro body. Parameter pkvk, fantastic, doesn't say. I'm going to go look this up. It's the dollar sign. According to the documentation, dollar signs are ignored and identifies everywhere. So that should do it. And to double check, let's just take a look at the listing. So our def state waiting here gets expanded to the correct code. So what this does, it allows us to do call set state waiting to change the current state. And when we call TTY put C, it just dispatches to the state handler, which in this case does nothing. So C for character. Okay. The other state start with waiting means that we actually have no pending state. The terminal is idle waiting for something to show up. This is going to be where the bulk of our code's going to be. This is going to take care of printing normal characters. Now, the first thing we're going to do is handle UTF-8 parsing. We are going to cheat. We are only going to support printing UTF-8 characters because trying to handle them inside escape sequences is a lot of work and pointless anyway, because they don't really make sense there. So what we need to do here is to check to see whether the top bit is set, which indicates it's a UTF-8 multibike character, or unset, which means it's simple ASCII. UTF-8 is a way of encoding Unicode. A Unicode code point can have up to, I believe, 31 bits, yeah. And UTF-8 encodes this with a fairly efficient variable length encoding. You can see the different number of bytes for different values here. It has the nice property that you can tell how many bytes a sequence will have just from the first byte of sequence. So we are going to, we are going to look up the Z80 instruction set because I always need help with this. We want to look at the top bit of the character, which is, of course, like this. If it is zero, then we know this is a normal ASCII character. Otherwise, it's UTF-8. And if it's UTF-8, we want to look at the top byte and then track how many bytes there are going to be for use in our state machine and initialize things like our UTF-8 accumulator. So we are also going to cheat. We are going to parse 32-bit UTF-8 code points, sorry, Unicode code points, where we're actually only gonna care about the bottom 16 bits because Astral Plane Unicode is rarely used and I don't think our terminal is going to care about them at all. So I don't want to do this the other way around. Yeah, let's do this the other way around. So we want to know how many bits there are in the, we want to know how many bytes there are in the sequence. So we want to test one of these prefixes. Or can we do that efficiently I think, let's stash a copy of the character in A and we want to rotate. It's a bit zero, rotate, right, shift, right. Is there a logical shift, right? Oh yes, of course there is this one. Shift, right, logical. Contents a bit zero, copy to the carry flag and zero is put into bit seven. So shift, right, logical A. We can then now compare A against this sequence in binary. I'm trying to remember whether Zmax supports binary constants. I think it doesn't. So we're gonna have to do this in hex. So that is going to be seven, E, okay. Then we shift it right again. This is of course three E. Shift, right, again, this is gonna be one E. Actually, we don't wanna do it like this. We want to invert all these. A label starting with a dot is local. It only has validity between normal labels. So it's really useful for labels that don't really need a formal name. Six, I believe. And there is no one zero. So if we get this, it means we've come across one of these where we didn't expect one, which is invalid. So to handle these, we are just going to just strip off the high bit and treat it as if it was ASCII, like so. Okay, so here we want to prepare for actually handling UTF-8. So the first thing we wanna do is reset our state machine. Well, reset the UTF-8 parser. So I'm trying to think of the quickest way to actually set this to zero. Actually, we only need to initialize UTF-8 chart because we're about to initialize this, which we do here. So we're gonna say that this is a six-byte sequence. So we set the remaining characters to five because we've already got the first character. And we want to initialize the UTF-8 chart with the bits of the character that are in the top byte, which is still safely in C. So to do that, we are going to do ANSI and stash this in A, in the low byte of UTF-8 chart. And then this gets copied here. I wonder, can we make this better? There's quite a lot of duplicated code here. Oh yeah, and I also forgot to put our RETs in. Yes, I think we can. So we're just going to set the, we're just gonna set A and B to be the values we're interested in. Four, three, two, and F, one zero, one F rather. And I also remembered that I need to do this to give us some syntax highlighting. Okay, so here we call pass UTF-8 leader, that sets the A and B. We have to change this. So let's just do that. So if A is zero, that means we are invalid. Otherwise, store the A to the number of pending UTF-8 bytes. B is our mask, so this gives us our value. Put that in L, set H to zero, write our 16-bit value as the character and jump to set state UTF-8 trailer. So this is our code path for if we got an invalid value. So we are going to mask off, we're going to turn it into a rather broken seven-bit ASCII value and jump to the routine that actually does that. All right, so we need a state for, you see this won't build, we need a state for UTF-8 trailer. When a character is processed, when we're in this state, we're actually reading one of these. So we need to shift the UTF-8 char left by six bytes and or in the value in C. Now I'm just trying to think of the best way to do this. Shifting left by six is of course the same as shifting left by eight and then right by two. But if we actually did it that way, then I think we would lose the top two bits. Yeah, these two bits would be shifted off the left into the third byte and then back two. So I think we're going to have to do it the crude way. So we want to load our UTF-8 char. Shift left by six and the easiest way to do that is to just add it to itself six times like so. We now want to or in C, which is our character into the low. So like so, write it back and now we want to get the address of the variable that's got our number of UTF bytes remaining in it. Decrement it and let me just find deck HL, not this one, this one and yes, Z flag is defined as effected as defined. So if it's not Z, just exit and we will stay in the same state that we were in before. Otherwise, we have finished, we have read a complete UTF-8 character. So we are going to jump to a routine to do it. So what are we going to do for this? Well, for the time being, we are just going to print it, print the value we got and then switch back to the waiting state. Okay, so we have some code and if you look at the listing, we can see that we have a reasonable amount of code. Here's our shift left by six. Add HL, HL is actually cheap. It's a single byte and a quite a fast byte too. Z80 code is annoyingly slow. That is 2929, add HL, HL. See, this is 11 clock cycles. That's way faster than trying to shift it piecemeal using the shift instructions, particularly as we would have to use some of the extended shift instructions. These, which are both, which are two bytes and like eight cycles per byte. Also the other add instructions here are two bytes and 15 cycles. Yes, and here, if we'll print ASCII, we are going to call put hex eight jrset state waiting. So put hex eight, yes, that prints the thing in A. Okay, so let us print this and see how that parses the emulator. We got a five five, that is exactly what we would expect to see. Now let us fake up a UTF-8 byte. We want, this isn't going to be a valid one, but it is at least going to be simple. We're going to do a two byte value. So that's going to be CO, meaning this prefix, but with all zeros for these Xs here, which is technically not allowed. And then we're going to do our five five in the second byte. Is that going to work? Yeah, that's a D five. So we run this, we get a D five, that's wrong. Yeah, that's because we forgot to mask off the top two bits here. So we're going to want to change this slightly. We're going to want to put, so that will mask the character with three F, which would give us the bottom six bits. Then we're going to or it in with the low value of the shifted, you see a shifted code point, and then put that back into L. One five, that is wrong. A is that wrong. So five five is 0101, 0101, which will not fit in our low byte. So this is just me with bad data. So the top one wants to be there, so we're going to have to put it there. So that should be like so. Five five, excellent. We should probably test the others that don't really fancy trying to put more test cases together. So, okay, let's try at least the three bit version. So that's going to be picking up a random value, which is E one, eight one, eight one. So we get one zero four one, which is 001, 001, 001, 001. So this is the six bit sequence from this byte. This is the six bit sequence from this byte, and these are the bits from here. And I can probably see one, three, seven O C ones. 0111, 00, 00, 1100, 001. So that gives us the six bit sequence from the last byte, which is n in a one. The six bit sequence from this byte, which ends in a three. And the sequence from this byte, which ends in a seven. Good, I think that works. So let me just do some, just divide things up a little. All right, this should give us the, we should now have a working state machine for UTF-8. How big is our program so far? 200 bytes. Wow, that's quite big. All right, so now we start with the more interesting stuff, which is the actual VT-102 state machine anyway. Luckily, this is documented reasonably well. We can look in the Linux console man page, which has a decent summary of the various instructions, the various control sequences. These are defined in various groups. So we have the simple instructions, which are one byte. We have instructions beginning with an esk, which are two bytes. These are fairly esoteric and mostly private, but the main ones are these. The CSI sequences. These are an escape followed by an open square bracket followed by some numeric parameters specified in decimal, followed by a byte indicating what it is that's actually happening. Annoyingly, you can have as many numeric parameters as you like, or at least I haven't been able to find any documented limit to the number of numeric parameters. The only instruction that seems to use more than two is M here, which sets character attributes. But yeah, here's an example of instruction that turns the LEDs on and off. We're just gonna ignore this one because we don't have any. Here are the parameters to the M command. These numbers all do particular things and you can turn multiple of these on and off at once. So we are going to need to define some states. So the first thing I think is to, here under print ascii, we want to test for the simple commands. So that's fairly straightforward. There's not a lot that can go wrong here. Ignore beep, I think. So do backspace, compare with, let me see, are these all in order? 10, no, they're not. Okay, 27, set, state, escape. If it's not any of these, then it's a printable character. In fact, so we compare with 32, which is space. Anything with a value less than this is not printable. So the way compare works is it subtracts. It will compute A minus 32. So if A is less than 32, then that will overflow and the carry will be set. If it is 32 or above, it will not overflow. So if the carry is set, then do nothing. So at this point, actually we're not, this is called from elsewhere, so we might not be in the waiting state. So at this point, we know it's a printable character, so we print it. So none of these are defined. So do backspace, do tab, do line feed, do return. In fact, ret is wrong here, because this is, we actually wanted to set state, waiting for all of them. Okay, so how are we going to handle these? Well, we have two variables to specify where the cursor is. Backspace just goes back one. So does LD set flags? I don't think it does. LD, we want the opposite of that. Here it is. Does not set flags. You know, I want to use a ret there, because it would make life cheaper. So let's just call set state waiting here, which will enforce that we are in the waiting state. This then allows us to do ret z there, ret there, and all of these can be a ret, which is one byte rather than potentially three for a long call. So this allows us to do this. So if the cursor X, if cursor X is zero, then just return and do nothing. Is there a easier way to do this? No, that won't work. So decrement it, wait, why am I do loading? Why am I doing that? Cursor X, come away. Update cursor is going to be a routine that will update the hardware when the cursor changes. And in fact, that can be a tab advances to the next tab stop. And traditionally tabs are on eight character boundaries. This one actually wants to know how big the screen is. So we're going to define some values. So to do this, so this will advance to the next eight character boundary. We now want to compare it against the last character. So again, this is going to do eight minus W. So if A is less than W, carry will be set. So do nothing if carry is clear. Otherwise, update, right. Oh, there's several of these. Okay, we're going to have to do more of them. 10, 11, 12. So each of these compares is two bytes. We can actually optimize this somewhat. It's a little annoying. So what we do is we would subtract eight from here. Z is set, but now we want to compare against nines. All we need to do is do deck A. And then we can keep going. Here we need to subtract another value. And I think in the interests of, let me see. So I believe that what you do here is subtract 27 by 13. And then here it would be 32 minus 27. Okay, TTY update cursor is going to be a stub for now. So we have to make that a long call because there's now too much code in between the definition of the label and the JR statement for a two-byte jump. Okay, so our code is now bigger. Here's our print ASCII routine. So you can see the one-byte comparisons. On the 8080 and children, these conditional returns are very convenient for dense code and tend to encourage refactoring into subroutines. Of course, calling a subroutine is always the full three bytes. Anyway, right, do line feed. This moves the cursor down unless we are on the last line. At which point the cursor does not move but the screen scrolls instead and the last line is blank. So get the Y value. Are we on the last line? If so, scroll. Otherwise, simply move the cursor down once. You know, I could have sworn that, hmm. Anyway, scrolling down is actually going to be implemented with the insert line delete line primitives. The actual implementation, the thing that actually draws onto video memory, this is just a state machine, has to know how to do this. So let me just look to see how the, this is my ADM3 state machine. Forms a new line and then a carriage return. Carriage return is, let's just do carriage return. This just sets the cursor X to zero. And let's just rename this to match the old code. New line is special. So let's add this. New line is a routine that will do a line feed and then a carriage return. Oh yes, it does also need to be a home, but we'll do that later. Okay, here is our cursor down. Scroll here, right. What scroll does is it temporarily stores the old Y position, sets it to zero and deletes that line. So that's as good a way to do it as any. So get the address of the cursor Y variable, load it, sets it to zero, deletes that line and puts it back again. And TTY deletes line is a primitive. So let me see. In fact, we can combine a bunch of these line feeds together because there's three of them that prevents us from doing multiple comparisons, but I won't just yet. So the way that traditionally you do state machines is by ending each routine with a jump to the new state. But these are other conflicts from the fact that we want to use RETs where possible. So this is three bytes, which you kind of don't want to do. Anyway, let's just do, let's just do some debugging. So that prints an equals sign. This loads the cursor position into HL and that prints it. And that prints a D to delete line. So up here, we are actually going to do a new line. And let's see what happens when we run it. So the high byte is Y. So that has in fact moved from location zero to location from row zero to row one. Okay. Let's put us in row 23. That's the last row, 22. That's the second last row of the screen. So we've gone down one and if moved the cursor, that's what the equals means. We've gone down one again and it's scrolled. It's deleted the line at zero. There we go, D zero. So I think that worked. So what do we got here? Escape sequences. So we are actually going to do a whole set of more comparisons. Is it SD, in which case it is an ordinary line feed? So, and again, we know that we're going to end up, we're probably going to end up in state weightings. We're going to call that there. So a return here will consume the character we just got without doing anything. SK is new line. SK set tab stop at current column. Oh wow, I didn't realize that this thing had programmable tab stops. Let's just ignore that one for now. The terminal capabilities are way more than most programs use and we're going to need a custom term info file for this anyway so we can tell it not to do that. But, SKM reverse line feed. So I'm going to have to implement this one. So, this is essentially the, excuse me, let's do this the right way around. So, this is essentially the same code as for line feed, but in reverse. So we want to compare again zero if we're already on line zero, scroll down. Otherwise, decrement A and update the cursor. There's a lot of these, which we could probably abbreviate. One of these is three bytes. Okay, scroll down. Again, this is similar except we're going to use insert line rather than delete line. Push AF, save old Y. HL, zero, TTY, insert line, pop AF, cursor Y, HL. There is a ZMAC mode, which will automatically expand JRs to JPs on demand, which isn't documented, so don't worry about it for the time being, I'm just going to change all this stuff. Okay, so let's put ourselves on line one. We want a escape followed by a reverse line feed is an M. So what does this do? Right, it's moved us to line zero. Try it again, but on line zero. And we have inserted line zero. Good. That works. Okay, to deck private identification. This is used to, it causes the terminal to type back on the keyboard a sequence identifying it. So, how do we do this? This is in line, so we're going to do, so TTY type back HL is going to be the primitive that types back into the keyboard, whatever is pointed out by HL. So, to do this, we load the, we load the value, can we do that? Yes, we can do that. I want to compare it again, zero. Okay, so that's 27 by, that's a Z. Deck ID, what does this do? It did print the right thing. I'm not sure where that closed, closed square bracket came from, came from there. Okay, we have to use less to see what the emulator produced, because if I do this, then of course that escape sequence is interpreted by the, this terminal emulator here and it will be eaten. Okay, so man page, save and restore state. So, we don't, there's going to be more state coming. So, we don't know how much there is to save and restore. This is in fact not terribly difficult. So, let's just do, right now, open square bracket is a tricky one because this is going to start the, switch the state where we're reading the most complex commands. So, this is going to be set state CSI. CSI is what these things are called, command sequence introducer. We probably want to implement these just to make them go away because we won't implement them. Yes, let's do that. So, set state gobble one, means gobble one, bite and discard it. Set state one, okay. So, dev state gobble one, discard one, discard and go into waiting. Right, we haven't done that one. Once this is all over, it's worth shunting the code around to put things that call each other, frequently next to each other to reduce the overall code size. Okay, right, CSI. CSI is, oh, here we go. At most N part, 16 parameters. Wow, these are decimal numbers separated by semicolons. And the sequence of parameters may be perceived by a single question mark. This indicates that this is a special private sequence. Now, I believe, here's one here. I believe that the question mark doesn't actually do anything semantically. Or does it? Yes, it does. So, question mark 3H is not the same as no question mark 3H. Well, that's a pain. Okay, let's do some parameters. So, we have 16 slots for parameters. We have a single byte that counts the number. And we have a flags byte, which is where a question mark's going to live. There may be more we want to add later. When we enter CSI mode, you want to reset all these. So, we would really want to put this code in setStateCSI, but our macro makes it harder. So, we're actually going to do initStateCSI, which falls through into setStateCSI. And this is going to want to initialize all these values to zero. Now, we can do this relatively straightforwardly using LDIR, which is capable of resetting memory. See, transfers a byte of data from HL to DE. So, we're going to put this initStateCSI, start CSI state, end CSI state minus start CSI state minus one, like so. LDIR is basically memCopy. It copies from HL to DE. I always get this mixed up. From HL to DE, that's correct. So, we write a zero to where HL is. So, this means that LDIR will copy this zero to the one at DE. Then it will advance them both, so that HL is now pointing at the old DE and it will do it BC times. So, this will reset all our parameters to zero, which is a reasonably dense bit of code. Okay. We're now in the CSI state, where we actually have to parse this stuff. So, we're going to stay in this state until we receive a command byte. So, we want to see whether it's a, if it's a question mark, then set the question mark bit. If it's a digit, then we want to add it to the current parameter. To test to see if it's a digit, then we are going to exploit a feature of the ASCII character set, which is that all digits are in this range. Hmm. It's not quite as easy as this. Semicolon is actually special. So, let's just do that one first. So, to do this, we load the parameter count field. We compare it with the highest possible value it can have. Again, this is doing a minus value. Oh, no, we don't want, we just want to, yeah. If it's zero, that is, we are already on the last parameter. Don't do anything. This means that more than 16 parameters will just all overwrite the last one. That's as good as any count. Okay. So, detect a digit. Actually, at this point, we know that anything lower than a zero is invalid. So, just ignore that. I think if this happens, we just want to cancel and we just give up trying to process this escape sequence. Okay. So, we're now going to compare against nine plus one. That's going to have to be a P, I'm sure. Yes, it is. So, at this point, we know that the character has to be zero or up. So, by comparing against nine plus one, we know that if carry is set, this means it must be one of these. So, no carry continue. So, here we want to add this digit on to our current parameter. So, the first thing is we need to get the address of the specific parameter we are dealing with unless there's an easier way to do this. So, I think we want a current parameter value. Actually, no, no, let's not do that. What we are going to do is to add a helper routine, which is provided by the system called addahl. This adds the 8-bit zero extended value in A to the value in HL. This is a standard tool in the Z80 world. I also say Z80 addahl, which is this. So, having this ret nc here means that it's so much shorter to call out to a routine than it is to do it in line, even though there's only actual three instructions that do work. So, all this does is it adds A to the lower byte of HL, and if it carries increments H. If you actually had to do this in line, this would be a two byte jump. So, as it is, this is one, two, three, four, five bytes. Every time it's used, it's three bytes for the call. If you did it in line, it would have to be one, two, three, four, five bytes. Wait a minute, yes. So, you save two bytes by calling that each time you call it. So, what we're going to do is, in fact, param count is the wrong word. It wants to be current param, because the number of parameters is actually one plus that. This is pointing at the last parameter being written to. So, we fetch the current parameter. We shift it left by one by multiplying it by two. We get the address of the parameter block. We add the two together. This gives us the address of the current parameter in HL. And then, we want to load it in order to do work on it. So, now we have the value of the parameter in HL and its addresses in DE. Because we are adding on decimal digits, we need to multiply the current parameter by 10 and then add on the value, the digit we just received. So, to multiply by 10, we want to multiply by eight and add two. Hang on, let me do this again. So, x times 10 equals x times eight plus x times two. So, we have the parameter in HL, remember. So, we double it and stash a copy of the doubled value in BC. We double it again and again. And add on the parameter times two that we initially first thought of. The syntax is wrong like so. We now want to add on the digit which used to be saved in C until we trashed it here. So, our digit is in A. I'm sorry, our digit is in C. So, we put it into A. We subtract zero from it. This gives us the decimal value of the digit and we call add AHL. Now, we want to store it back again. Now, we've incremented HL here and that then got copied DE. So, DE is actually pointing to address plus one. That's fine. We swap things around so that the address is in HL and the parameter is in DE. Save the high byte, decrement HL again and save the low byte. And we're done. Okay, then the last byte, a combination of the last byte and the question mark flag tells it what it's actually going to do. So, the first thing we do is reset the state because we're not in state CSI anymore. Once we finish our processing, we're going to be in state waiting. So, we do this so we can do red. We then want to test the command flags bit to know whether we are private or not. If we're not there, if we're not private, then sorry, if it's not there, then it's private. Okay. So, I think at this point we want to do some testing. So, we want to print the parameters. Now, or at least print some of them. There's no point doing them all. Now, it would be, actually, you're now just going to simplify things slightly with a put C. A, D, C, C, 6, J, P, E, 5. Again, this is all debugging code. Now, we could do a loop here, but this is debugging code and that sounds like work. So, let's just do this. So, that just prints the first four parameters in hex. Okay. So, escape. Question mark. Open angle bracket. A1. A semicolon. A1. And then a 2. Or rather, A2. A semicolon. And a Q. Right. Now, what does this do when we run it? Because I forgot to call putparams. Right. Well, this is garbage. Lovely. Now, I have remembered to put brackets around this. So, it is loading the value at the parameter rather than the address of the parameter. Has it been reset correctly? Well, to do that, we call putparams here. Right. Those are all zeros. So, that means that this code is wrong. Well, we can call putparams here. So, we'll get the parameters after every byte. That 3.1 looks very much like the digit 1, to be honest. Okay. Well, we're going to have to figure out what's going on here. And to do this, we're going to use the debugger. So, the first thing we want to do is to figure out where CSI lives. State CSI. Here we are. Set state CSI is at address 1D0. So, we start up the emulator with debugger. We do trace 1. Actually, I don't want to put parameters there because that will produce lots of spammy tracing. So, trace on go. So, that has run the entire program tracing out the every instruction. Actually, let's put that through less. And that does not do... Okay. So, it needs to be... Yeah. It needs to be interactive. So, I'll do it with script. Script logs everything. So, run the debugger. Trace 1.go.exit. It's now generated a file called typescript containing the trace of what happened. So, here is where it called set state CSI. You can see here it is taking the address of CSI and stashing it in the variable. So, a bit further down. Here we go. 1D7 LDA A comma C. So, on entry, C is 3.1. That is our digit. So, compare with question mark. It's not a question mark. The jump is taken. Compare with semicolon. It's not a semicolon. The jump is taken. Compare with 3.0. That's a zero digit. JPC. The jump is not taken. Compare with 3A. The jump is not taken. Okay. We now think correctly that what we've got is a digit. We are here. Load the current parameter into A. It's a zero. Double it. Still a zero. Load the address of the parameters to HL. Add them. And HL doesn't change. 2F7. We load DE. The value we get is a zero, which is what we expect. HL has been incremented by one. Swap them. Wait a minute. Okay. Swap them. HL now contains the parameter value, which is a zero. Here's all our arithmetic. Here's what's wrong. Put our character into B, which is the high byte. Wait a minute. That should be an H. You can see here that the 3.1 is ending up in the upper byte of our parameter accumulator, which is all wrong. Okay. Let's try that. Okay. That seems not so correct, to be honest. It means it's different. Does the script take a single command? Nope. Okay. We're going to have to do this manually. Oh, wait a minute. Wait a minute. I did forget to put the minus D in. There we go. But this does seem to now spin forever. Right. It's jumping into nowhere. D7. Wait a minute. That's my program gone. That does not seem like valid code to me. Oh, I managed to overwrite it with the TypeScript. Okay. There we go. That's better. This one. Go. Okay. 1D7. Here is the top of our routine. So if we go down, we can see. So we're here. Push BC there. Push BC here. You can see it contains our digit. So we've multiplied HL by 10. Pop BC load A, C sub 30. This gives us 01, which is the right value. And now we add it to HL, which is here. And this is wrong. We are writing back our parameter in the wrong order. Okay. This looks like the right numbers. So we have written a 1 into the first parameter, which is this one. Then we write a 1, 2 into the second parameter. 12 is C in hex. So that's correct. The third parameter is empty and Q is then ignored. So now we just need to pause and execute the actual commands. So these are more complex than the other commands. So I'm just thinking how to do this. For most commands, the first parameter, Act as account. So that this command here seems to insert spaces. The number is the number of spaces to insert. This one moves the cursor up. Again, the number is the number of movements to make. But if the parameter is empty or zero, it moves once. So how are we going to do this? Like this, I believe. So copy our character into C, which should still be there. Compare with... So this will then load the parameter. We are then going to load the address of a routine to do it and call a routine that is actually going to repeatedly call this that many times. Hang on, we don't need to do that. We know that the parameter is always going to be in parameter zero. So do CSI, repeat it as zero. So we are going to make an unjustified assumption that there are going to be fewer than 256 motions, because that's the only real thing that makes sense. Or... No, actually, we're not going to do that. I mean, something like ICH does need to be repeated, but something like CUU here, like Move Cursor Up, would be done in a single pass. So this is going to want to be... Load the number of blank characters into B. Save that on the stack because it's probably going to be about to be corrupted. Call the routine to print it and print it that many times. So where does print ASCII get its value from? It's in C. We probably don't want to call print ASCII because it's going to go through this stuff. So let's just do... Let's add a routine here. Print Printable. So Move Up, Move Cursor Up. So the interesting thing about these cursor motions, which isn't actually mentioned here, is that if it hits the edge of the screen, it doesn't scroll. So load the current cursor position into A. Subtract by the parameter, ignoring the top byte. No carry means it worked. Otherwise, A becomes zero and then update the cursor. CUD is cursor down, so it's the same code but different. Okay, so this is value minus screen height. That's 23. So any value that is less carry will be set. Any value which is greater, which is equal to or greater carry will not be set. Okay, let's do some testing. Okay, so cursor down. Now this is not actually going to do anything. It's moved the cursor nowhere, so it's still at 00. That is because these both default to... That's the wrong piece of code. These both default to zero. Just thinking how to do this. Yeah, this code can be cleaned up a lot, actually. Okay, so we put the parameter into A. If it is zero, we increment it. But we now want to do a reverse subtract. Because we want to subtract the parameter. Does this thing support negate? I think we can do a negate with... There we go, neg. So what does this do? Right, we're now on line one, which is correct. So if we, say, put a nine in, we should end up on line nine. We are A1. We are now on line one. Excellent. So now let's move up by nine. We should end up... We should see a motion to line one and then back to line zero again. That's not right. So I don't think this has done what I wanted. However... However... We should just be able to check to see if the resulting value is negative. Or rather, minus. It's... Yeah, there isn't a JR for this. So it's always the two byte version. I wonder... Yes, I think the carry flag is inverted here. We'll do some careful testing. Okay, that looks good. Line one, line zero. Should still be line zero. Yep. Yeah, I think that works. So let's try and do 99 downs. That should put us in the last line. Yep, that worked. Good. So forwards and backwards are very similar. They just work in... ...wards... ...backwards. We're using the x-coordinate rather than the y-coordinate. So we just duplicate all of this. I mean, literally, we just duplicate it. That wants to be... So forwards... ...backwards... No, wait. These are the wrong way around. Curse the backwards... ...and curse forwards. Forward... ...backwards. For f should be line 79... ...column 79. What's next then? CNL, move down the number of rows and go to column 1. That should be relatively easy to do. All we're going to do is... ...like this. So we literally just set x to 1 and then do a down. Move up the number of rows does coup and G move cursor to indicated column in current row wait wait we are indexing by zero so that what this calls column one our code is column zero so CSI char move to indicated column in current row do CSI jar so if this is a zero no hang on hang on we wish to convert this to the same numbering scheme that we're using and so if they don't provide a parameter or provide a zero treat that as column one by this terminology so we test for zero if it's not zero we decrement a then we compare with the screen width if it is a minus 79 78 is a carry 79 is not a carry okay now I am not sure whether this is correct actually because if you're comparing if you're doing a minus 24 and a equals 24 then the carry won't be set but zero will be set after doing the comparison no that's fine because if we're at screen width minus one then carry is not set so it will think that it's out of bounds and it will execute this but it won't actually change the value so it's actually it's logically incorrect but won't make a difference but this is the better code so that was char G so this should put us into column 79 it has this should put us into column 79 it right yes that is correct this is what the terminals one base numbering system thinks is the right hand column so this 79 is one less than that and that is correct so this should give us a zero which it does this should also give us a zero which it does and should this which it does good right ED raised display these erasers chunks of the screen but which chunk depends on the parameter and the default is cursor to end of display that's a zero then so we actually going to want to load this if it's a zero if it's a one if it's a two in fact if it's anything other than a two let's just do this just erase everything and this our terminal won't have any scroll back in fact the video chip we're using supports all kinds of really cool things it's got 8k of video memory you can have multiple windows you can have multiple banks you can pan around you can do smooth scrolling by pixel by pixel which well vertically which is really cool it seems to have been designed for doing exactly this kind of thing but actually using it all is more work than I'm willing to do raise from start to cursor raise from cursor to end arrays all now thinking about how these are going to be implemented in the back end the current ADM 3a implementation as that the arrays primitives are clear to EOS which is effectively cursor to end clear to end of screen and clear to end of line clear to EOL and you can tell the ADM 3 state machine to emulate these by printing spaces if you try to clear the entire screen it will try to use it will home and do clear to EOS if you have clear to EOL and no clear to EOS then you get the emulated clear to EOS but it uses clear to EOL to clear each line which is way faster than trying to print lots of spaces but if you don't have clear to EOL it will emulate that as well that's with this code here for the time being I think I am what am I doing clear to EOL is important for text editors because it happens quite a lot when you're editing a single line of text I think let's just have a clear line and then implement these in terms of that so to clear the entire screen we are going to load XY into cursor X save it restore it again later okay so this will clear X clear Y and leave HL pointing at Y we now have our loop we load Y increment Y load it compare it with screen height if it's not that loop so this should erase the entire screen but leave the cursor position unchanged so that was a 2 so here it is clearing the screen one line at a time now these are going to be similar I think we want to help a routine actually so we're going to write a routine where you give it the range of lines to arrays and it will do it so D becomes the first line E is the last line E is actually at this point one plus the last line but we want it to be inclusive so let's just decrement E so that should be an increment of E so now this code becomes DE is going to be the high byte goes into D which is 0 the low byte goes into E which is screen height minus one and fall through does this do the same thing no it doesn't because we need to save DE as well there we go and we also want to know we are going to have E being the line after the one we want to erase that is it's exclusive because this allows us to do you can be compare E ret Z so if the if we ask it to erase a null range then it does nothing because that will make this easier so start to cursor well the cursors online cursor why of course so this will erase all the screen except for the line the cursor is on and likewise here this will erase all lines all lines after the cursor except the one the cursor is on so if you look at the man page erase from start to cursor so if we put the cursor online 5 say then we can see that it has erased 0 1 2 3 and 4 but not 5 which is what we want because let's look at look at those console codes again because the next one is the is a similar command for erasing the line so we've got from cursor to end of line start a line to cursor and erase whole line so this means that from do ed start to cursor we erase everything above the cursor and then call do ed do yell start to cursor which handles dealing with the current line and likewise here we do cursor to end do yell cursor to end so compare with K yell raise line so if it says 0 it's cursor to end of line it's 1 start of line to cursor otherwise erase the whole line I'll fix this later JP versus JR is a pain okay so so we're going to start with raise current line which is do yell all line that's all that's needed the TTY back end knows which line we're on and it will do the rest from cursor to end so save the cursor and restore it again now does this actually erase the cursor the erase where the cursor is itself I think not I think but I think I'm actually going to have to go look up more detailed documentation on these in a moment so are we on the first row if we are give up no no I got to do this differently so X goes into L so let's stack that stash that in E so we set the cursor to zero no we don't we want to put this in B for reasons push BC LD C to space actually we can do that here push BC call print printable this will advance the cursor on by one and loop DJNZ decrements B and jumps if not zero so what this will do is it will home the cursor at the beginning of the line and keep printing spaces until it reaches the cursor position and then stop cursor to end is going to be similar but different print printable is going to increment the cursor so once we reach the end of the line it will the cursor will be screen width and go on hang on is this right yeah B is our count no we can do it better than this you can do it better than this okay so we calculate how many spaces we need to print and then otherwise it's the same code right so let's put let's put us at five comma five and let's do a yellow it starts to cursor so what do we see so it raises line zero to four it then prints five lines of garbage print printable is supposed to print the thing in C but it doesn't let's try that there we go line zero to raises line zero to four and prints five spaces we don't get a cursor motion because we don't call update cursor which we don't want to do here because we haven't like actually moved the cursor but let's just put it in for debugging purposes there we go high byte is y low byte is x it's moves us to zero and then we should be at five afterwards okay that is what we would expect to see a raise to end of screen so it erases these lines and it should then print well 80 minus five characters so let's just put our cursor at 575 here we go five spaces good well that's worked how big is our program now hmm it's growing it's growing this stuff is the fake implementation this is our state data and everything above this is the actual state machine well apart from a little bit of test program at the top so my throat is getting dry and it's dinner time so I think I'm going to break it here I'm going to have to come back again with more because wow there's a lot of this how much more of there it more of it is there well just in terms of the state machine we have these I'm going delete the indicated number of characters this will require either a primitive or not actually implementing it I think I would prefer to go with not implementing it to be honest I'm going to have to look up to see exactly what these means easy easy easy easy easy tab stops let's ignore those not implement them modes we can ignore attributes we will need to implement but that's not actually too complicated status report yeah this is this is I think we have most of what we really care about the mode switches I don't think we need to implement there's a lot of the standard that I don't think I care about and of course once we've done with the state machine you then have to start working the actual implementation that will be fun so I'm going to call a break see you later