; ; vim.asm - minimal modal text editor for the kernal-os-skeleton shell ; ; Build: ; ca65 vim.asm -o vim.o ; ld65 -C vim.cfg vim.o -o vim.prg ; ; From the OS shell type: VIM ; All external commands load at $2000 and end with RTS. ; ; Modes ; NORMAL (default) h j k l or cursor keys = move ; i = insert before cursor ; a = append after cursor ; o = open line below ; x = delete char under cursor ; : = enter command mode ; INSERT type text, DEL = backspace, RUN/STOP = back to NORMAL ; COMMAND :q quit :w NAME save to disk ; RUN/STOP = cancel ; ; Text model: the 24x40 text area of screen RAM IS the buffer. ; Row 24 (bottom line) is the status bar. ; .setcpu "6502" ; ---- KERNAL ---- GETIN = $FFE4 STOPKEY = $FFE1 CHROUT = $FFD2 SETLFS = $FFBA SETNAM = $FFBD OPEN = $FFC0 CLOSE = $FFC3 CHKOUT = $FFC9 CLRCH = $FFCC CLALL = $FFE7 ; ---- hardware ---- SCRRAM = $0400 ; ---- layout ---- SCR_COLS = 40 SCR_ROWS = 24 ; rows 0-23 = text, row 24 = status bar ; ---- modes ---- MODE_NORMAL = 0 MODE_INSERT = 1 MODE_CMD = 2 ; ---- PETSCII ---- CR_CH = $0D DEL_CH = $14 ; ---- command buffer ---- CMD_MAX = 20 ; ---- save LFN ---- SAVE_LFN = 5 SAVE_DEV = 8 ; ---- zero page (user area, safe under OS banking) ---- ZP_LO = $FB ; general destination pointer lo ZP_HI = $FC ; general destination pointer hi STR_LO = $FD ; string source pointer lo STR_HI = $FE ; string source pointer hi ZP_TMP = $02 ; scratch byte ; ================================================================ ; PRG load address header ; ================================================================ .segment "LOADADDR" .word $2000 ; ================================================================ ; CODE ; ================================================================ .segment "CODE" ; ---- entry ---- vim_main: jsr CLALL ; close any open channels from shell jsr vim_init jsr full_redraw main_loop: lda quit_flag beq ml_no_quit jmp vim_do_exit ml_no_quit: jsr get_key ; waits for keypress; $FF = RUN/STOP cmp #$FF bne ml_key_ok ; RUN/STOP pressed lda cur_mode beq ml_stop_quit ; normal mode: quit lda #MODE_NORMAL ; other modes: back to normal sta cur_mode jsr full_redraw jmp main_loop ml_stop_quit: lda #1 sta quit_flag jmp main_loop ml_key_ok: sta last_key lda cur_mode cmp #MODE_INSERT bne ml_not_insert jmp handle_insert ml_not_insert: cmp #MODE_CMD bne ml_not_cmd jmp handle_cmd ml_not_cmd: ; fall through = normal mode ; ================================================================ ; NORMAL MODE ; ================================================================ handle_normal: lda last_key cmp #'H' beq nm_left cmp #$9D ; cursor-left hardware key beq nm_left cmp #'L' beq nm_right cmp #$1D ; cursor-right hardware key beq nm_right cmp #'K' beq nm_up cmp #$91 ; cursor-up hardware key beq nm_up cmp #'J' beq nm_down cmp #$11 ; cursor-down hardware key beq nm_down cmp #'I' beq nm_ins cmp #'A' beq nm_app cmp #'X' beq nm_del cmp #'O' beq nm_open cmp #':' beq nm_cmd jmp nm_done nm_left: jsr cursor_left jmp nm_done nm_right: jsr cursor_right jmp nm_done nm_up: jsr cursor_up jmp nm_done nm_down: jsr cursor_down jmp nm_done nm_ins: lda #MODE_INSERT sta cur_mode jmp nm_done nm_app: jsr cursor_right lda #MODE_INSERT sta cur_mode jmp nm_done nm_del: jsr delete_char jmp nm_done nm_open: jsr open_line_below lda #MODE_INSERT sta cur_mode jmp nm_done nm_cmd: lda #MODE_CMD sta cur_mode lda #0 sta cmd_len nm_done: jsr full_redraw jmp main_loop ; ================================================================ ; INSERT MODE ; ================================================================ handle_insert: lda last_key cmp #CR_CH beq ins_nl cmp #DEL_CH beq ins_bs cmp #$20 ; ignore control chars below $20 bcc ins_done cmp #$7F bcs ins_done jsr insert_char ; A = PETSCII char to insert jsr cursor_right jmp ins_done ins_nl: jsr do_newline jmp ins_done ins_bs: jsr backspace_char ins_done: jsr full_redraw jmp main_loop ; ================================================================ ; COMMAND MODE ; ================================================================ handle_cmd: lda last_key cmp #CR_CH beq cmd_exec cmp #DEL_CH beq cmd_bs cmp #$20 bcc cmd_done cmp #$7F bcs cmd_done ldx cmd_len cpx #CMD_MAX bcs cmd_done sta cmd_buf,x inc cmd_len jmp cmd_done cmd_bs: lda cmd_len beq cmd_done dec cmd_len jmp cmd_done cmd_exec: jsr run_cmd cmd_done: jsr full_redraw jmp main_loop ; ================================================================ ; EXIT ; ================================================================ vim_do_exit: jsr restore_cursor ; de-invert cursor char before leaving rts ; return to kernal-os-skeleton shell ; ================================================================ ; COMMAND EXECUTION ; ================================================================ run_cmd: lda cmd_len beq rc_cancel ; --- :Q quit --- cmp #1 bne rc_not_q lda cmd_buf cmp #'Q' bne rc_not_q lda #1 sta quit_flag lda #MODE_NORMAL sta cur_mode rts rc_not_q: ; --- :W [NAME] save --- lda cmd_buf cmp #'W' bne rc_unknown jsr do_save lda #MODE_NORMAL sta cur_mode rts rc_unknown: ; show "?" in status then return to normal lda #msg_cmd_err jsr show_status_text rc_cancel: lda #MODE_NORMAL sta cur_mode rts ; ================================================================ ; FILE SAVE ; do_save: reads cmd_buf "W NAME" and writes text to disk ; ================================================================ do_save: ; need at least "W N" (3 chars: W, space, one char of name) lda cmd_len cmp #3 bcs ds_len_ok jmp ds_no_name ds_len_ok: lda cmd_buf + 1 cmp #' ' beq ds_space_ok jmp ds_no_name ds_space_ok: ; copy filename from cmd_buf[2..] to save_fn ldy #2 ldx #0 ds_copy_name: lda cmd_buf,y beq ds_name_done cmp #' ' beq ds_name_done sta save_fn,x inx iny cpx #15 bcc ds_copy_name ds_name_done: stx save_fn_len lda #0 sta save_fn,x ; build "0:NAME,S,W" in full_fn jsr build_save_fname ; SETLFS lda #SAVE_LFN ldx #SAVE_DEV ldy #2 jsr SETLFS ; SETNAM lda full_fn_len ldx #full_fn jsr SETNAM jsr OPEN bcc ds_open_ok ; open error lda #msg_disk_err jsr show_status_text rts ds_open_ok: lda #SAVE_LFN jsr CHKOUT bcc ds_write_rows jsr CLRCH lda #SAVE_LFN jsr CLOSE lda #msg_disk_err jsr show_status_text rts ds_write_rows: lda #0 sta save_row ds_row_loop: ldx save_row lda row_lo,x sta ZP_LO lda row_hi,x sta ZP_HI ; find rightmost non-space column ldy #SCR_COLS - 1 ds_find_end: lda (ZP_LO),y cmp #$20 bne ds_row_found tya beq ds_row_empty dey jmp ds_find_end ds_row_empty: ; empty row: just write CR lda #CR_CH jsr CHROUT jmp ds_row_done ds_row_found: ; write cols 0..Y then CR sty save_col_end ldy #0 ds_write_col: lda (ZP_LO),y jsr screen_to_petscii jsr CHROUT cpy save_col_end beq ds_write_cr iny jmp ds_write_col ds_write_cr: lda #CR_CH jsr CHROUT ds_row_done: inc save_row lda save_row cmp #SCR_ROWS bne ds_row_loop ; close jsr CLRCH lda #SAVE_LFN jsr CLOSE lda #msg_saved jsr show_status_text rts ds_no_name: lda #msg_need_name jsr show_status_text rts ; builds "0:save_fn,S,W" into full_fn, length into full_fn_len build_save_fname: ldx #0 lda #'0' sta full_fn,x inx lda #':' sta full_fn,x inx ldy #0 bsf_loop: lda save_fn,y beq bsf_suffix sta full_fn,x inx iny bne bsf_loop bsf_suffix: lda #',' sta full_fn,x inx lda #'S' sta full_fn,x inx lda #',' sta full_fn,x inx lda #'W' sta full_fn,x inx stx full_fn_len lda #0 sta full_fn,x rts ; ================================================================ ; CURSOR MOVEMENT ; ================================================================ cursor_left: lda cur_col beq cl_wrap dec cur_col rts cl_wrap: lda cur_row beq cl_top dec cur_row lda #SCR_COLS - 1 sta cur_col cl_top: rts cursor_right: lda cur_col cmp #SCR_COLS - 1 bcs cr_wrap inc cur_col rts cr_wrap: lda cur_row cmp #SCR_ROWS - 1 bcs cr_bot inc cur_row lda #0 sta cur_col cr_bot: rts cursor_up: lda cur_row beq cu_top dec cur_row cu_top: rts cursor_down: lda cur_row cmp #SCR_ROWS - 1 bcs cd_bot inc cur_row cd_bot: rts ; ================================================================ ; TEXT EDITING ; ================================================================ ; Set ZP_LO/ZP_HI to the screen RAM base for cur_row row_ptr: ldx cur_row lda row_lo,x sta ZP_LO lda row_hi,x sta ZP_HI rts ; Insert PETSCII char (in A) at cursor; shift rest of line right (last char lost) insert_char: jsr petscii_to_screen sta ZP_TMP jsr row_ptr ldy #SCR_COLS - 1 ic_shift: cpy cur_col beq ic_write dey lda (ZP_LO),y iny sta (ZP_LO),y dey jmp ic_shift ic_write: lda ZP_TMP ldy cur_col sta (ZP_LO),y rts ; Delete char at cursor; shift rest of line left, fill end with space delete_char: jsr row_ptr ldy cur_col dc_shift: cpy #SCR_COLS - 1 beq dc_space iny lda (ZP_LO),y dey sta (ZP_LO),y iny jmp dc_shift dc_space: lda #$20 sta (ZP_LO),y rts ; Backspace: move cursor left then delete backspace_char: lda cur_col beq bs_done jsr cursor_left jsr delete_char bs_done:rts ; RETURN in insert mode: move to col 0 of next row do_newline: lda cur_row cmp #SCR_ROWS - 1 bcs nl_done inc cur_row lda #0 sta cur_col nl_done:rts ; Open new line below: advance row, clear it, col = 0 open_line_below: lda cur_row cmp #SCR_ROWS - 1 bcs ol_done inc cur_row lda #0 sta cur_col jsr clear_current_row ol_done:rts clear_current_row: jsr row_ptr ldy #0 lda #$20 ccr_lp: sta (ZP_LO),y iny cpy #SCR_COLS bne ccr_lp rts ; Clear all 24 text rows to spaces clear_text: lda #0 sta ZP_TMP ct_loop: ldx ZP_TMP lda row_lo,x sta ZP_LO lda row_hi,x sta ZP_HI ldy #0 lda #$20 ct_row: sta (ZP_LO),y iny cpy #SCR_COLS bne ct_row inc ZP_TMP lda ZP_TMP cmp #SCR_ROWS bne ct_loop rts ; ================================================================ ; SCREEN RENDERING ; ================================================================ ; Restore the saved char at (prev_row, prev_col) restore_cursor: ldx prev_row lda row_lo,x clc adc prev_col sta ZP_LO lda row_hi,x adc #0 sta ZP_HI lda prev_char ldy #0 sta (ZP_LO),y rts ; Show cursor by inverting char at (cur_row, cur_col); saves old char show_cursor: ldx cur_row lda row_lo,x clc adc cur_col sta ZP_LO lda row_hi,x adc #0 sta ZP_HI ldy #0 lda (ZP_LO),y sta prev_char ora #$80 ; set bit 7 = reversed character sta (ZP_LO),y lda cur_row sta prev_row lda cur_col sta prev_col rts ; Draw status bar at row 24 draw_status: ; clear status row lda row_lo + 24 sta ZP_LO lda row_hi + 24 sta ZP_HI ldy #0 lda #$20 dstat_clr: sta (ZP_LO),y iny cpy #SCR_COLS bne dstat_clr lda cur_mode cmp #MODE_INSERT beq dstat_ins cmp #MODE_CMD beq dstat_cmd ; NORMAL lda #str_normal jsr write_str_to_status rts dstat_ins: lda #str_insert jsr write_str_to_status rts dstat_cmd: ; write ":" then cmd_buf lda row_lo + 24 sta ZP_LO lda row_hi + 24 sta ZP_HI lda #$3A ; screen code for ':' ldy #0 sta (ZP_LO),y iny ldx #0 dstat_cmd_lp: cpx cmd_len beq dstat_done lda cmd_buf,x jsr petscii_to_screen sta (ZP_LO),y iny inx cpy #SCR_COLS bcc dstat_cmd_lp dstat_done: rts ; Write null-terminated PETSCII string (A=lo, Y=hi) to status row write_str_to_status: sta STR_LO sty STR_HI lda row_lo + 24 sta ZP_LO lda row_hi + 24 sta ZP_HI ldy #0 wss_lp: lda (STR_LO),y beq wss_done jsr petscii_to_screen sta (ZP_LO),y iny cpy #SCR_COLS bcc wss_lp wss_done: rts ; Copy a PETSCII string directly to status (no screen-code conversion) ; Used for messages that already have screen codes (msg_saved etc.) ; Actually we just reuse write_str_to_status since it converts show_status_text: jsr write_str_to_status rts ; Restore cursor, redraw cursor, redraw status bar full_redraw: jsr restore_cursor jsr show_cursor jsr draw_status rts ; ================================================================ ; INIT ; ================================================================ vim_init: lda #0 sta cur_row sta cur_col sta prev_row sta prev_col sta cur_mode sta cmd_len sta quit_flag sta save_row sta save_col_end lda #$20 ; cursor starts on a space sta prev_char jsr clear_text rts ; ================================================================ ; KEY INPUT ; ================================================================ get_key: gk_loop: jsr STOPKEY bne gk_getin lda #$FF ; RUN/STOP = special exit code rts gk_getin: jsr GETIN beq gk_loop ; no key yet, keep polling rts ; ================================================================ ; CHARACTER CONVERSION ; ================================================================ ; PETSCII → screen code ; $41-$5A (A-Z unshifted) → $01-$1A (subtract $40) ; $61-$7A (a-z lowercase) → $01-$1A (subtract $60) ; $20-$3F (space,nums,punct) → same ; others: pass through petscii_to_screen: cmp #$41 bcc pts_pass cmp #$5B bcc pts_upper cmp #$61 bcc pts_pass cmp #$7B bcc pts_lower pts_pass: rts pts_upper: sec sbc #$40 rts pts_lower: sec sbc #$60 rts ; screen code → PETSCII (inverse of above) ; $01-$1A → $41-$5A (add $40) ; $20-$3F → same ; $00 → '@' ; others: return space $20 screen_to_petscii: cmp #$01 bcc stp_at cmp #$1B bcc stp_letter cmp #$20 bcc stp_space cmp #$40 bcc stp_pass stp_space: lda #$20 rts stp_at: lda #$40 ; '@' rts stp_letter: clc adc #$40 rts stp_pass: rts ; ================================================================ ; RODATA ; ================================================================ .segment "RODATA" str_normal: .byte "NORMAL (hjkl=move i=ins x=del :=cmd)", 0 str_insert: .byte "INSERT (type text, DEL=bksp, STOP=normal)", 0 msg_cmd_err: .byte "? unknown command", 0 msg_saved: .byte "saved.", 0 msg_disk_err: .byte "disk error.", 0 msg_need_name: .byte "usage: :w filename", 0 ; Row-base lookup tables (25 entries: rows 0-23 = text, row 24 = status) row_lo: .byte <(SCRRAM + 0*40), <(SCRRAM + 1*40), <(SCRRAM + 2*40) .byte <(SCRRAM + 3*40), <(SCRRAM + 4*40), <(SCRRAM + 5*40) .byte <(SCRRAM + 6*40), <(SCRRAM + 7*40), <(SCRRAM + 8*40) .byte <(SCRRAM + 9*40), <(SCRRAM + 10*40), <(SCRRAM + 11*40) .byte <(SCRRAM + 12*40), <(SCRRAM + 13*40), <(SCRRAM + 14*40) .byte <(SCRRAM + 15*40), <(SCRRAM + 16*40), <(SCRRAM + 17*40) .byte <(SCRRAM + 18*40), <(SCRRAM + 19*40), <(SCRRAM + 20*40) .byte <(SCRRAM + 21*40), <(SCRRAM + 22*40), <(SCRRAM + 23*40) .byte <(SCRRAM + 24*40) row_hi: .byte >(SCRRAM + 0*40), >(SCRRAM + 1*40), >(SCRRAM + 2*40) .byte >(SCRRAM + 3*40), >(SCRRAM + 4*40), >(SCRRAM + 5*40) .byte >(SCRRAM + 6*40), >(SCRRAM + 7*40), >(SCRRAM + 8*40) .byte >(SCRRAM + 9*40), >(SCRRAM + 10*40), >(SCRRAM + 11*40) .byte >(SCRRAM + 12*40), >(SCRRAM + 13*40), >(SCRRAM + 14*40) .byte >(SCRRAM + 15*40), >(SCRRAM + 16*40), >(SCRRAM + 17*40) .byte >(SCRRAM + 18*40), >(SCRRAM + 19*40), >(SCRRAM + 20*40) .byte >(SCRRAM + 21*40), >(SCRRAM + 22*40), >(SCRRAM + 23*40) .byte >(SCRRAM + 24*40) ; ================================================================ ; BSS (zero-initialised by the OS / KERNAL CINT on cold start; ; vim_init zeros what it cares about explicitly) ; ================================================================ .segment "BSS" cur_row: .res 1 cur_col: .res 1 prev_row: .res 1 prev_col: .res 1 prev_char: .res 1 ; screen code saved before cursor inversion cur_mode: .res 1 cmd_len: .res 1 cmd_buf: .res CMD_MAX + 1 last_key: .res 1 quit_flag: .res 1 save_row: .res 1 save_col_end: .res 1 save_fn: .res 16 ; extracted filename (no path) save_fn_len: .res 1 full_fn: .res 24 ; "0:filename,S,W" full_fn_len: .res 1