├── imgs └── screen.png ├── Makefile ├── README.md ├── LICENSE └── src ├── main.asm ├── model.asm └── view.asm /imgs/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zeal8bit/Zepto/HEAD/imgs/screen.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | SRCS = main.asm model.asm view.asm 4 | BIN = zepto.bin 5 | 6 | # Directory where source files are and where the binaries will be put 7 | INPUT_DIR = src 8 | OUTPUT_DIR = bin 9 | 10 | # Include directory containing Zeal 8-bit OS header file. 11 | ifndef ZOS_PATH 12 | $(error "Please define ZOS_PATH environment variable. It must point to Zeal 8-bit OS source code path.") 13 | endif 14 | 15 | ZOS_INCLUDE = $(ZOS_PATH)/kernel_headers/z88dk-z80asm/ 16 | 17 | # Assembler binary name 18 | ASM = z88dk-z80asm 19 | # Assembler flags 20 | ASMFLAGS = -m -b -I$(ZOS_INCLUDE) -O$(OUTPUT_DIR) 21 | 22 | # If z88dk has been install through snap, the binary may be prefixed with "z88dk" 23 | # So choose any of z88dk-dis or z88dk.z88dk-dis, as long as one exists 24 | DISASSEMBLER=$(shell which z88dk-dis z88dk.z88dk-dis | head -1) 25 | 26 | # Mark version.txt as PHONY to force generating it every time 27 | .PHONY: all version.txt 28 | 29 | all: version.txt $(OUTPUT_DIR) $(BIN) 30 | 31 | version.txt: 32 | git describe --always | tr "\n" " " > $@ 33 | 34 | $(BIN): $(addprefix $(INPUT_DIR)/, $(SRCS)) 35 | $(ASM) $(ASMFLAGS) -o$@ $^ 36 | mv $(OUTPUT_DIR)/*_TEXT.bin $(OUTPUT_DIR)/$@ 37 | $(DISASSEMBLER) -mz80 -o 0x4000 -x $(OUTPUT_DIR)/zepto.map $(OUTPUT_DIR)/$@ > $(OUTPUT_DIR)/zepto.dump 38 | 39 | $(OUTPUT_DIR): 40 | mkdir -p $@ 41 | 42 | clean: 43 | rm -rf bin/ version.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Zepto text editor

2 |

3 | Screenshot 4 |

5 |

6 | 7 | Licence 8 | 9 |

Presenting Zepto: A clone of nano entirely written in Z80 assembly, for Zeal 8-bit OS

10 |

11 | 12 | 13 | ## About Zepto 14 | 15 | The goal of Zepto is to provide a text editor for [Zeal 8-bit OS](https://github.com/Zeal8bit/Zeal-8-bit-OS) that is complete enough to write some code of text files, without taking too much disk space. 16 | This is why I decided to make a clone of the famous `nano` text editor present on Linux. The problem is `nano` is written in C and is tied to the libC and Linux kernel. As such, I decided to write a clone from scratch, in Z80 assembly, to minimize the space and time it will take. 17 | 18 | Thus, `nano` was only used as a source of **inspiration**, its code was not used **at all** in this project. 19 | 20 | ## Features and limitations 21 | 22 | Zepto is still in alpha, here are the features implemented: 23 | 24 | * Less than **4KB** once compiled! 25 | * Use Zeal 8-bit OS cursor and keyboard API, so completely portable 26 | * Use *Gap Buffer* data structure to avoid line length limitation 27 | * Use Controller-Model-View architecture 28 | * Open an existing file given as a parameter 29 | * Edit the file content, can split lines, can add characters at the beginning, at the middle and at the end of lines 30 | * Support upper, lower case and special characters 31 | * Support scrolling up and down in the file 32 | * Support `Ctrl + Key` for menu selection 33 | * Support `Location` option 34 | * Support `Save file` option 35 | * Support `Exit` option 36 | * Support "Save file on exit" question 37 | 38 | Regarding the current limitations: 39 | 40 | * Screen size is hardcoded to 80x40 characters. In the future, it should use Zeal 8-bit OS video API to get the screen area size 41 | * Doesn't support memory paging, so the maximum file size is limited by the amount of RAM mapped at once: around 40KB 42 | * No way to remove an empty line. Once a new line is created, it cannot be removed, it'll always be in the file 43 | * It is not possible to modify the file destination once the file is opened. For example, if the file `B:/file.txt` is opened, it is not possible to save it under a new name 44 | * Copy/Paste/Cut/Help/Open File/New File are not implemented 45 | 46 | ## Building the project 47 | 48 | ### Requirements 49 | 50 | At the moment, the project has only been assembled on Linux (Ubuntu 20.04 and 22.04), it should be compatible with Mac OS and Windows as long as you have: 51 | 52 | * bash 53 | * git (to clone this repo) 54 | * make 55 | * Zeal 8-bit OS source code. Only the `kernel_headers` directory is required 56 | * Zeal 8-bit OS v0.3.0 or above on the target computer. The older versions don't implement cursor and video APIs 57 | * z88dk v2.2 (or later). Only its assembler, `z80asm`, is strictly required. The latest version of `z80asm` must be used as earlier versions don't have support for `MACRO`. 58 | 59 | To install z88dk, please [check out their Github project](https://github.com/z88dk/z88dk). 60 | 61 | ### Building 62 | 63 | To build the program, define the path to Zeal 8-bit OS, this will let us find the header files used to assemble zepto: 64 | ``` 65 | export ZOS_PATH=/your/path/to/Zeal8bitOS 66 | ``` 67 | Then simply use the command: 68 | ``` 69 | make 70 | ``` 71 | 72 | After compiling, the folder `bin/` should contain the binary `zepto.bin`. This file can be then loaded to Zeal 8-bit OS through UART thanks to the `load` command. 73 | 74 | The binary can also be embedded within the `romdisk` that will contain both the OS and a read-only file system. For example: 75 | ``` 76 | cd $ZOS_PATH 77 | export EXTRA_ROMDISK_FILES="/path/to/zepto/bin/zepto.bin" 78 | make 79 | ``` 80 | 81 | More info about compiling Zeal 8-bit OS [here](https://github.com/Zeal8bit/Zeal-8-bit-OS#getting-started). 82 | 83 | The resulted ROM image can then be provided to an emulator or directly flashed to the computer's ROM that will use it. 84 | 85 | ## Usage 86 | 87 | If you are using Zeal 8-bit OS default romdisk, you can use `exec` command to execute `zepto.bin` or directly use `./`, followed by the source/destination file: 88 | ``` 89 | A:/>exec zepto.bin B:/your_file.txt 90 | ``` 91 | or 92 | ``` 93 | A:/>./zepto.bin B:/your_file.txt 94 | ``` 95 | 96 | As said above, keep in mind that once opened, the file cannot be renamed or saved somewhere else, so make sure you specify a destination disk that is **write-read**! (`A:/` disk isn't by default) 97 | 98 | 99 | ## Contact 100 | 101 | For any suggestion or request, you can contact me at contact [at] zeal8bit [dot] com 102 | 103 | Or, you can join Zeal 8-bit projects [Discord server](https://discord.gg/UzEjwRvBBb). 104 | 105 | For features requests, you can also open an issue or a pull request. 106 | 107 | ## Contributing 108 | 109 | Contributions are welcome! Feel free to fix any bug that you may see or encounter, or implement any feature that you find important. 110 | 111 | To contribute: 112 | * Fork the Project 113 | * Create your feature Branch (*optional*) 114 | * Commit your changes. Please make a clear and concise commit message (*) 115 | * Push to the branch 116 | * Open a Pull Request 117 | 118 | 119 | (*) A good commit message is as follow: 120 | ``` 121 | Module: add/fix/remove a from b 122 | 123 | Explanation on what/how/why 124 | ``` 125 | For example: 126 | ``` 127 | Flash: add the possibility to flash programs bigger than 48KB 128 | ``` 129 | 130 | ## License 131 | 132 | Distributed under the Apache 2.0 License. See `LICENSE` file for more information. 133 | 134 | You are free to use it for personal and commercial use, the boilerplate present in each file must not be removed. 135 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main.asm: -------------------------------------------------------------------------------- 1 | ; SPDX-FileCopyrightText: 2023 Zeal 8-bit Computer 2 | ; 3 | ; SPDX-License-Identifier: Apache-2.0 4 | 5 | INCLUDE "zos_sys.asm" 6 | INCLUDE "zos_keyboard.asm" 7 | 8 | ; Linker script part 9 | SECTION TEXT 10 | ORG 0x4000 11 | SECTION DATA 12 | SECTION BUFFER 13 | ; Program part 14 | 15 | 16 | EXTERN editor_set_end_memory 17 | EXTERN editor_view_init 18 | EXTERN editor_model_init 19 | EXTERN model_show_location 20 | EXTERN model_save_file 21 | EXTERN _buffer_insert_char_at_cursor 22 | EXTERN _buffer_insert_new_line 23 | EXTERN _buffer_remove_char_at_cursor 24 | EXTERN _buffer_previous_char 25 | EXTERN _buffer_next_char 26 | EXTERN _buffer_previous_line 27 | EXTERN _buffer_next_line 28 | EXTERN editor_show_notification 29 | 30 | DEFC FLAGS_CAPS_MASK = 1 << 0 31 | DEFC FLAGS_SHIFT_MASK = 1 << 1 32 | DEFC FLAGS_CTRL_MASK = 1 << 2 33 | 34 | ; Entry point of the program 35 | ; Parameters: 36 | ; DE - String parameter (NULL-terminated), must be ignored if BC is 0 37 | ; BC - Length of the string parameter 38 | SECTION TEXT 39 | _start: 40 | ; Before processing any parameter, let's estimate the size of free RAM we have. 41 | ; As this program can be executed on a target than has no MMU, the stack pointer 42 | ; may not be 0xFFFF. So, we need to calculate the last byte address of memory. 43 | ld hl, 0 44 | add hl, sp 45 | ; Allocate 256 bytes for the stack, should be enough, if any problem is encountered, it 46 | ; should be increased. 47 | dec h 48 | dec hl 49 | call editor_set_end_memory 50 | ; Process the argument if any 51 | call process_argument 52 | push de 53 | ; Start by drawing the menu bar 54 | call editor_view_init 55 | pop de 56 | push de 57 | call editor_model_init 58 | call _editor_controller_init 59 | ; If a file was provided, no need to write the test data 60 | pop de 61 | ld a, d 62 | or e 63 | jp _editor_main_loop 64 | 65 | ; Draw the different element of the editor, they must be updated after each action 66 | ; of the user 67 | ld ix, _controller_print_printable 68 | ld iy, _editor_event_process_special_ascii 69 | _editor_main_loop: 70 | ; Load the routine to jump to in IX if the character received is a printable one 71 | ; and the one to jump to if it is a special ASCII character, in IY 72 | ld ix, _controller_print_printable 73 | ld iy, _editor_event_process_special_ascii 74 | call _editor_wait_for_event 75 | jp _editor_main_loop 76 | 77 | 78 | ; Parameters: 79 | ; DE - Address of the NULL-terminated string 80 | ; BC - Length of the string 81 | ; Returns: 82 | ; DE - Processed argument, NULL if not valid 83 | process_argument: 84 | ld a, b 85 | or c 86 | jr z, process_argument_error 87 | ; Left trim the parameter 88 | ex de, hl 89 | call ltrim 90 | jr z, process_argument_error 91 | ; If the argument is made out of several argument, split and keep the first 92 | push hl 93 | call strsep 94 | pop de 95 | ret 96 | process_argument_error: 97 | ; Set DE to 0 98 | ld d, a 99 | ld e, a 100 | ret 101 | 102 | ; Parameters: 103 | ; HL - Address of the string to separate 104 | ; Alters: 105 | ; A, HL 106 | strsep: 107 | inc hl 108 | ld a, (hl) 109 | or a 110 | ret z 111 | sub ' ' 112 | jp nz, strsep 113 | ; Character is space, replace it with 0 (A) 114 | ld (hl), a 115 | ret 116 | 117 | ; Parameters: 118 | ; HL - Address of the string to trim 119 | ; Returns: 120 | ; HL - Address of the first non-space character 121 | ; Z - The resulted string is empty 122 | ; NZ - Resulted string is not empty 123 | ltrim: 124 | ld a, (hl) 125 | or a 126 | ret z 127 | cp ' ' 128 | ret nz 129 | inc hl 130 | jp ltrim 131 | 132 | 133 | ; Routine returning the length of a NULL-terminated string 134 | ; Parameters: 135 | ; HL - NULL-terminated string to get the length from 136 | ; Returns: 137 | ; BC - Length of the string 138 | ; Alters: 139 | ; A, BC 140 | PUBLIC strlen 141 | strlen: 142 | push hl 143 | xor a 144 | ld b, a 145 | ld c, a 146 | _strlen_loop: 147 | cp (hl) 148 | jr z, _strlen_end 149 | inc hl 150 | inc bc 151 | jr _strlen_loop 152 | _strlen_end: 153 | pop hl 154 | ret 155 | 156 | 157 | ; Function copying src string into dest, including the terminating null byte 158 | ; At most BC characters are copied 159 | PUBLIC strncpy 160 | strncpy: 161 | _strncpy_loop: 162 | ld a, (hl) 163 | ldi 164 | or a 165 | ret z 166 | ld a, b 167 | or c 168 | ret z 169 | jp _strncpy_loop 170 | 171 | 172 | ; Return the basename of a path 173 | ; Parameters: 174 | ; HL - Address of the file path, which can be relative or absolute 175 | ; Returns: 176 | ; HL - Address of the file basename 177 | ; Alters: 178 | ; A, HL, BC 179 | PUBLIC basename 180 | basename: 181 | ; BC will contain the address of the last slash + 1 182 | ld b, h 183 | ld c, l 184 | _basename_loop: 185 | ld a, (hl) 186 | inc hl 187 | or a 188 | jr z, _basename_end 189 | cp '/' 190 | jr nz, _basename_loop 191 | ; Found a slash, copy the current incremented HL to BC 192 | ld b, h 193 | ld c, l 194 | jr _basename_loop 195 | _basename_end: 196 | ; Copy the basename address in HL and return 197 | ld h, b 198 | ld l, c 199 | ret 200 | 201 | 202 | ;=========================================================; 203 | ;================== CONTROLLER ==========================; 204 | ;=========================================================; 205 | 206 | _editor_controller_init: 207 | ; Set STDIN to raw input 208 | ld h, DEV_STDIN 209 | ld c, KB_CMD_SET_MODE 210 | ld e, KB_MODE_RAW 211 | IOCTL() 212 | or a 213 | ret z 214 | ; Error, the target doesn't support RAW mode 215 | S_WRITE3(DEV_STDOUT, _editor_raw_err_str, _editor_raw_err_str_end - _editor_raw_err_str) 216 | EXIT() 217 | _editor_raw_err_str: 218 | DEFM "Could not switch input to RAW mode\n" 219 | _editor_raw_err_str_end: 220 | 221 | 222 | is_print: 223 | ; Printable characters are above 0x20 (space) and below 0x7F 224 | cp ' ' 225 | ret c 226 | cp 0x7F 227 | ccf 228 | ret 229 | 230 | 231 | _editor_wait_for_event: 232 | ; Get the key pressed from the keyboard 233 | S_READ3(DEV_STDIN, _editor_keys, _editor_keys_end - _editor_keys) 234 | _event_received: 235 | ld a, b 236 | or c 237 | jr z, _editor_wait_for_event 238 | _editor_event_next: 239 | ; We received at least one character, process it 240 | ld a, (de) 241 | inc de 242 | cp KB_RELEASED 243 | jr nz, _editor_event_not_ignore 244 | ; Make the assumption that the released key follows directly 245 | dec c 246 | ld a, (de) 247 | call controller_check_if_special 248 | ret z 249 | inc de 250 | dec c 251 | ret z 252 | jp _editor_event_next 253 | _editor_event_not_ignore: 254 | push bc 255 | push de 256 | call _editor_event_process_char 257 | pop de 258 | pop bc 259 | dec c 260 | jp nz, _editor_event_next 261 | ret 262 | 263 | 264 | ; Parameters: 265 | ; A - Character to test 266 | ; Returns: 267 | ; A - 0 if the character is not special 268 | ; > 0 if it is 269 | controller_check_if_special: 270 | cp KB_LEFT_SHIFT 271 | jr z, _controller_toggle_shift 272 | cp KB_RIGHT_SHIFT 273 | jr z, _controller_toggle_shift 274 | cp KB_LEFT_CTRL 275 | jr z, _controller_toggle_ctrl 276 | cp KB_RIGHT_CTRL 277 | jr z, _controller_toggle_ctrl 278 | ; Return 0, it was not a special character 279 | xor a 280 | ret 281 | 282 | _controller_toggle_ctrl: 283 | ld a, FLAGS_CTRL_MASK 284 | jr _controller_toggle 285 | _controller_toggle_shift: 286 | ld a, FLAGS_SHIFT_MASK 287 | jr _controller_toggle 288 | _controller_toggle_caps: 289 | ld a, FLAGS_CAPS_MASK 290 | _controller_toggle: 291 | ld hl, _editor_flags 292 | xor (hl) 293 | ld (hl), a 294 | ret 295 | 296 | 297 | 298 | ; Process the keyboard key stored in A register: 299 | ; Parameters: 300 | ; A - Key code to process 301 | ; IX - Routine to jump to if A is a printable character 302 | ; IY - Routine to jump to if A is a special ASCII character 303 | ; Returns: 304 | ; None 305 | ; Alters: 306 | ; A, BC, DE, HL 307 | _editor_event_process_char: 308 | ; Copy character in B 309 | ld b, a 310 | call is_print 311 | ; C flag not set is A is printable 312 | ; jp nc, _controller_print_printable 313 | jp c, _editor_event_process_not_printable 314 | jp (ix) 315 | _editor_event_process_not_printable: 316 | cp KB_CAPS_LOCK 317 | jr z, _controller_toggle_caps 318 | ; Check if the character is special, i.e. CTRL or SHIFT 319 | call controller_check_if_special 320 | ; Return A > 0 if it was, we can return directly then 321 | or a 322 | ; This RET will be replaced by the routines that need to receive user input without interpreting them 323 | ret nz 324 | jp (iy) 325 | 326 | ; Jump to this branch if the keyboard key/character to process is an ASCII 327 | ; special key, such as newline, backspace, arrows, etc... 328 | ; Parameters: 329 | ; B - ASCII special character 330 | _editor_event_process_special_ascii: 331 | ; Put back the character value in A 332 | ld a, b 333 | cp KB_KEY_TAB 334 | jr z, _controller_insert_tab 335 | cp KB_KEY_ENTER 336 | jp z, _buffer_insert_new_line 337 | cp KB_KEY_BACKSPACE 338 | jp z, _buffer_remove_char_at_cursor 339 | cp KB_LEFT_ARROW 340 | jp z, _buffer_previous_char 341 | cp KB_RIGHT_ARROW 342 | jp z, _buffer_next_char 343 | cp KB_UP_ARROW 344 | jp z, _buffer_previous_line 345 | cp KB_DOWN_ARROW 346 | jp z, _buffer_next_line 347 | ret 348 | 349 | _controller_insert_tab: 350 | ld b, ' ' 351 | call _buffer_insert_char_at_cursor 352 | ld b, ' ' 353 | jp _buffer_insert_char_at_cursor 354 | 355 | 356 | ; A, B - Character to print 357 | _controller_print_printable: 358 | ; If caps lock XOR shift is 1, look for the alternate keys 359 | ld hl, _editor_flags 360 | ld a, (hl) 361 | ; If CTRL key is pressed, interpret the key differently 362 | and FLAGS_CTRL_MASK 363 | jr nz, _controller_ctrl_combination 364 | ld a, (hl) 365 | ; Put LSB in D 366 | ld d, 0 367 | rrca 368 | rl d 369 | and 1 370 | xor d 371 | ; If the result is 0, no need to look for the alternate set 372 | jp z, _buffer_insert_char_at_cursor 373 | ; Else, we have to get the alternate set of keys 374 | ld a, b 375 | ; Check if it starts before or after 0x5B 376 | cp 0x5b 377 | jp c, _controller_print_printable_before 378 | ld hl, alternate_key_set_from_bracket 379 | sub 0x5b 380 | jp _controller_print_printable_table 381 | _controller_print_printable_before: 382 | ld hl, alternate_key_set_from_space 383 | sub 0x20 384 | _controller_print_printable_table: 385 | add l 386 | ld l, a 387 | adc h 388 | sub l 389 | ld h, a 390 | ld b, (hl) 391 | jp _buffer_insert_char_at_cursor 392 | 393 | 394 | ; A CTRL + Key combination was pressed, interpret it here 395 | ; Parameter: 396 | ; B - Character pressed with CTRL (printable char) 397 | ; Returns: 398 | ; - 399 | ; Alters: 400 | ; A, BC, DE, HL 401 | _controller_ctrl_combination: 402 | ld a, b 403 | cp KB_KEY_L 404 | jp z, model_show_location 405 | cp KB_KEY_S 406 | jp z, model_save_file 407 | cp KB_KEY_K 408 | jr z, _controller_ask_and_exit 409 | ; Unsupported feature 410 | ld hl, str_unsupported 411 | jp editor_show_notification 412 | 413 | 414 | ; Routine called before exiting, it will ask the user whether he wants to save the file or not 415 | _controller_ask_and_exit: 416 | ; User wants to exit the program, ask whether to save the file or not 417 | call controller_ask_save_file 418 | ; If A is 0, do not save, simply exit 419 | ; If A is 1, save the file 420 | ; If A is 2, cancel 421 | or a 422 | jr z, controller_exit 423 | dec a 424 | jp z, controller_save_exit 425 | controller_cancelled: 426 | ; Exit cancelled, print a message and continue 427 | ld hl, str_cancelled 428 | jp editor_show_notification 429 | controller_save_exit: 430 | call model_save_file 431 | controller_exit: 432 | EXIT() 433 | 434 | ; Ask the user whether he wants to save the file or not 435 | ; Returns: 436 | ; A - 0, do not save, simply exit 437 | ; 1, save the file 438 | ; 2, cancel the operation 439 | controller_ask_save_file: 440 | ld hl, str_choices 441 | call editor_show_notification 442 | ; Re-use the regular even loop to wait on Y or N or C 443 | ld ix, controller_ask_save_file_printable 444 | ld iy, controller_ask_save_file_printable_ret 445 | ; Empty the response 446 | ld a, -1 447 | ld (_controller_ask_response), a 448 | ; Call to the event loop routine 449 | controller_ask_save_file_loop: 450 | call _editor_wait_for_event 451 | ; Check the response 452 | ld a, (_controller_ask_response) 453 | call controller_check_response 454 | ; Returns 0, 1, 2 or -1 if invalid 455 | or a 456 | ; If positive, return directly 457 | ret p 458 | jp controller_ask_save_file_loop 459 | 460 | 461 | ; Decode teh response contained in register A 462 | ; Parameters: 463 | ; A - ASCII character corresponding to a response 464 | ; Returns: 465 | ; A - 0 for Yes, 1 for No, 2 for Cancel, -1 else 466 | controller_check_response: 467 | cp 'y' 468 | jr z, controller_ask_save_yes 469 | cp 'n' 470 | jr z, controller_ask_save_no 471 | cp 'c' 472 | jr z, controller_ask_save_cancel 473 | ld a, -1 474 | ret 475 | controller_ask_save_no: 476 | ld a, 0 477 | ret 478 | controller_ask_save_yes: 479 | ld a, 1 480 | ret 481 | controller_ask_save_cancel: 482 | ld a, 2 483 | ret 484 | 485 | ; Called from the event loop! 486 | ; Parameters: 487 | ; A, B - Keyboard key code/ASCII character 488 | controller_ask_save_file_printable: 489 | ld (_controller_ask_response), a 490 | controller_ask_save_file_printable_ret: 491 | ret 492 | _controller_ask_response: DEFS 1 493 | 494 | 495 | str_unsupported: DEFM "Not implemented", 0 496 | str_cancelled: DEFM "Cancelled", 0 497 | str_choices: DEFM "Save the file? Y(es) / N(o) / C(ancel)", 0 498 | alternate_key_set_from_space: ; Characters starting at 0x20 499 | DEFM " !\"#$%&\"()*+<_>?)!@#$%^&*(::<+>?" 500 | alternate_key_set_from_bracket: ; Characters starting at 0x5B 501 | DEFM "{|}^_~ABCDEFGHIJKLMNOPQRSTUVWXYZ" 502 | 503 | 504 | SECTION DATA 505 | _editor_keys: DEFS 32 506 | _editor_keys_end: 507 | DEFS 1 ; For debugging so that symbols are correct 508 | _editor_flags: DEFM 0 509 | 510 | 511 | SECTION BUFFER 512 | ; By adding a newline before the buffer, finding the length of the first line 513 | ; can use the same algorithm as the other lines 514 | DEFM "\n" 515 | ; Start of the buffer, must be the last symbol of the binary! 516 | PUBLIC _start_buffer 517 | _start_buffer: 518 | 519 | -------------------------------------------------------------------------------- /src/model.asm: -------------------------------------------------------------------------------- 1 | ; SPDX-FileCopyrightText: 2023 Zeal 8-bit Computer 2 | ; 3 | ; SPDX-License-Identifier: Apache-2.0 4 | 5 | INCLUDE "zos_sys.asm" 6 | 7 | DEFC EDITOR_MAX_COL = (80 - 1) 8 | 9 | EXTERN _editor_view_insert_string_at_cursor 10 | EXTERN _editor_view_insert_at_cursor 11 | EXTERN _editor_view_insert_string_before_cursor 12 | EXTERN _editor_view_remove_at_cursor 13 | EXTERN _editor_view_left_cursor 14 | EXTERN _editor_view_right_cursor 15 | EXTERN _editor_view_clean_string_at_cursor 16 | EXTERN _editor_view_goto_newline 17 | EXTERN _editor_view_cursor_prev_line 18 | EXTERN _editor_view_cursor_prev_line 19 | EXTERN _editor_view_8bit_var 20 | EXTERN _editor_view_cursor_next_line 21 | EXTERN _editor_view_init_new_line 22 | EXTERN _editor_view_restore_cursor 23 | EXTERN editor_show_notification 24 | EXTERN _start_buffer 25 | EXTERN strlen 26 | ; Use this buffer as temporary one only! 27 | EXTERN _editor_view_buffer 28 | DEFC TEMPORARY_BUFFER = _editor_view_buffer 29 | 30 | ;=========================================================; 31 | ;===================== MODEL ============================; 32 | ;=========================================================; 33 | 34 | SECTION TEXT 35 | 36 | 37 | ; Text buffer address (RAM bank) 38 | DEFC BUFFER_START_ADDRESS = _start_buffer 39 | ; DEFC BUFFER_MAX_SIZE = 32*1024 40 | ; DEFC BUFFER_END_ADDRESS = _start_buffer + BUFFER_MAX_SIZE - 1 41 | DEFC BUFFER_MAX_LINE_LENGTH = 0xFFFF 42 | 43 | DEFC EDITOR_MAX_LINE = (40 - 1) - 4 44 | 45 | ; Set the end memory address. Used to determine how many bytes our work 46 | ; buffer will have. 47 | ; It also determines how big can be the file we are going to work on can be. 48 | ; Parameters: 49 | ; HL - End of memory address (last usable byte) 50 | PUBLIC editor_set_end_memory 51 | editor_set_end_memory: 52 | ; The last byte will be a '\n' to simplify the calculations, so reserve one byte 53 | dec hl 54 | ld (_buffer_memory_end), hl 55 | ret 56 | 57 | 58 | ; Initialize the buffers and read the file if any was given. 59 | ; Parameters: 60 | ; DE - File name to open 61 | PUBLIC editor_model_init 62 | editor_model_init: 63 | ld hl, _start_buffer 64 | ld (_buffer_left_addr), hl 65 | ld hl, (_buffer_memory_end) 66 | ld (_buffer_right_addr), hl 67 | ; To simplify the calculations for the last line, add a final \n 68 | inc hl 69 | ld (hl), '\n' 70 | ld hl, 1 71 | ld (_buffer_line_count), hl 72 | ld hl, 0 73 | ld (_buffer_line_num), hl 74 | ld (_buffer_col_num), hl 75 | ; Check if a file was given 76 | ld a, d 77 | or e 78 | ret z 79 | ; TODO: Check that the filename is valid? 80 | ; A file was given, open it, save the full path 81 | ld (_model_full_path), de 82 | ld b, d 83 | ld c, e 84 | ld h, O_RDONLY 85 | OPEN() 86 | ; If an error occurred, return 87 | or a 88 | jp m, _editor_model_open_error 89 | ; Save file descriptor in a static variable 90 | ld (_model_file_dev), a 91 | ; Check if the file is too big, use stat to get file info 92 | ld de, TEMPORARY_BUFFER 93 | ld h, a ; opened device descriptor 94 | DSTAT() 95 | or a 96 | jp nz, _editor_model_init_error 97 | ; Make sure the file size doesn't go above the limit 98 | ex de, hl 99 | ld e, (hl) 100 | inc hl 101 | ld d, (hl) 102 | ; DE contains the size of the file, calculate the maximum file size in HL: 103 | ; HL = BUFFER_END_ADDRESS - _start_buffer + 1 104 | ; BC is free to use here 105 | _free_size: 106 | ld bc, -_start_buffer + 1 107 | ld hl, (_buffer_memory_end) 108 | add hl, bc 109 | ; HL now contains the maximum number of bytes 110 | sbc hl, de ; C is 0 already 111 | jp c, _editor_model_init_error 112 | ; TODO: check if the entry is a directory (once d_flags is part of stat structure) 113 | ; We can start reading from the file, the buffer of destination is spread across 114 | ; three virtual pages, so we will perform `read` three times if necessary. 115 | ld de, BUFFER_START_ADDRESS 116 | ; First bank finishes at 0x8000 117 | ld bc, 0x8000 - BUFFER_START_ADDRESS 118 | push bc 119 | call _editor_model_read_bc 120 | ; We can now close the file 121 | ld a, (_model_file_dev) 122 | ld h, a 123 | CLOSE() 124 | ; DE contains the remaining size in the buffer, so the size read from the 125 | ; file is BC - DE 126 | or a ; clear carry flag 127 | pop hl 128 | sbc hl, de 129 | ; If BC is 0, we have nothing to read more from the file, else, we have to 130 | ; continue reading, inside the rest of the buffer. 131 | ld a, b 132 | or c 133 | ; TODO: Support copying to the next buffers 134 | jp nz, _editor_model_open_error 135 | ; Copy the file content to the end of the buffer 136 | ld de, (_buffer_memory_end) 137 | ; Copy the file size (number of bytes to copy) in BC 138 | ld b, h 139 | ld c, l 140 | ; Calculate the address of the last byte of the loaded file 141 | ld hl, BUFFER_START_ADDRESS 142 | add hl, bc 143 | ; HL points to the next free character, decrement it to point to the last char of the file 144 | dec hl 145 | lddr 146 | ; Copy the characters to the view, DE contains the address of the first char (-1) 147 | ; Set the right cursor to this value 148 | ld (_buffer_right_addr), de 149 | inc de 150 | ex de, hl 151 | ; Manually handle the first line 152 | call _model_line_show_lines 153 | ld (_buffer_cur_length), bc 154 | _model_lines_show_loop: 155 | ; Put next line address in DE 156 | ex de, hl 157 | ; We can continue if the address of the next line is not the end 158 | ld hl, (_buffer_memory_end) 159 | dec hl ; - 1 because we want C to be set if equal 160 | sbc hl, de 161 | jr c, _model_lines_show_loop_end 162 | ; Do not print the line to the screen if we reached the end of the screen 163 | ; (we are simply counting the total number of lines) 164 | ; Increment the total number of lines 165 | ld hl, (_buffer_line_count) 166 | inc hl 167 | ld (_buffer_line_count), hl 168 | ld a, h 169 | or a 170 | jr nz, _model_lines_show_loop 171 | or l 172 | cp EDITOR_MAX_LINE + 1 173 | ; Put back the next line address in HL before (potentially) jumping 174 | ex de, hl 175 | jr nc, _model_lines_show_loop 176 | call _model_line_show_lines 177 | jp _model_lines_show_loop 178 | _model_lines_show_loop_end: 179 | jp _editor_view_restore_cursor 180 | 181 | 182 | ; Parameter: 183 | ; HL - Address of the line to show 184 | ; B - Line iteration 185 | ; Returns: 186 | ; HL - Address of the next line 187 | ; C - Length of the line 188 | _model_line_show_lines: 189 | push hl 190 | ; Calculate the length of the first line 191 | call _editor_model_line_length 192 | ; Get the original line back 193 | ex (sp), hl 194 | ; Save the length on the stack 195 | push bc 196 | call _editor_view_init_new_line 197 | pop bc 198 | pop hl 199 | ret 200 | 201 | 202 | ; Parameters: 203 | ; HL - Address of the line 204 | ; Returns: 205 | ; BC - Length of the line 206 | ; HL - Address of the next line 207 | _editor_model_line_length: 208 | ld bc, 0 209 | ld a, '\n' 210 | push bc 211 | cpir 212 | ; Put next line address in DE 213 | ex de, hl 214 | pop hl 215 | xor a 216 | sbc hl, bc 217 | ; Do not count the \n in the line length 218 | dec hl 219 | ld b, h 220 | ld c, l 221 | ex de, hl 222 | ret 223 | 224 | 225 | _editor_model_read_bc: 226 | ; Put the opened file dev in H 227 | ld a, (_model_file_dev) 228 | ld h, a 229 | ; Save the maximum size to read on the stack 230 | push bc 231 | READ() 232 | ; BC contains the number of bytes read from the file 233 | pop hl 234 | or a 235 | jp nz, _editor_model_init_error 236 | ; Carry flag is 0 because of `or a` 237 | sbc hl, bc 238 | ; Put the result in DE 239 | ex de, hl 240 | ; If the result is 0, we have to read in the next buffer 241 | ret z 242 | ; If BC is 0, we finished reading the file 243 | ld a, b 244 | or c 245 | ret z 246 | ; Else, continue reading the file, maximum remaining size is in DE, 247 | ; HL is the destination of the buffer. 248 | add hl, bc 249 | ex de, hl 250 | ld b, h 251 | ld c, l 252 | jp _editor_model_read_bc 253 | 254 | 255 | _editor_model_open_error: 256 | ; If the error is "ERR_NO_SUCH_ENTRY", it's not really an error as we will 257 | ; create this file, so don't exit is that case 258 | cp -ERR_NO_SUCH_ENTRY 259 | ret z 260 | _editor_model_init_error: 261 | S_WRITE3(DEV_STDOUT, _editor_model_error_str, _editor_model_error_str_end - _editor_model_error_str) 262 | EXIT() 263 | 264 | _editor_model_error_str: 265 | DEFM "ERROR OPENING THE FILE\n", 0 266 | _editor_model_error_str_end: 267 | 268 | ; Get the size of the line after the cursor 269 | ; Parameters: 270 | ; None 271 | ; Returns: 272 | ; HL - Number of characters after the cursor on the current line 273 | ; Alters: 274 | ; HL, BC 275 | _buffer_size_right_line: 276 | ld hl, (_buffer_cur_length) 277 | ld bc, (_buffer_col_num) 278 | sbc hl, bc 279 | ret 280 | 281 | 282 | ; Generic function to insert a character in the buffer, at the left address 283 | ; The only check performed here is whether the file is full or not. 284 | ; No check is performed for the current line length, it shall be done by 285 | ; the caller. This routine also updates the _file_size global variable. 286 | ; Parameters: 287 | ; B - Character to print 288 | ; Returns: 289 | ; A - 0 if no shift on the view is required, 1 else 290 | ; HL - Right address + 1 (when A = 1) 291 | ; BC - Size of the rest of the line, from cursor to end of line (when A = 1) 292 | PUBLIC _buffer_generic_insert_char 293 | _buffer_generic_insert_char: 294 | ; - Total length has not been reached (i.e. left_addr != right_addr) 295 | ld hl, (_buffer_right_addr) 296 | ld de, (_buffer_left_addr) 297 | xor a 298 | sbc hl, de 299 | ret z 300 | ; Line is not full, neither is the buffer, add the character. Put left 301 | ; cursor address in HL (currently in DE) 302 | ex de, hl 303 | ld (hl), b 304 | ; Increment the left cursor and store it back 305 | inc hl 306 | ld (_buffer_left_addr), hl 307 | ; Before incrementing the rest of the fields, calculate the size of the right buffer. In other words, 308 | ; the number of characters on the same line but after the cursor 309 | call _buffer_size_right_line 310 | push hl 311 | ; Increment line's length, no need to check for overflow (already done) 312 | ld hl, (_buffer_cur_length) 313 | inc hl 314 | ld (_buffer_cur_length), hl 315 | ; Increment the col number 316 | ld hl, (_buffer_col_num) 317 | inc hl 318 | ld (_buffer_col_num), hl 319 | ; Return the right buffer address if the (right) size is not zero 320 | pop bc 321 | ld a, b 322 | or c 323 | ret z 324 | ld hl, (_buffer_right_addr) 325 | inc hl 326 | ret 327 | 328 | 329 | ; Insert the character in the split buffer, at the cursor position 330 | ; Tested: OK! 331 | ; Parameters: 332 | ; B - Character to print 333 | ; Alters: 334 | ; A, BC, DE, HL 335 | PUBLIC _buffer_insert_char_at_cursor 336 | _buffer_insert_char_at_cursor: 337 | ; Check that the current line length has not exceed the maximum size 338 | ld hl, (_buffer_cur_length) 339 | ld de, BUFFER_MAX_LINE_LENGTH 340 | or a 341 | sbc hl, de 342 | ; The current line is full, do nothing 343 | ret z 344 | ; We have to save BC as B contains the character to print! 345 | push bc 346 | ; Call the generic function that pushes the char into the buffer 347 | ; and returns the right_address + 1 in HL and the length of the rest 348 | ; of the line in BC 349 | call _buffer_generic_insert_char 350 | ; If returned A is 0, no need to shift the view 351 | or a 352 | jp z, _insert_no_shift 353 | ; Pop the character to print in A 354 | pop af 355 | ; Modify the behavior here to simplify writing to the screen 356 | dec hl 357 | ld (hl), a 358 | ; More character to print 359 | inc bc 360 | jp _editor_view_insert_string_at_cursor 361 | _insert_no_shift: 362 | ; Pop the character to print in A 363 | pop af 364 | jp _editor_view_insert_at_cursor 365 | 366 | 367 | ; Remove the char currently pointed by the cursor 368 | PUBLIC _buffer_remove_char_at_cursor 369 | _buffer_remove_char_at_cursor: 370 | ; Make sure the cursor is not at the beginning (this also happens if the line is empty) 371 | ; TODO: Support going to previous line here too 372 | ld hl, (_buffer_col_num) 373 | ld a, h 374 | or l 375 | ret z 376 | ; Before modifying the properties of the buffer, calculate the size of the right buffer 377 | call _buffer_size_right_line 378 | push hl 379 | ; Decrement the column 380 | ld hl, (_buffer_col_num) 381 | dec hl 382 | ld (_buffer_col_num), hl 383 | ; Decrement the line length 384 | ld hl, (_buffer_cur_length) 385 | dec hl 386 | ld (_buffer_cur_length), hl 387 | ; Line is not empty, decrement left pointer 388 | ld hl, (_buffer_left_addr) 389 | dec hl 390 | ld (_buffer_left_addr), hl 391 | ; To print this character to the view, we have to shift all the line left. 392 | ; It shall not be done if the size of the right buffer is 0. 393 | pop bc 394 | ld a, b 395 | or c 396 | jp z, _editor_view_remove_at_cursor 397 | ; We have to shift BC characters (exclude \n) from right_addr + 1 398 | ld hl, (_buffer_right_addr) 399 | inc hl 400 | jp _editor_view_insert_string_before_cursor 401 | 402 | 403 | ; Move the cursor of the buffer left down to the given cursor 404 | ; No check will be performed here. 405 | ; Parameters: 406 | ; HL - New cursor index 407 | ; Returns: 408 | ; - 409 | PUBLIC model_move_cursor_left_to 410 | model_move_cursor_left_to: 411 | ld de, (_buffer_col_num) 412 | ex de, hl 413 | or a 414 | sbc hl, de 415 | ret z 416 | ld (_buffer_col_num), de 417 | ld b, h 418 | ld c, l 419 | ld hl, (_buffer_left_addr) 420 | dec hl 421 | ld de, (_buffer_right_addr) 422 | lddr 423 | inc hl 424 | ld (_buffer_left_addr), hl 425 | ld (_buffer_right_addr), de 426 | ret 427 | 428 | 429 | ; Make the buffer cursor point to the previous character. 430 | ; This won't change the size of course. 431 | PUBLIC _buffer_previous_char 432 | _buffer_previous_char: 433 | ; Cursor shall not be at the beginning! 434 | ; TODO: Support going to the previous line 435 | ld hl, (_buffer_col_num) 436 | ld a, h 437 | or l 438 | ret z 439 | ; Decrement the cursor col position 440 | dec hl 441 | ld (_buffer_col_num), hl 442 | ; We can go back safely, save the new address of the left cursor, 443 | ; put the character in the right cursor and decrement right cursor. 444 | ld hl, (_buffer_left_addr) 445 | dec hl 446 | ld (_buffer_left_addr), hl 447 | ld a, (hl) 448 | ld hl, (_buffer_right_addr) 449 | ld (hl), a 450 | dec hl 451 | ld (_buffer_right_addr), hl 452 | ; Update the view accordingly 453 | jp _editor_view_left_cursor 454 | 455 | 456 | ; Make the buffer cursor point to the next character. 457 | PUBLIC _buffer_next_char 458 | _buffer_next_char: 459 | ; Same as the previous routine but with right cursor 460 | ld de, (_buffer_right_addr) 461 | ld hl, (_buffer_memory_end) 462 | ; Clear carry flag 463 | or a 464 | ; 16-bit 'add' doesn't update Z flag 465 | sbc hl, de 466 | ret z 467 | ; Put back the right address in HL 468 | ex de, hl 469 | ; Check that the next char is NOT a new line 470 | ; TODO: Go to the next line 471 | inc hl 472 | ld a, (hl) 473 | cp '\n' 474 | ret z 475 | ; We can go forward safely, save the new address of the right cursor, 476 | ; put the character in the left cursor and increment the left cursor. 477 | ld (_buffer_right_addr), hl 478 | ld hl, (_buffer_left_addr) 479 | ld (hl), a 480 | inc hl 481 | ld (_buffer_left_addr), hl 482 | ; Update the column number 483 | ld hl, (_buffer_col_num) 484 | inc hl 485 | ld (_buffer_col_num), hl 486 | ; Update the view accordingly 487 | jp _editor_view_right_cursor 488 | 489 | 490 | ; Insert a new line in the buffer at the cursor position. 491 | ; Tested: OK! 492 | ; Parameters: 493 | ; B - '\n' char value 494 | PUBLIC _buffer_insert_new_line 495 | _buffer_insert_new_line: 496 | ; Call the generic function that pushes \n into the buffer 497 | ; and returns the right_address + 1 in HL and the length of the rest 498 | ; of the line in BC 499 | call _buffer_generic_insert_char 500 | ; Store BC, rest of the line in _buffer_cur_length 501 | ld (_buffer_cur_length), bc 502 | push bc 503 | ; If returned A is 0, no need to shift the view 504 | or a 505 | call nz, _editor_view_clean_string_at_cursor 506 | ; Add a line to the count 507 | ld hl, (_buffer_line_count) 508 | inc hl 509 | ld (_buffer_line_count), hl 510 | ; Reset column number 511 | ld hl, 0 512 | ld (_buffer_col_num), hl 513 | ; Increment the line index 514 | ld hl, (_buffer_line_num) 515 | inc hl 516 | ld (_buffer_line_num), hl 517 | ; Retrieve the parameters from the stack and jump to the view routine 518 | pop bc 519 | ld hl, (_buffer_right_addr) 520 | inc hl 521 | jp _editor_view_goto_newline 522 | 523 | 524 | ; Get the address of the next line 525 | ; Parameters: 526 | ; None 527 | ; Returns: 528 | ; HL - Address of the next line 529 | ; Alters: 530 | ; A, BC, HL 531 | PUBLIC model_next_line_address 532 | model_next_line_address: 533 | ; Get the size of the line in the right buffer 534 | call _buffer_size_right_line 535 | ex de, hl 536 | ld hl, (_buffer_right_addr) 537 | ; Increment HL to point to the next valid character 538 | inc hl 539 | ; Set the carry to add 1 in the calculation below, to skip the \n 540 | scf 541 | adc hl, de 542 | ret 543 | 544 | 545 | ; Get the number of lines that follow the cursor, including the current one 546 | ; Tested: OK! 547 | ; Parameters: 548 | ; - 549 | ; Returns: 550 | ; HL - Number of lines remaining 551 | ; Alters: 552 | ; A, HL, DE 553 | PUBLIC model_get_remaining_lines 554 | model_get_remaining_lines: 555 | ld hl, (_buffer_line_count) 556 | ld de, (_buffer_line_num) 557 | xor a 558 | sbc hl, de 559 | ret 560 | 561 | 562 | ; Retrieve the the A-th previous line of the current buffer pointer 563 | ; Parameters: 564 | ; A - Index of the previous line to get (0 = current line, 1 = previous line, etc...) 565 | ; Returns: 566 | ; HL - Address of the A-th previous line 567 | ; Alters: 568 | ; A, HL, BC, E 569 | PUBLIC _buffer_previous_ath_line_address 570 | _buffer_previous_ath_line_address: 571 | ld hl, (_buffer_left_addr) 572 | dec hl 573 | ld e, a 574 | inc e 575 | ld a, '\n' 576 | _buffer_previous_ath_line_address_loop: 577 | ld bc, BUFFER_MAX_LINE_LENGTH + 1 578 | cpdr 579 | ret nz ; Should not happen 580 | dec e 581 | jp nz, _buffer_previous_ath_line_address_loop 582 | ; After CPDR, HL points to the char before the \n that was found, 583 | ; we want it to point to the beginning of the line, so increment it twice. 584 | inc hl 585 | inc hl 586 | ret 587 | 588 | 589 | ; Calculate the minimum between HL and BC and put the result in BC 590 | ; Parameters: 591 | ; HL - 16-bit value 592 | ; BC - 16-bit value 593 | ; Returns: 594 | ; BC - Minimum between HL and BC 595 | ; Alters: 596 | ; HL, BC 597 | _buffer_min_hl_bc: 598 | or a ; Clear carry flag 599 | sbc hl, bc 600 | ret nc 601 | ; HL was the minimum, restore it and store it in BC 602 | add hl, bc 603 | ld b, h 604 | ld c, l 605 | ret 606 | 607 | 608 | ; Get a pointer to characters located on the left of the 609 | ; cursor. 610 | ; The parameter BC must not exceed the size of the buffer, 611 | ; no check is performed here! 612 | ; Parameters: 613 | ; BC - Number of characters to get 614 | ; Returns: 615 | ; HL - Pointer to the string 616 | ; Alters: 617 | ; A, HL, BC 618 | PUBLIC model_get_bc_left_chars 619 | model_get_bc_left_chars: 620 | ld hl, (_buffer_left_addr) 621 | or a ; clear carry flag 622 | sbc hl, bc 623 | ret 624 | 625 | ; Get a pointer to characters located on the right of the 626 | ; cursor. 627 | ; Parameters: 628 | ; BC - Request size 629 | ; Returns: 630 | ; HL - Pointer to the string 631 | ; BC - Minimum between requested size and size of the rest of the line 632 | ; Alters: 633 | ; HL 634 | PUBLIC model_get_bc_right_chars 635 | model_get_bc_right_chars: 636 | push bc 637 | call _buffer_size_right_line 638 | pop bc 639 | call _buffer_min_hl_bc 640 | ld hl, (_buffer_right_addr) 641 | inc hl 642 | ret 643 | 644 | 645 | ; Fills the given buffer with current line content. 646 | ; Parameters: 647 | ; DE - Buffer to fill with the current line content 648 | ; BC - Maximum number of bytes to fill in DE 649 | ; Returns: 650 | ; DE - End of the buffer 651 | ; BC - Number of characters written 652 | ; Alters: 653 | ; A, BC, DE, HL 654 | PUBLIC model_get_current_line 655 | model_get_current_line: 656 | push bc 657 | ; Copy 'col' characters from left iterator 658 | ld hl, (_buffer_col_num) 659 | ld a, h 660 | or l 661 | jr z, model_get_current_line_right 662 | ; Make HL point to the beginning of the line 663 | push de 664 | ld de, (_buffer_left_addr) 665 | ex de, hl 666 | sbc hl, de ; Carry is 0 here 667 | ex de, hl 668 | ; HL - contains 'col' number 669 | ; BC - max number to write to caller buffer 670 | ; DE - start of current line buffer 671 | ; [SP] - User buffer 672 | ; Keep BC on the stack, and get the minimum between BC and HL, store it in BC 673 | push bc 674 | call _buffer_min_hl_bc 675 | ; Before actually start copying, subtract BC from the user buffer size 676 | pop hl 677 | xor a 678 | sbc hl, bc 679 | ; HL contains the user buffer remaining size after copying. Exchange it with user buffer. 680 | ex (sp), hl 681 | ; Copy BC bytes from current line (DE) to the user buffer (HL) 682 | ex de, hl 683 | ldir 684 | pop bc 685 | ; BC - Remaining size in user buffer 686 | ; DE - User buffer next free byte 687 | ; If there is no space anymore, return 688 | ld a, b 689 | or c 690 | jr z, model_get_current_line_return 691 | ; Calculate for the right buffer now 692 | ld hl, (_buffer_col_num) 693 | model_get_current_line_right: 694 | ; HL contains column index, calculate the length of the line in the right buffer 695 | push de 696 | ld de, (_buffer_cur_length) 697 | ex de, hl 698 | xor a 699 | sbc hl, de 700 | pop de 701 | ; If the result is 0, we have nothing more to write to the buffer 702 | jr z, model_get_current_line_calculate_return 703 | ; Calculate the minimum between HL (line size in right buffer) and BC (user buffer remaining size) 704 | call _buffer_min_hl_bc 705 | ; We have to write BC characters from right buffer 706 | ld hl, (_buffer_right_addr) 707 | inc hl 708 | ldir 709 | ; The total amount of bytes written is MIN(line_length, user_buffer_size) 710 | ld hl, (_buffer_cur_length) 711 | pop bc 712 | jp _buffer_min_hl_bc 713 | model_get_current_line_return: 714 | ; Pop original buffer size, to say that we filled the buffer 715 | pop bc 716 | ret 717 | model_get_current_line_calculate_return: 718 | ; Calculate user buffer original size - remaining size 719 | pop hl 720 | ; Carry is 0 if we reached this point 721 | sbc hl, bc 722 | ld b, h 723 | ld c, l 724 | ret 725 | 726 | 727 | ; Calculate the length of the previous line. 728 | ; Parameters: 729 | ; HL - Address of the \n finalizing the previous line 730 | ; Returns: 731 | ; HL - Length of the previous line 732 | _buffer_previous_line_length: 733 | ; Go past the previous line's \n 734 | dec hl 735 | ; The previous line is preceded by a \n, look for it 736 | ld bc, BUFFER_MAX_LINE_LENGTH + 1 737 | ld a, '\n' 738 | cpdr 739 | ; When found, length of line is BUFFER_MAX_LINE_LENGTH - BC 740 | ld hl, BUFFER_MAX_LINE_LENGTH 741 | sbc hl, bc 742 | ret 743 | 744 | 745 | ; Make the buffer cursor point to the previous line. 746 | ; If the previous line has less character than current line, the cursor 747 | ; will be placed at the end of the previous line. 748 | ; Parameters: 749 | ; None 750 | ; Returns: 751 | ; None 752 | PUBLIC _buffer_previous_line 753 | _buffer_previous_line: 754 | ; Make sure we are not at the first line 755 | ld hl, (_buffer_line_num) 756 | ld a, h 757 | or l 758 | ; Return if HL is 0 (meaning we are at the first line) 759 | ret z 760 | ; Update _buffer_line_num now 761 | dec hl 762 | ld (_buffer_line_num), hl 763 | ; Load the pointers 764 | ld hl, (_buffer_left_addr) 765 | dec hl 766 | ld de, (_buffer_right_addr) 767 | ld bc, (_buffer_col_num) 768 | ; Copy BC bytes from left buffer into right buffer 769 | ld a, b 770 | or c 771 | jr z, _buffer_previous_line_no_copy 772 | lddr 773 | _buffer_previous_line_no_copy: 774 | push hl 775 | call _buffer_previous_line_length 776 | ; Store the current line length 777 | ld (_buffer_cur_length), hl 778 | ; If the new line has the same amount or more characters than the current cursor index, do not move it, 779 | ; else, move it to the end of the line. 780 | ld bc, (_buffer_col_num) 781 | ; If the cursor is at the end, trigger a carry flag during 'sbc' 782 | scf 783 | sbc hl, bc 784 | jr nc, _buffer_previous_line_cursor_valid 785 | ; BC is bigger than the size, set the cursor to the end of the line 786 | adc hl, bc 787 | ld (_buffer_col_num), hl 788 | ; Pop the left buffer index from the stack 789 | pop hl 790 | ; Copy the ending \n, we can alter BC, it won't be used later 791 | ldd 792 | jp _buffer_previous_line_cursor_no_copy 793 | _buffer_previous_line_cursor_valid: 794 | ; HL contains size - cursor, copy that many bytes into right buffer (DE) 795 | ld b, h 796 | ld c, l 797 | ; Increment BC because we subtracted a carry too 798 | inc bc 799 | ; Increment again to integrate the previous line's \n in the copy 800 | inc bc 801 | ; Pop the left buffer index from the stack 802 | pop hl 803 | lddr 804 | _buffer_previous_line_cursor_no_copy: 805 | ; Make HL point to the next available spot 806 | inc hl 807 | ld (_buffer_left_addr), hl 808 | ld (_buffer_right_addr), de 809 | ld bc, (_buffer_col_num) 810 | jp _editor_view_cursor_prev_line 811 | 812 | 813 | ; Calculate the length of the next line. 814 | ; Parameters: 815 | ; HL - Address of the first character of next line 816 | ; Returns: 817 | ; HL - Length of the next line 818 | _buffer_next_line_length: 819 | ; The previous line is preceded by a \n, look for it 820 | ld bc, BUFFER_MAX_LINE_LENGTH + 1 821 | ld a, '\n' 822 | cpir 823 | ; When found, length of line is BUFFER_MAX_LINE_LENGTH - BC 824 | ld hl, BUFFER_MAX_LINE_LENGTH 825 | sbc hl, bc 826 | ret 827 | 828 | 829 | ; Make the buffer cursor point to the next line. 830 | ; If the next line has less character than current line, the cursor 831 | ; will be placed at the end of the next line. 832 | ; Parameters: 833 | ; None 834 | ; Returns: 835 | ; None 836 | PUBLIC _buffer_next_line 837 | _buffer_next_line: 838 | ; Make sure we are not at the last line 839 | ld bc, (_buffer_line_num) 840 | ld hl, (_buffer_line_count) 841 | scf 842 | sbc hl, bc 843 | ; Return if HL is the last line 844 | ret z 845 | ; Update _buffer_line_num now 846 | inc bc 847 | ld (_buffer_line_num), bc 848 | ; Calculate the number of bytes to send to the end of the buffer, in other 849 | ; words, make the cursor point to the end of the current line: 850 | call _buffer_size_right_line 851 | ; It doesn't count the \n, so add it 852 | inc hl 853 | ld b, h 854 | ld c, l 855 | ; Perform the copy 856 | ld de, (_buffer_left_addr) 857 | ld hl, (_buffer_right_addr) 858 | inc hl 859 | ldir 860 | ; Look for the length of the next line, HL is already pointing to its first character 861 | push hl 862 | call _buffer_next_line_length 863 | ; Store the current line length 864 | ld (_buffer_cur_length), hl 865 | ld bc, (_buffer_col_num) 866 | ; Now, we have to transfer MIN(length, cursor) characters <=> MIN(HL, BC) 867 | call _buffer_min_hl_bc 868 | ld (_buffer_col_num), bc 869 | ; Pop the right buffer index from the stack 870 | pop hl 871 | ; Check if BC is 0 (cursor at the beginning already) 872 | ld a, b 873 | or c 874 | jp z, _buffer_next_line_cursor_no_copy 875 | ldir 876 | _buffer_next_line_cursor_no_copy: 877 | ; HL points to the right buffer (source) 878 | dec hl 879 | ld (_buffer_right_addr), hl 880 | ld (_buffer_left_addr), de 881 | ld bc, (_buffer_col_num) 882 | jp _editor_view_cursor_next_line 883 | 884 | 885 | ; Save the current buffer to a file 886 | PUBLIC model_save_file 887 | model_save_file: 888 | ld hl, str_saving 889 | call editor_show_notification 890 | ld bc, (_model_full_path) 891 | ld h, O_WRONLY | O_CREAT | O_TRUNC 892 | OPEN() 893 | ; If an error occurred, return 894 | or a 895 | jp m, model_print_error_neg 896 | ; There are two parts to write to the file: 897 | ; - From the beginning to the cursor 898 | ; - From the cursor to the end 899 | ld hl, (_buffer_left_addr) 900 | ld de, BUFFER_START_ADDRESS 901 | or a ; clear carry flag 902 | sbc hl, de 903 | ld b, h 904 | ld c, l 905 | ex de, hl 906 | push af ; Save the opened descriptor 907 | call model_save_buffer 908 | pop de 909 | or a 910 | jr nz, model_print_error 911 | ; Get back the opened descriptor 912 | ld a, d 913 | ; Save for the right buffer now 914 | ld de, (_buffer_memory_end) 915 | ld hl, (_buffer_right_addr) 916 | ex de, hl 917 | sbc hl, de ; carry flag is already 0 918 | ld b, h 919 | ld c, l 920 | ex de, hl 921 | inc hl 922 | push af 923 | call model_save_buffer 924 | pop hl 925 | or a 926 | jr nz, model_print_error 927 | ; Close the file descriptor in H 928 | CLOSE() 929 | ; Success! 930 | ld hl, str_success 931 | jp editor_show_notification 932 | 933 | model_print_error_neg: 934 | neg 935 | model_print_error: 936 | ld hl, str_error_code 937 | call model_show_location_to_decimal 938 | ld (hl), 0 939 | ld hl, str_error_msg 940 | jp editor_show_notification 941 | 942 | str_saving: DEFM "Saving...", 0 943 | str_success: DEFM "Success", 0 944 | str_error_msg: DEFM "Error saving file: " 945 | str_error_code: DEFS 3 946 | DEFB 0 947 | 948 | 949 | ; Save the given buffer of given size to the opened descriptor (file) 950 | ; Parameters: 951 | ; A - Opened descriptor 952 | ; HL - Buffer containing data to write 953 | ; BC - Size of the buffer 954 | ; Returns: 955 | ; A - ERR_SUCCESS on success, error code else 956 | ; Alters: 957 | ; A, BC, DE, HL 958 | model_save_buffer: 959 | ld (_model_file_dev), a 960 | model_save_buffer_descriptor_saved: 961 | ; If BC is 0, return success directly 962 | ld a, b 963 | or c 964 | ret z 965 | ; Check if the buffer goes beyond one virtual page (the kernel would refuse it else) 966 | push hl 967 | ld a, h 968 | ; Get the virtual page of HL and store it in D 969 | and 0xc0 970 | ld d, a 971 | ; Get the address of the last char of the buffer 972 | dec hl 973 | add hl, bc 974 | ; Get the virtual page of this new address 975 | ld a, h 976 | and 0xc0 977 | ld h, a 978 | ; If it is the same as the former one, the buffer doesn't cross-boundary, we have to call 979 | ; WRITE syscall only once 980 | cp d 981 | jr z, model_save_buffer_not_cross_boundary 982 | ; The buffer crosses boundaries, at most 3, the first address to write is the original one, but 983 | ; we need to modify the size. 984 | pop hl ; Original buffer address 985 | ; Calculate the boundary address out of the virtual page count 986 | ld a, d 987 | add 0x40 ; the virtual page is not 3 for sure, so increment it by 1 988 | ld d, a 989 | ld e, 0 990 | push de ; save next virtual size to write 991 | ; DE contains the end of page address, calculate the new size 992 | push bc ; Save original size 993 | ex de, hl 994 | or a 995 | sbc hl, de 996 | ; Size is in HL 997 | ld b, h 998 | ld c, l 999 | ; Buffer is in DE, write now 1000 | call model_save_buffer_not_cross_boundary_buffer_in_de 1001 | ; Calculate remaining size to write 1002 | pop hl 1003 | or a 1004 | sbc hl, bc 1005 | ; Put the remaining size in BC 1006 | ld b, h 1007 | ld c, l 1008 | ; Pop the next buffer to write 1009 | pop hl 1010 | ; If there was no error, continue writing 1011 | or a 1012 | jp z, model_save_buffer_descriptor_saved 1013 | ; Else, return directly 1014 | ret 1015 | 1016 | ; Parameters: 1017 | ; (_model_file_dev) - Opened descriptor 1018 | ; [SP] - Address of the buffer to print 1019 | ; BC - Size of the buffer to print 1020 | ; Returns: 1021 | ; BC - Number of bytes written 1022 | model_save_buffer_not_cross_boundary: 1023 | ; Buffer to write to file in DE 1024 | pop de 1025 | model_save_buffer_not_cross_boundary_buffer_in_de: 1026 | ; Opened descriptor in H 1027 | ld a, (_model_file_dev) 1028 | ld h, a 1029 | WRITE() 1030 | ret 1031 | 1032 | ; Show the current current of the cursor in the notification bar 1033 | PUBLIC model_show_location 1034 | model_show_location: 1035 | ld de, loc_line 1036 | ; Since the line_num is an index, we have to increment it 1037 | ld hl, (_buffer_line_num) 1038 | inc hl 1039 | call model_show_location_to_decimal 1040 | ; Same goes for the column 1041 | ld (hl), ',' 1042 | inc hl 1043 | ld (hl), ' ' 1044 | inc hl 1045 | ld (hl), 'C' 1046 | inc hl 1047 | ld (hl), 'o' 1048 | inc hl 1049 | ld (hl), 'l' 1050 | inc hl 1051 | ld (hl), ' ' 1052 | inc hl 1053 | ex de, hl 1054 | ld hl, (_buffer_col_num) 1055 | inc hl 1056 | call model_show_location_to_decimal 1057 | ; Clean the end of the buffer 1058 | ld (hl), 0 1059 | ; Finally, we can print the resulting buffer 1060 | ld hl, loc_start 1061 | jp editor_show_notification 1062 | 1063 | 1064 | ; Convert a 16-bit value to decimal (ASCII) and store it in the 1065 | ; buffer pointed by DE 1066 | ; Parameters: 1067 | ; DE - Buffer to store the result in 1068 | ; HL - 16-bit value to convert 1069 | ; Returns: 1070 | ; HL - Address of the last digit + 1 1071 | model_show_location_to_decimal: 1072 | ld c, 10 ; Constant, not modified by the code 1073 | push de 1074 | pop ix ; Move DE in IX 1075 | ld d, 0 ; Number of digits 1076 | model_show_location_to_decimal_loop: 1077 | call _model_divide_hl_c 1078 | ; Remainder in A, convert to ASCII 1079 | add '0' 1080 | push af 1081 | inc d 1082 | ; Check if HL is 0 1083 | ld a, h 1084 | or l 1085 | jp nz, model_show_location_to_decimal_loop 1086 | ; We have D digits, iterate over all the digits 1087 | ld b, d 1088 | push ix 1089 | pop hl 1090 | model_show_location_to_decimal_pop_loop: 1091 | pop af 1092 | ld (hl), a 1093 | inc hl 1094 | djnz model_show_location_to_decimal_pop_loop 1095 | ret 1096 | 1097 | 1098 | loc_start: DEFM "Ln " 1099 | loc_line: DEFS 20 1100 | 1101 | 1102 | ; Divide a 16-bit value by an 8-bit value 1103 | ; Parameters: 1104 | ; HL - Number of divide 1105 | ; C - Divider 1106 | ; Returns: 1107 | ; HL - Result 1108 | ; A - Remainder 1109 | _model_divide_hl_c: 1110 | xor a 1111 | ld b, 16 1112 | _model_divide_hl_c_loop: 1113 | add hl, hl 1114 | rla 1115 | jp c, _model_divide_hl_carry 1116 | cp c 1117 | jp c, _model_divide_hl_next 1118 | _model_divide_hl_carry: 1119 | sub c 1120 | inc l 1121 | _model_divide_hl_next: 1122 | djnz _model_divide_hl_c_loop 1123 | ret 1124 | 1125 | 1126 | SECTION DATA 1127 | _model_full_path: DEFS 2 1128 | _model_file_dev: DEFS 1 1129 | ; Split buffer related 1130 | ; Current length of the line, in bytes 1131 | _buffer_cur_length: DEFS 2 1132 | ; Contains the line number the cursor is currently at 1133 | _buffer_line_num: DEFS 2 1134 | ; Same for the column 1135 | _buffer_col_num: DEFS 2 1136 | ; Contains the number of lines 1137 | _buffer_line_count: DEFS 2 1138 | ; Contains the address of the LEFT cursor in the file buffer 1139 | _buffer_left_addr: DEFS 2 1140 | ; Same but for the RIGHT cursor 1141 | _buffer_right_addr: DEFS 2 1142 | ; Last valid memory address, in other words, maximum value of _buffer_right_addr 1143 | _buffer_memory_end: DEFS 2 -------------------------------------------------------------------------------- /src/view.asm: -------------------------------------------------------------------------------- 1 | ; SPDX-FileCopyrightText: 2023 Zeal 8-bit Computer 2 | ; 3 | ; SPDX-License-Identifier: Apache-2.0 4 | 5 | INCLUDE "zos_sys.asm" 6 | INCLUDE "zos_video.asm" 7 | 8 | DEFC EDITOR_CHARS_COLOR = (TEXT_COLOR_BLACK << 8) | TEXT_COLOR_WHITE 9 | DEFC EDITOR_BACKGROUND_COLOR = (TEXT_COLOR_WHITE << 8) | TEXT_COLOR_BLACK 10 | DEFC EDITOR_FOOTER_HEIGHT = 3 11 | 12 | EXTERN _buffer_previous_ath_line_address 13 | EXTERN model_get_current_line 14 | EXTERN model_next_line_address 15 | EXTERN model_get_remaining_lines 16 | EXTERN model_get_bc_left_chars 17 | EXTERN model_get_bc_right_chars 18 | EXTERN model_move_cursor_left_to 19 | EXTERN strlen 20 | EXTERN strncpy 21 | EXTERN basename 22 | 23 | SECTION TEXT 24 | 25 | 26 | ;=========================================================; 27 | ;===================== VIEW =============================; 28 | ;=========================================================; 29 | 30 | ; Parameters: 31 | ; DE - String containing the file name to open, NULL else 32 | PUBLIC editor_view_init 33 | editor_view_init: 34 | ; Copy the filename 35 | ld a, d 36 | or e 37 | jr z, _editor_view_init_no_file 38 | ex de, hl 39 | call basename 40 | ; Basename in HL 41 | ld de, _editor_filename 42 | ld bc, 16 43 | call strncpy 44 | _editor_view_init_no_file: 45 | ld a, 80 46 | ld (_editor_screen_width), a 47 | ld a, 40 48 | ld (_editor_screen_height), a 49 | ; Absolute X coordinate for lines 50 | ld hl, 0 51 | ld (_file_cursor_x), hl 52 | ; Draw the initial view 53 | call _editor_draw_init_screen 54 | ; Set X and Y to (0, 1) at the same time, cursor_y comes first 55 | ld de, 1 56 | ld (_cursor_y), de 57 | jp _editor_view_restore_cursor_no_load 58 | 59 | 60 | _editor_name: DEFM "ZOS Zepto " 61 | INCBIN "version.txt" 62 | _editor_name_end: 63 | _editor_filename: DEFS 17, "New_File" ; \0 will be added automatically 64 | ; The welcome message must NOT be bigger than the screen width or it will corrupt other data 65 | _editor_welcome_msg: DEFM "Welcome to zepto. For help, type Ctrl+H", 0 66 | _editor_shortcuts_msg: 67 | DEFM "^H Help ^L Location ^X Cut ^C Copy ^V Paste \n" 68 | DEFM "^K Exit ^S Save File ^O Open File ^N New File " 69 | _editor_shortcuts_msg_end: 70 | 71 | 72 | ; Routine to draw the initial screen, with the top menu and the options at the bottom 73 | nop 74 | _editor_draw_init_screen: 75 | ld de, EDITOR_BACKGROUND_COLOR 76 | call _editor_draw_set_color 77 | ; Set the cursor to the top of the screen and start drawing 78 | ld de, 0 79 | call _editor_view_restore_cursor_no_load 80 | ; Print the file name or "New file*" in the absence of file name 81 | call _editor_view_erase_buffer 82 | push bc ; Save the screen width 83 | ld hl, _editor_filename 84 | xor a ; not a notification 85 | call _editor_copy_message 86 | ; Print the name of the program at the top left corner 87 | ld de, _editor_view_buffer + 1 88 | ld hl, _editor_name 89 | ld bc, _editor_name_end - _editor_name 90 | ldir 91 | ld de, _editor_view_buffer 92 | pop bc 93 | inc bc 94 | call _editor_draw_line 95 | ; Let's print this buffer once, prepare the length 96 | ld de, EDITOR_CHARS_COLOR 97 | call _editor_draw_set_color 98 | call _editor_view_erase_buffer 99 | ; Add a newline at the end of the buffer 100 | ld a, '\n' 101 | ld (de), a 102 | ld de, _editor_view_buffer 103 | ; Then print height - (footer + 1) lines 104 | ld a, (_editor_screen_height) 105 | sub EDITOR_FOOTER_HEIGHT + 1 106 | ; Put this amount in L and H 107 | ld h, a 108 | ld l, a 109 | ; Print the \n that follows the buffer 110 | inc bc 111 | _editor_draw_init_lines_loop: 112 | call _editor_draw_line 113 | dec l 114 | jp nz, _editor_draw_init_lines_loop 115 | ; Now we have to print the footer, set the background colors again 116 | ld de, EDITOR_BACKGROUND_COLOR 117 | call _editor_draw_set_color 118 | ; Before calling editor_show_notification, set the cursor_y position that will be set 119 | ; at the end of the routine (_cursor_x was already set to 0 by the loader) 120 | ld a, h 121 | add 2 122 | ld (_cursor_y), a 123 | ld hl, _editor_welcome_msg 124 | call editor_show_notification 125 | ; Print the shortcuts 126 | S_WRITE3(DEV_STDOUT, _editor_shortcuts_msg, _editor_shortcuts_msg_end - _editor_shortcuts_msg) 127 | ; And reset the colors 128 | ld de, EDITOR_CHARS_COLOR 129 | jp _editor_draw_set_color 130 | 131 | 132 | ; Write a message in the notification zone 133 | ; Parameters: 134 | ; HL - Message to write 135 | ; Returns: 136 | ; None 137 | PUBLIC editor_show_notification 138 | editor_show_notification: 139 | push hl 140 | call _editor_view_erase_buffer 141 | ; Save the length of the buffer to print on the stack 142 | ld h, b 143 | ld l, c 144 | ex (sp), hl 145 | ld a, 1 146 | call _editor_copy_message 147 | ; Set the position of the cursor to the notification zone 148 | ld a, (_editor_screen_height) 149 | sub EDITOR_FOOTER_HEIGHT 150 | ld d, 0 151 | ld e, a 152 | call _editor_view_restore_cursor_no_load 153 | ; Set the color of the notification text 154 | ld de, EDITOR_BACKGROUND_COLOR 155 | call _editor_draw_set_color 156 | ; Print the buffer, pop its size from the stack 157 | pop bc 158 | ld de, _editor_view_buffer 159 | S_WRITE1(DEV_STDOUT) 160 | ; Restore the current text color 161 | ld de, EDITOR_CHARS_COLOR 162 | call _editor_draw_set_color 163 | ; Restore the cursor position and return 164 | jp _editor_view_restore_cursor 165 | 166 | 167 | ; Copy a message to the message buffer, it will be copied at the middle 168 | ; Parameters: 169 | ; HL - Message to copy 170 | ; A - 1 if notification ([] appended), 0 else 171 | _editor_copy_message: 172 | push af 173 | call strlen 174 | ; B is 0 for sure here, make sure it is not bigger than the width - 4 (for brackets) 175 | ld a, (_editor_screen_width) 176 | sub 4 177 | cp c 178 | jr nc, _editor_copy_message_size_ok 179 | ld c, a 180 | _editor_copy_message_size_ok: 181 | ; DE = _editor_view_buffer + (size/2 - name_len/2) 182 | ld a, c 183 | srl a 184 | adc 0 185 | ld d, a 186 | ld a, (_editor_screen_width) 187 | srl a 188 | sub d 189 | ; DE += A 190 | ld de, _editor_view_buffer 191 | add e 192 | ld e, a 193 | adc d 194 | sub e 195 | ld d, a 196 | ; Copy the name and finally copy the line 197 | pop af 198 | or a 199 | jr z, _editor_copy_message_no_brack 200 | ; Go backward to add the '[', the buffer was cleaned with spaces, no need to write a space again 201 | ex de, hl 202 | dec hl 203 | dec hl 204 | ld (hl), '[' 205 | inc hl 206 | inc hl 207 | ex de, hl 208 | _editor_copy_message_no_brack: 209 | ldir 210 | or a 211 | ret z 212 | ex de, hl 213 | inc hl 214 | ld (hl), ']' 215 | ret 216 | 217 | 218 | ; Returns: 219 | ; BC - Width of the screen 220 | ; HL - Address of the last character 221 | ; DE - Address of the last character + 1 222 | ; Alters: 223 | ; BC, HL, DE 224 | _editor_view_erase_buffer: 225 | ; Initialize the temporary buffer with spaces first 226 | ld bc, (_editor_screen_width) 227 | push bc 228 | ld hl, _editor_view_buffer 229 | ld (hl), ' ' 230 | ld d, h 231 | ld e, l 232 | inc de 233 | dec bc 234 | ldir 235 | pop bc 236 | ret 237 | 238 | 239 | ; Parameters: 240 | ; DE - Line to print 241 | ; BC - Length of the line to print 242 | ; Returns: 243 | ; A - ERR_SUCCESS on success, error code else 244 | ; Alters: 245 | ; A 246 | _editor_draw_line: 247 | push hl 248 | push bc 249 | S_WRITE1(DEV_STDOUT) 250 | pop bc 251 | pop hl 252 | ret 253 | 254 | 255 | ; Set the current colors 256 | ; Parameter: 257 | ; DE - Background and foreground colors 258 | _editor_draw_set_color: 259 | push hl 260 | push bc 261 | ld h, DEV_STDOUT 262 | ld c, CMD_SET_COLORS 263 | IOCTL() 264 | pop bc 265 | pop hl 266 | ret 267 | 268 | 269 | 270 | ; Adjust the value of BC to be the minimum between BC and screen width 271 | ; Parameters: 272 | ; BC - Value to test 273 | ; Returns: 274 | ; BC - MIN(BC,_editor_screen_width) 275 | ; Alters: 276 | ; A 277 | editor_adjust_bc_width: 278 | ld a, b 279 | or a 280 | jr nz, _editor_adjust_bc_big 281 | ld a, (_editor_screen_width) 282 | cp c 283 | ret nc 284 | _editor_adjust_bc_big: 285 | ld bc, (_editor_screen_width) 286 | ret 287 | 288 | 289 | ; Calculate the minimum between HL and BC and put the result in BC 290 | ; Parameters: 291 | ; HL - 16-bit value 292 | ; BC - 16-bit value 293 | ; Returns: 294 | ; BC - Minimum between HL and BC 295 | ; Alters: 296 | ; HL, BC 297 | editor_min_hl_bc: 298 | or a ; Clear carry flag 299 | sbc hl, bc 300 | ret nc 301 | ; HL was the minimum, restore it and store it in BC 302 | add hl, bc 303 | ld b, h 304 | ld c, l 305 | ret 306 | 307 | ; Calculate the remaining size on the screen line 308 | ; Parameters: 309 | ; - 310 | ; Returns: 311 | ; A - Remaining size on screen line 312 | ; Alters: 313 | ; A, L 314 | editor_line_rem_size: 315 | ld a, (_cursor_x) 316 | ld l, a 317 | ld a, (_editor_screen_width) 318 | sub l 319 | ret 320 | 321 | 322 | 323 | ; Print a line at the cursor location and go to the next line afterwards. 324 | ; This is used when loading a file, in order to show it on screen. 325 | ; Parameters: 326 | ; HL - Address of the line to show 327 | ; BC - Length of the line to print 328 | PUBLIC _editor_view_init_new_line 329 | _editor_view_init_new_line: 330 | call editor_adjust_bc_width 331 | ex de, hl 332 | S_WRITE1(DEV_STDOUT) 333 | ; Print a new line at the end 334 | ld hl, _editor_view_buffer 335 | ld (hl), '\n' 336 | ex de, hl 337 | ld c, 1 338 | S_WRITE1(DEV_STDOUT) 339 | ret 340 | 341 | 342 | ; Clean the whole string of size BC at cursor, DO NOT modify the cursor 343 | ; This shall be called when a new line is inserted in the middle of a line. 344 | ; Parameters: 345 | ; BC - Size to clean 346 | PUBLIC _editor_view_clean_string_at_cursor 347 | _editor_view_clean_string_at_cursor: 348 | ret 349 | 350 | ; Restore the cursor to its original position 351 | PUBLIC _editor_view_restore_cursor 352 | _editor_view_restore_cursor: 353 | ld de, (_cursor_y) 354 | _editor_view_restore_cursor_no_load: 355 | ld c, CMD_SET_CURSOR_XY 356 | ld h, DEV_STDOUT 357 | IOCTL() 358 | ret 359 | 360 | 361 | ; Increment both the view cursor as well as the absolute cursor 362 | ; Parameters: 363 | ; - 364 | ; Returns: 365 | ; - 366 | editor_view_increment_x: 367 | ld hl, (_file_cursor_x) 368 | inc hl 369 | ld (_file_cursor_x), hl 370 | ld hl, _cursor_x 371 | inc (hl) 372 | ret 373 | 374 | 375 | ; Decrement both the view cursor as well as the absolute cursor 376 | ; Parameters: 377 | ; - 378 | ; Returns: 379 | ; - 380 | editor_view_decrement_x: 381 | ld hl, (_file_cursor_x) 382 | dec hl 383 | ld (_file_cursor_x), hl 384 | ld hl, _cursor_x 385 | dec (hl) 386 | ret 387 | 388 | 389 | ; Checks if the current line is a long line (requires horizontal scrolling) 390 | ; Parameters: 391 | ; - 392 | ; Returns: 393 | ; NZ flag - Is a long line 394 | ; Z - Not a long line 395 | ; Alters: 396 | ; A, HL, DE 397 | editor_view_long_line: 398 | ld hl, (_file_cursor_x) 399 | ld de, (_editor_screen_width) 400 | or a 401 | ; X cursor starts at index 0, increment it to get the actual length 402 | inc hl 403 | sbc hl, de 404 | ; If no carry: 405 | ; Z is set - the line is not longer than the width 406 | ; NZ is not set - the line is longer than the width 407 | ret nc 408 | ; On carry, the width is bigger than the cursor, set Z flag 409 | xor a 410 | ret 411 | 412 | 413 | ; Clean the current line and position the cursor at the beginning 414 | ; Parameters: 415 | ; - 416 | ; Returns: 417 | ; - 418 | ; Alters: 419 | ; A, BC, DE, HL 420 | _editor_view_line_clean_beginning: 421 | xor a 422 | ld (_cursor_x), a 423 | call _editor_view_restore_cursor 424 | call _editor_view_erase_buffer 425 | ld de, _editor_view_buffer 426 | S_WRITE1(DEV_STDOUT) 427 | call _editor_view_restore_cursor 428 | ret 429 | 430 | 431 | ; Print a whole string at current cursor position without moving it. After printing, move 432 | ; it to the right once. 433 | ; At the end the cursor must be at original position + 1. 434 | ; Parameters: 435 | ; HL - String to print 436 | ; BC - Size of the string (guaranteed > 1) 437 | PUBLIC _editor_view_insert_string_at_cursor 438 | _editor_view_insert_string_at_cursor: 439 | ex de, hl 440 | ; Set BC to the minimum between BC and the remaining size on this line on screen 441 | call editor_line_rem_size 442 | ; If there is 1 space left on screen, we will reach the border after writing the current one 443 | dec a 444 | jr z, _editor_view_insert_string_at_cursor_overflow 445 | inc a 446 | ld l, a 447 | ld h, 0 448 | call editor_min_hl_bc 449 | _editor_view_insert_string_at_cursor_print: 450 | S_WRITE1(DEV_STDOUT) 451 | call editor_view_increment_x 452 | jp _editor_view_restore_cursor 453 | ; Jump to the following label if the cursor is at the end of the line 454 | ; Clear the whole line, and make the cursor jump back to the beginning of the line 455 | _editor_view_insert_string_at_cursor_overflow: 456 | push de 457 | push bc 458 | call _editor_view_line_clean_beginning 459 | pop bc 460 | pop de 461 | jp _editor_view_insert_string_at_cursor_print 462 | 463 | 464 | ; Print the character given at the current cursor location, 465 | ; reset the cursor (i.e. switch back to characters colors). 466 | ; All checks have been performed already by the buffer/model. 467 | ; Parameters: 468 | ; A - Character to print 469 | PUBLIC _editor_view_insert_at_cursor 470 | _editor_view_insert_at_cursor: 471 | ; D is not altered by the routine below 472 | ld d, a 473 | call editor_line_rem_size 474 | ; If there is only a single spot on the screen line remaining, scroll to the right 475 | dec a 476 | jr z, _editor_view_insert_at_cursor_overflow 477 | ld a, d 478 | _editor_view_insert_at_cursor_print: 479 | ld de, _editor_view_buffer 480 | ld (de), a 481 | ld bc, 1 482 | S_WRITE1(DEV_STDOUT) 483 | jp editor_view_increment_x 484 | _editor_view_insert_at_cursor_overflow: 485 | push de 486 | call _editor_view_line_clean_beginning 487 | pop af 488 | ; Increment the absolute cursor here too, in total it needs to be incremented twice: 489 | ; - Once when we switch screen 490 | ; - Once after inserting the new character 491 | ld hl, (_file_cursor_x) 492 | inc hl 493 | ld (_file_cursor_x), hl 494 | jp _editor_view_insert_at_cursor_print 495 | 496 | 497 | ; Print a whole string at cursor - 1, and update the cursor. 498 | ; Routine called when a character is removed from the middle of a 499 | ; line. So also clear the character after printing. 500 | ; Parameters: 501 | ; HL - String to print 502 | ; BC - Size of the string 503 | ; Alters: 504 | ; BC, DE, HL 505 | PUBLIC _editor_view_insert_string_before_cursor 506 | _editor_view_insert_string_before_cursor: 507 | push hl 508 | push bc 509 | ; Move the cursor left, it will perform the horizontal scroll if needed 510 | call _editor_view_left_cursor 511 | pop bc 512 | ; If the cursor is at the end of the screen, we need to print the next 513 | ; character below it 514 | call editor_line_rem_size 515 | dec a 516 | jr z, _editor_view_insert_string_before_cursor_next_char 517 | ; Regular insertion at cursor, we are not at the edge of the screen 518 | inc a 519 | ld l, a 520 | ld h, 0 521 | call editor_min_hl_bc 522 | pop hl 523 | ; We can print BC characters on screen, copy it to our buffer first, because 524 | ; we need to append a ' ' to clean the last character on the line 525 | push bc 526 | ld de, _editor_view_buffer 527 | ldir 528 | ld a, ' ' 529 | ld (de), a 530 | pop bc 531 | ; We just added a space to the string, increment the size 532 | inc bc 533 | ld de, _editor_view_buffer 534 | S_WRITE1(DEV_STDOUT) 535 | jp _editor_view_restore_cursor 536 | _editor_view_insert_string_before_cursor_next_char: 537 | pop hl 538 | ld de, _editor_view_buffer 539 | ldi 540 | ; Continue by adding a backspace character 541 | ld a, '\b' 542 | ld (de), a 543 | dec de 544 | ; Print out these chars 545 | ld bc, 2 546 | S_WRITE1(DEV_STDOUT) 547 | ; The cursors have been updated already in _editor_view_left_cursor routine 548 | ret 549 | 550 | 551 | ; Remove the character on the view. The cursor must be moved to the left. 552 | PUBLIC _editor_view_remove_at_cursor 553 | _editor_view_remove_at_cursor: 554 | ; Simplify this function by moving left first 555 | call _editor_view_left_cursor 556 | ; Replace the character under the cursor, do not update the cursors anymore 557 | ld hl, _editor_view_buffer 558 | ld (hl), ' ' 559 | inc hl 560 | ld (hl), '\b' 561 | ; Reposition HL and put it in DE 562 | dec hl 563 | ex de, hl 564 | ld bc, 2 565 | S_WRITE1(DEV_STDOUT) 566 | ret 567 | 568 | 569 | ; Move the cursor left, all checks have been performed by 570 | ; the model. 571 | PUBLIC _editor_view_left_cursor 572 | _editor_view_left_cursor: 573 | ; If the X cursor is 0, it means we are currently on a long line, we need to 574 | ; horizontally scroll to the left 575 | ld a, (_cursor_x) 576 | ; Check if A is 1 577 | dec a 578 | jr z, _editor_view_left_cursor_check_scroll 579 | _editor_view_left_cursor_regular: 580 | ; Else, no scroll, we can decrement both cursors 581 | call editor_view_decrement_x 582 | jp _editor_view_restore_cursor 583 | _editor_view_left_cursor_check_scroll: 584 | ; Check if we need to go to the line left content. This is the case when 585 | ; the file cursor is NOT 1 586 | ld hl, (_file_cursor_x) 587 | dec hl 588 | ; Check if HL is 0, if yes, we are not on a long line 589 | ld a, h 590 | or l 591 | jp z, _editor_view_left_cursor_regular 592 | ; We are on a long line, load the left content! 593 | ; Decrement HL again as we are in fact moving the cursor twice 594 | dec hl 595 | ld (_file_cursor_x), hl 596 | ; Get the last screen width - 1 characters from the model 597 | ld bc, (_editor_screen_width) 598 | dec c 599 | ; Take advantage of the fact that BC is set to set the X cursor on screen 600 | ld a, c 601 | ld (_cursor_x), a 602 | ; Get the previous BC bytes from where the cursor is 603 | call model_get_bc_left_chars 604 | ; The cursor is not at X = 0, we can do a syscall for thi, but it will add 605 | ; an overhead, so, a faster way is to insert a \b before the buffer. 606 | ; Of course, we have to save its content first 607 | dec hl 608 | ld a, (hl) 609 | ld (hl), '\b' 610 | ; We jus added a new char, increment BC 611 | inc bc 612 | ex de, hl 613 | push af 614 | S_WRITE1(DEV_STDOUT) 615 | ; Restore the -1st byte of the buffer 616 | pop af 617 | ld (de), a 618 | ; Print the character that must be under the cursor 619 | ; FIXME: Use a '>' to show that there are more characters hiding? 620 | ld bc, 1 621 | call model_get_bc_right_chars 622 | ex de, hl 623 | S_WRITE1(DEV_STDOUT) 624 | jp _editor_view_restore_cursor 625 | 626 | 627 | ; Move the cursor right 628 | PUBLIC _editor_view_right_cursor 629 | _editor_view_right_cursor: 630 | ld hl, _cursor_x 631 | ; If the cursor has reached the end of the line (last character), we have to 632 | ; horizontally scroll the content. 633 | call editor_line_rem_size 634 | ; If there is 1 space left on screen, we will reach the border after writing the current one 635 | dec a 636 | jr z, _editor_view_right_cursor_overflow 637 | _editor_view_right_cursor_no_overflow: 638 | ; No overflow, we can simply move X cursor right (increment) 639 | call editor_view_increment_x 640 | jp _editor_view_restore_cursor 641 | _editor_view_right_cursor_overflow: 642 | ; In total we will increment the absolute X cursor twice, because we have to print 643 | ; back the last character on the first column. 644 | call editor_view_increment_x 645 | ; Clean the buffer line, set the visual X cursor to 0 646 | call _editor_view_erase_buffer 647 | ; Reposition the cursor to the beginning of the screen 648 | xor a 649 | ld (_cursor_x), a 650 | call _editor_view_restore_cursor 651 | ; Get the character that is right before the cursor and the characters that are 652 | ; right after, put them inside our own buffer. 653 | ld bc, 1 654 | call model_get_bc_left_chars 655 | ld de, _editor_view_buffer 656 | ldi 657 | ; DE points to the rest of the buffer 658 | ld bc, (_editor_screen_width) 659 | push bc 660 | dec c ; the width is in fact an 8-bit value 661 | call model_get_bc_right_chars 662 | ; Copy them into our buffer now (BC has been adjusted) 663 | ldir 664 | ; Print our buffer now 665 | pop bc 666 | ld de, _editor_view_buffer 667 | S_WRITE1(DEV_STDOUT) 668 | jp _editor_view_right_cursor_no_overflow 669 | 670 | 671 | ; Check if the cursor is at the bottom of the text area 672 | ; Parameters: 673 | ; - 674 | ; Returns: 675 | ; Z flag - Bottom of text area 676 | ; NZ flag - Not at the bottom of text area 677 | ; Alters: 678 | ; A, D 679 | _editor_cursor_bottom_line: 680 | ld a, (_cursor_y) 681 | ld d, a 682 | ld a, (_editor_screen_height) 683 | ; The text area is smaller than the screen height because of the header and footer 684 | sub EDITOR_FOOTER_HEIGHT + 1 685 | cp d 686 | ret 687 | 688 | 689 | ; Copy a given string to the given buffer. 690 | ; If the string is smaller than the buffer, the latter will be stuffed with spaces. 691 | ; At most _editor_screen_width characters will be written to the buffer. 692 | ; Parameters: 693 | ; HL - Source string address 694 | ; Returns: 695 | ; BC - Total bytes written 696 | _editor_copy_string_to_buffer: 697 | ; Copy the lines and stuff the buffer with spaces 698 | ld bc, (_editor_screen_width) 699 | push bc 700 | ld de, _editor_view_buffer 701 | ld b, c 702 | ld a, '\n' 703 | _editor_copy_string_to_buffer_fst_loop: 704 | cp (hl) 705 | jr z, _editor_copy_string_to_buffer_stuff 706 | ; Use LDI but don't use BC as the size, simply use B 707 | ldi 708 | djnz _editor_copy_string_to_buffer_fst_loop 709 | ; No more space in the buffer, return it 710 | jp _editor_copy_string_to_buffer_ret 711 | _editor_copy_string_to_buffer_stuff: 712 | ; Write the remaining characters 713 | ld a, ' ' 714 | _editor_copy_string_to_buffer_loop: 715 | ld (de), a 716 | inc de 717 | djnz _editor_copy_string_to_buffer_loop 718 | ; Force a newline at the end 719 | _editor_copy_string_to_buffer_ret: 720 | ld a, '\n' 721 | ld (de), a 722 | pop bc 723 | inc bc 724 | ret 725 | 726 | 727 | ; Redraw the current line. If the line was horizontally scrolled, it will 728 | ; start back from its first character. 729 | ; The cursor will be positioned at the beginning of the following line. 730 | ; Parameters: 731 | ; None 732 | ; Returns: 733 | ; None; 734 | ; Alters: 735 | ; A, BC, DE, HL 736 | _editor_view_restore_current_line: 737 | ld a, 1 738 | call _buffer_previous_ath_line_address 739 | call _editor_copy_string_to_buffer 740 | push bc 741 | _editor_view_restore_line_from_buffer: 742 | ; Re-position the cursors to the beginning of the line 743 | ld hl, 0 744 | ld (_file_cursor_x), hl 745 | xor a 746 | ld (_cursor_x), a 747 | call _editor_view_restore_cursor 748 | ; Print the current buffer 749 | pop bc 750 | ld de, _editor_view_buffer 751 | S_WRITE1(DEV_STDOUT) 752 | ret 753 | 754 | ; Same as above but for the next line, which is bigger than the 755 | ; screen width for sure. 756 | ; Parameters: 757 | ; None 758 | ; Returns: 759 | ; None 760 | ; Alters: 761 | ; A, BC, DE, HL 762 | _editor_view_restore_next_line: 763 | call model_next_line_address 764 | ld bc, (_editor_screen_width) 765 | push bc 766 | ld de, _editor_view_buffer 767 | ldir 768 | ld a, '\n' 769 | ld (de), a 770 | jp _editor_view_restore_line_from_buffer 771 | 772 | 773 | ; Move cursor to the next line 774 | ; Parameters: 775 | ; BC - Length of the new line 776 | ; HL - Newline characters 777 | ; Parameters must be popped out of the stack! 778 | PUBLIC _editor_view_goto_newline 779 | _editor_view_goto_newline: 780 | ; We have to increment Y, if it is at the bottom of the page, we must 781 | ; manually scroll the screen by redrawing all the previous lines 782 | call _editor_cursor_bottom_line 783 | jp z, _editor_cursor_move_lines_up 784 | push hl 785 | ; Optimize a bit by restoring the current line if BC is not 0 or the current 786 | ; line is scrolled 787 | call _editor_view_restore_current_line_if_needed 788 | ; Get the remaining number of lines, so that we can update them 789 | call model_get_remaining_lines 790 | ; The total includes the newly created line. 791 | ld b, h 792 | ld c, l 793 | ; Get the remaining number of lines that we can show on screen 794 | ld hl, _cursor_y 795 | inc (hl) 796 | ld a, (_editor_screen_height) 797 | sub EDITOR_FOOTER_HEIGHT 798 | sub (hl) 799 | ; Store the result in HL 800 | ld h, 0 801 | ld l, a 802 | call editor_min_hl_bc 803 | ; The new line address was given to us as a parameter 804 | pop hl 805 | ; Move the number of lines to update in B 806 | ld b, c 807 | call _editor_view_print_b_lines 808 | jp _editor_view_goto_newline_size_0 809 | 810 | ; Checks if the current line needs to be restored, adn restores it 811 | _editor_view_restore_current_line_if_needed: 812 | call editor_view_long_line 813 | jp nz, _editor_view_restore_current_line 814 | ld a, b 815 | or c 816 | jp nz, _editor_view_restore_current_line 817 | ; Position the cursor at the beginning of the next line 818 | ld a, (_cursor_y) 819 | inc a 820 | ld e, a 821 | ld d, 0 822 | jp _editor_view_restore_cursor_no_load 823 | 824 | 825 | ; Jump to here if the cursor is at the bottom of the screen and we need to 826 | ; go to the next line. In order word, we will scroll down all the lines 827 | ; (by moving them up once...) 828 | _editor_cursor_move_lines_up: 829 | ld a, (_editor_screen_height) 830 | sub EDITOR_FOOTER_HEIGHT + 1 831 | ; Save the new line size 832 | push bc 833 | call _editor_view_scroll_down 834 | pop bc 835 | ; As scrolling printed all the lines, including the new one, the only thing we 836 | ; have to do now is clean BC characters on screen. Because these chars used to 837 | ; be at the end of the previous line. 838 | ld a, b 839 | or c 840 | ; If the size is 0, nothing to clean, simply reposition the cursor 841 | jr z, _editor_view_goto_newline_size_0 842 | ; Calculate the minimum between the screen width and the number of characters to 843 | ; clean on screen 844 | push bc 845 | call _editor_view_erase_buffer 846 | pop hl 847 | call editor_min_hl_bc 848 | ld de, _editor_view_buffer 849 | S_WRITE1(DEV_STDOUT) 850 | ; The position is 0 for sure because we just jumped to a new line. 851 | ; Store 0 in X and absolute X. 852 | _editor_view_goto_newline_size_0: 853 | ld hl, 0 854 | ld (_file_cursor_x), hl 855 | ld a, l 856 | ld (_cursor_x), a 857 | jp _editor_view_restore_cursor 858 | 859 | 860 | ; Reposition the cursor sent by the model if necessary and print the current 861 | ; line at the current cursor location. 862 | ; Parameters: 863 | ; H - Former X coordinate 864 | ; BC - New X position given by the model 865 | ; Returns: 866 | ; BC - New coordinates 867 | _editor_view_reposition_cursor: 868 | ld a, h 869 | ld l, a 870 | ld h, 0 871 | or a 872 | sbc hl, bc 873 | ; If no carry, the buffer is only printed on screen, no need to modify it either 874 | ret nc 875 | ld h, 0 876 | ld l, a 877 | push hl 878 | ; The screen is actually already showing the beginning of the line, so there 879 | ; is no need to re-print it (this routine was entered because we don't need 880 | ; to scroll) 881 | call model_move_cursor_left_to 882 | pop bc 883 | ret 884 | 885 | 886 | ; Move the cursor to the previous line 887 | ; Parameters: 888 | ; BC - New cursor position after moving to the previous line 889 | PUBLIC _editor_view_cursor_prev_line 890 | _editor_view_cursor_prev_line: 891 | push bc 892 | ld a, (_cursor_y) 893 | dec a 894 | jp z, _editor_view_prev_line_scroll 895 | ; No need to scroll here, first, check if the current line is a long line (and the 896 | ; cursor is beyond the limit) 897 | call editor_view_long_line 898 | ; Save the current X value 899 | ld a, (_cursor_x) 900 | push af 901 | ; If it is, we need to restore current line (which is now considered next by the 902 | ; model as the cursor was repositioned) 903 | call nz, _editor_view_restore_next_line 904 | ; Current line was restored, save the new coordinate 905 | ld hl, _cursor_y 906 | dec (hl) 907 | _editor_view_line_changed_restore: 908 | ; New cursor position must not be scrolled, so we have to reposition it. 909 | pop hl 910 | pop bc 911 | ; This label can be used to reposition and restore the model cursor to a value 912 | ; that won't require horizontal scrolling 913 | _editor_view_reposition_model_and_restore: 914 | call _editor_view_reposition_cursor 915 | ; Returns the new X coordinate in BC 916 | ld (_file_cursor_x), bc 917 | ld a, c 918 | ld (_cursor_x), a 919 | jp _editor_view_restore_cursor 920 | 921 | _editor_view_next_line_scroll: 922 | ld a, (_editor_screen_height) 923 | sub EDITOR_FOOTER_HEIGHT + 1 924 | call _editor_view_scroll_down 925 | jp _editor_view_reposition_after_scroll 926 | 927 | _editor_view_prev_line_scroll: 928 | call _editor_view_scroll_up 929 | ; Fall-through 930 | 931 | _editor_view_reposition_after_scroll: 932 | pop bc 933 | ; All the lines have been scrolled, and the current one has been updated, we simply 934 | ; need to update the cursor now 935 | ld a, (_cursor_x) 936 | ld h, a 937 | jp _editor_view_reposition_model_and_restore 938 | 939 | 940 | ; Move the cursor to the next line 941 | ; Parameters: 942 | ; BC - New cursor position after moving to the next line 943 | PUBLIC _editor_view_cursor_next_line 944 | _editor_view_cursor_next_line: 945 | push bc 946 | call _editor_cursor_bottom_line 947 | jp z, _editor_view_next_line_scroll 948 | ; No scroll needed but we may need to restore the current line if 949 | ; it was horizontally scrolled. 950 | call editor_view_long_line 951 | ; Save the current X value 952 | ld a, (_cursor_x) 953 | push af 954 | ; If it is, we need to restore current line (which is now considered 955 | ; previous by the model as the cursor was repositioned) 956 | call nz, _editor_view_restore_current_line 957 | ; Current line was restored, save the new coordinate 958 | ld hl, _cursor_y 959 | inc (hl) 960 | jp _editor_view_line_changed_restore 961 | 962 | 963 | ; Prepare the buffer and the watermark counter before a scroll loop 964 | ; Alters: 965 | ; HL, DE, A 966 | _editor_view_scroll_init: 967 | ; Add \n at the end of the buffer FOR THE CURRENT SCREEN WIDTH 968 | ld a, '\n' 969 | ld hl, _editor_view_buffer 970 | ld de, (_editor_screen_width) 971 | add hl, de 972 | ld (hl), a 973 | ; Set the previous line length to the maximum so that it's cleaned 974 | ; by the first line to print 975 | ld a, e 976 | ld (_editor_view_8bit_counter), a 977 | ; Write the code that "returns" the length of the line to override in A 978 | ; For scrolling down: ld a, (_editor_view_8bit_counter) 979 | ld a, 0x3a ; ld a, (nnnn) 980 | ld hl, _editor_view_8bit_counter ; nnnn 981 | ld (_editor_view_copy_and_fill_modify_me), a 982 | ld (_editor_view_copy_and_fill_modify_me + 1), hl 983 | ret 984 | 985 | 986 | ; Calculate the length of the string present in HL 987 | ; This routine must NOT modify any register (except A of course) 988 | ; Parameters: 989 | ; HL - Address of the line to erase 990 | ; Returns: 991 | ; A - Length of the line to erase 992 | _editor_get_line_to_erase_length: 993 | push hl 994 | push bc 995 | ld bc, (_editor_screen_width) 996 | push bc 997 | ld a, '\n' 998 | cpir 999 | pop hl 1000 | or a 1001 | sbc hl, bc 1002 | ; The result will be 8-bit because the screen width is 8-bit 1003 | ld a, l 1004 | pop bc 1005 | pop hl 1006 | ret 1007 | 1008 | 1009 | ; Parameters: 1010 | ; B - Number of lines to print 1011 | ; HL - Address of the buffer containing the lines 1012 | _editor_view_print_b_lines: 1013 | push hl 1014 | call _editor_view_scroll_init 1015 | pop hl 1016 | jp _editor_view_print_n_lines 1017 | 1018 | 1019 | ; Scroll the lines up on screen. 1020 | ; Parameters: 1021 | ; BC - New position of the cursor after moving it to the previous line 1022 | ; Returns: 1023 | ; - 1024 | _editor_view_scroll_up: 1025 | ; Set the cursor to the top of the view and start drawing 1026 | ld de, 1 ; Set the cursor to (0,1) 1027 | call _editor_view_restore_cursor_no_load 1028 | ; Set the watermark to the minimum 1029 | call _editor_view_scroll_init 1030 | ; Print current line WITH newline at the end 1031 | ld a, 1 1032 | call _editor_view_print_current_line 1033 | call model_next_line_address 1034 | ; Number of lines to restore 1035 | ld a, (_editor_screen_height) 1036 | sub EDITOR_FOOTER_HEIGHT + 2 1037 | ld b, a 1038 | _editor_view_print_n_lines: 1039 | dec b 1040 | inc b 1041 | ret z 1042 | ; Modify the code in the routine _editor_view_copy_and_fill to calculate the length of the line 1043 | ; to override 1044 | ; New code: call _editor_get_line_to_erase_length 1045 | ld a, 0xcd 1046 | ld de, _editor_get_line_to_erase_length 1047 | ld (_editor_view_copy_and_fill_modify_me), a 1048 | ld (_editor_view_copy_and_fill_modify_me + 1), de 1049 | _editor_view_scroll_up_loop: 1050 | push bc 1051 | ; Fill the temporary buffer with the current line data 1052 | call _editor_view_copy_and_fill 1053 | ; Print the line on screen 1054 | push hl 1055 | S_WRITE1(DEV_STDOUT) 1056 | pop hl 1057 | pop bc 1058 | djnz _editor_view_scroll_up_loop 1059 | ret 1060 | 1061 | 1062 | ; Scroll the lines on screen, optimized by only writing enough characters 1063 | ; to hide previous line content. Speed now depends on the amount of characters 1064 | ; on screen. 1065 | ; For lines that have around 1-4 characters, this routine is now 8x faster, for a 1066 | ; whole screen refresh. 1067 | ; Parameters: 1068 | ; A - Number of lines to scroll 1069 | _editor_view_scroll_down: 1070 | ; Get the A-th previous line of text. For example we have 36 lines to show, 1071 | ; 36th is the first one (top), because 0 is the new empty one. Ignore the 1072 | ; top most line, so decrement A. 1073 | dec a 1074 | ld d, a ; D is not altered 1075 | call _buffer_previous_ath_line_address 1076 | push hl 1077 | push de 1078 | ; Set the cursor to the top of the view and start drawing 1079 | ld de, 1 ; Set the cursor to (0,1) 1080 | call _editor_view_restore_cursor_no_load 1081 | call _editor_view_scroll_init 1082 | ; Store the number of iterations in BC 1083 | pop bc 1084 | pop hl 1085 | _editor_view_scroll_down_loop: 1086 | push bc 1087 | ; Fill the temporary buffer with the current line data 1088 | call _editor_view_copy_and_fill 1089 | ; Print the line on screen 1090 | push hl 1091 | S_WRITE1(DEV_STDOUT) 1092 | pop hl 1093 | pop bc 1094 | djnz _editor_view_scroll_down_loop 1095 | ; Print the current line without a \n, to prevent the cursor from going to the next line 1096 | xor a 1097 | ; Print the model current line 1098 | ; Parameters: 1099 | ; A - 1 if print a newline at the end of the current line, 0 else 1100 | _editor_view_print_current_line: 1101 | push af 1102 | ; Write the last line, which is the current line 1103 | ld de, _editor_view_buffer 1104 | ; B will be 0, so the routine will return B = 0 1105 | ld bc, (_editor_screen_width) 1106 | call model_get_current_line 1107 | ld a, c 1108 | call _editor_view_copy_and_fill_size_in_a 1109 | ; Decrement C if we must not include/print the final \n 1110 | pop af 1111 | or a 1112 | ; A != 0 <=> Do not decrement C 1113 | jr nz, _editor_view_print_current_line_with_newline 1114 | dec c 1115 | _editor_view_print_current_line_with_newline: 1116 | S_WRITE1(DEV_STDOUT) 1117 | ret 1118 | 1119 | 1120 | ; Parameters: 1121 | ; HL - Address of the current line to print 1122 | ; Returns: 1123 | ; DE - Address of the buffer to print 1124 | ; HL - Address of the next line to print 1125 | ; Alters: 1126 | ; A, BC, DE, HL 1127 | _editor_view_copy_and_fill: 1128 | ld de, _editor_view_buffer 1129 | ld a, '\n' 1130 | ; Maximum size to write is the size of the screen 1131 | ld bc, (_editor_screen_width) 1132 | push bc 1133 | _editor_view_copy_and_fill_loop: 1134 | cp (hl) 1135 | jr z, _editor_view_copy_and_fill_end 1136 | ldi 1137 | jp pe, _editor_view_copy_and_fill_loop 1138 | ; The given line from the model has a bigger size than the screen size 1139 | ; Look for its ending \n, to return next line address 1140 | cpir 1141 | ; Add a newline at the end of DE 1142 | ld (de), a 1143 | ; Pop the size of the current line in BC 1144 | pop bc 1145 | ; Save the current buffer size 1146 | ld a, c 1147 | ; Increment C after setting A as the newline doesn't count as part of the current line length 1148 | inc c 1149 | jp _editor_view_copy_and_fill_ret 1150 | _editor_view_copy_and_fill_end: 1151 | inc hl ; Make HL point to the character after the newline 1152 | ; Add a new line at the end of DE 1153 | ld (de), a 1154 | ; Calculate the current size 1155 | ld a, c 1156 | pop bc 1157 | ld b, a 1158 | ld a, c 1159 | sub b 1160 | _editor_view_copy_and_fill_size_in_a: 1161 | ; Check if the previous buffer was bigger than the current, if so, 1162 | ; fill the difference with spaces 1163 | ld b, a 1164 | ; Code that will be modified at runtime, upon execution, it stores the length 1165 | ; of the size to overwrite in A 1166 | ; For scrolling down: ld a, (_editor_view_8bit_counter) 1167 | ; For scrolling up: call _editor_get_line_to_erase_length 1168 | _editor_view_copy_and_fill_modify_me: 1169 | nop 1170 | nop 1171 | nop 1172 | ld c, a 1173 | ; Calculate current_size - previous_size 1174 | ld a, b 1175 | sub c 1176 | ; If there is no carry, the previous size was smaller, no need to fill 1177 | ; the buffer with spaces but we have to update the size. 1178 | jp nc, _editor_view_copy_and_fill_smaller 1179 | ; Fill the buffer with |A - C| spaces, current length is still in B. 1180 | neg 1181 | ld c, b ; Current length in C 1182 | ld b, a ; Number of spaces to write 1183 | ; Save the current length in the global variable 1184 | ld a, c 1185 | ld (_editor_view_8bit_counter), a 1186 | ; At the end, return BC = current length + number of spaces written 1187 | add b 1188 | push af 1189 | ex de, hl 1190 | ld a, ' ' 1191 | _editor_view_copy_and_fill_end_loop: 1192 | ld (hl), a 1193 | inc hl 1194 | djnz _editor_view_copy_and_fill_end_loop 1195 | ld (hl), '\n' 1196 | ex de, hl 1197 | ; Return the number of characters to write on screen 1198 | pop bc 1199 | ld c, b 1200 | ld b, 0 1201 | ; Increment (B)C because of the final \n 1202 | inc c 1203 | ld de, _editor_view_buffer 1204 | ret 1205 | _editor_view_copy_and_fill_smaller: 1206 | ; The current size, in B, is bigger or equal to the previous line size. 1207 | ; We need to print B characters. 1208 | ld c, b 1209 | ld b, 0 1210 | ld a, c 1211 | ; Increment (B)C because a '\n' was added 1212 | inc c 1213 | _editor_view_copy_and_fill_ret: 1214 | ld (_editor_view_8bit_counter), a 1215 | ld de, _editor_view_buffer 1216 | ret 1217 | 1218 | 1219 | SECTION DATA 1220 | 1221 | ; The following represent the cursor on screen, it is synchronized with the 1222 | ; actual driver's cursor 1223 | _cursor_y: DEFS 1 1224 | _cursor_x: DEFS 1 1225 | 1226 | ; This cursor represents the absolute value of the cursor on the line. 1227 | ; If a line is longer than the screen width, this cursor will still be incremented 1228 | ; and monotonic 1229 | _file_cursor_x: DEFS 2 1230 | 1231 | ; Using a 16-bit value will simply the code 1232 | _editor_screen_width: DEFS 2 1233 | _editor_screen_height: DEFS 1 1234 | ; Temporary buffer used to store data to print 1235 | PUBLIC _editor_view_buffer 1236 | _editor_view_buffer: 1237 | DEFS 80 1238 | DEFM '\n' 1239 | _editor_view_buffer_end: 1240 | ; 8-bit iterator for loops 1241 | _editor_view_8bit_counter: DEFS 1 1242 | --------------------------------------------------------------------------------