|
- ;
- ; 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
- ldy #>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
- ldy #>full_fn
- jsr SETNAM
-
- jsr OPEN
- bcc ds_open_ok
- ; open error
- lda #<msg_disk_err
- ldy #>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
- ldy #>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
- ldy #>msg_saved
- jsr show_status_text
- rts
-
- ds_no_name:
- lda #<msg_need_name
- ldy #>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
- ldy #>str_normal
- jsr write_str_to_status
- rts
-
- dstat_ins:
- lda #<str_insert
- ldy #>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
|