Skip to content

tachijuan/hvi

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

HVI - VI Clone for CP/M

Version 2.0

A lightweight VI-compatible editor for CP/M 2.2 and CP/M 3.0, written in HI-TECH C. Uses a gap buffer for efficient editing and ANSI escape sequences for terminal control. Implements most of the basic movement and editing commands including the . operator for repeat. Also has a single level undo.

Author: Juan Orlandini
License: MIT


Building

Requirements

  • HI-TECH C Compiler for Z80/CP/M (V3.09 or later)
  • CP/M 2.2 or CP/M 3.0 system with at least 48K TPA

Source Files

cstart.as  hvi.c  hvi.h  gap.c  term.c  screen.c  emove.c
edit.c  erepeat.c  ex.c  util.c  cpmio.c

Build Steps (on CP/M)

  1. Copy all source files to a CP/M disk/drive.

  2. Prepare LX.LIB (one-time setup — removes csv.obj to avoid symbol conflicts with cstart.as):

PIP LX.LIB=LIBC.LIB
LIBR d LX.LIB csv.obj
  1. Compile all source files:
C -CPM -O -C cstart.as
C -CPM -O -C hvi.c gap.c term.c screen.c emove.c edit.c erepeat.c ex.c util.c cpmio.c
  1. Link (the backslash \ continues the command past CP/M's 128-character line limit):
l
-Ptext=100H,data,bss -C100H -oh.com CSTART.OBJ CPMIO.OBJ UTIL.OBJ \
GAP.OBJ TERM.OBJ SCREEN.OBJ EMOVE.OBJ EREPEAT.OBJ EX.OBJ EDIT.OBJ HVI.OBJ LX.LIB
  1. Rename the output:
REN HVI.COM=H.COM

Note: The HI-TECH C linker command is l (lowercase L). The -Ptext=100H,data,bss flag is required to place data and BSS sections correctly. cstart.as must be first in the link order; it replaces the standard CRTCPM.OBJ.

Cross-Compilation (Linux/macOS host)

If using the HI-TECH Z80 cross-compiler on a Unix host, use the same flags and link order as above, then transfer hvi.com to your CP/M system via XMODEM, ZMODEM, or disk image.


Usage

HVI [filename]
  • filename — file to open (created if it does not exist)

Supported Commands

Normal Mode — Movement

Key Action
h / Move left one character
l / Move right one character
j / Move down one line
k / Move up one line
Enter Move to first non-blank of next line
w Forward to start of next word
b Backward to start of previous word
e Forward to end of word
0 Move to beginning of line
^ Move to first non-blank of line
$ Move to end of line
G Go to last line (or line N with count)
gg Go to first line (or line N: 5gg)
Ctrl-F Scroll forward one page; cursor lands in the middle of the new page. No-op if already at the end of the file.
Ctrl-B Scroll backward one page; cursor lands in the middle of the new page. No-op if already at the beginning of the file.
Ctrl-D Scroll forward half page
Ctrl-U Scroll backward half page

Normal Mode — Insert / Append

Key Action
i Insert before cursor
a Append after cursor
I Insert at beginning of line
A Append at end of line
o Open new line below, enter insert mode
O Open new line above, enter insert mode
s Substitute character(s) (delete + insert)
S Substitute entire line

Normal Mode — Delete / Change

Key Action
x Delete character under cursor
X Delete character before cursor
dd Delete current line
dw Delete word forward
db Delete word backward
d$ Delete to end of line
d0 Delete to beginning of line
dG Delete to end of file
D Delete to end of line (same as d$)
cc Change current line
cw Change word
c$ Change to end of line
C Change to end of line (same as c$)
r Replace single character
J Join line below to current line
~ Toggle case of character

Normal Mode — Yank and Put

Key Action
yy Yank (copy) current line
Y Yank current line (same as yy)
yw Yank word
y$ Yank to end of line
p Put (paste) after cursor / below current line
P Put before cursor / above current line

Normal Mode — Search

Key Action
/ Search forward for pattern
? Search backward for pattern
n Repeat last search
N Repeat last search in reverse

Search is case-insensitive plain substring (no regular expressions). In a large file, search scans the entire file — not just the loaded buffer. When the match is found in an unloaded section HVI reloads the window around it automatically. search hit BOTTOM, continuing at TOP (or TOP … BOTTOM) is shown only when the search genuinely wraps past the end (or beginning) of the file.

Normal Mode — Character Search (current line)

Key Action
f{c} Move to next occurrence of character c on line
F{c} Move to previous occurrence of character c on line
; Repeat last f or F in the same direction
, Repeat last f or F in the opposite direction

Both repeat commands accept a count prefix (e.g. 3; skips to the third next match).

Normal Mode — Miscellaneous

Key Action
. Repeat last change
u Undo last change
: Enter ex command mode
Ctrl-L Redraw screen

Insert Mode

Key Action
(any char) Insert character
Enter Insert newline
Backspace Delete previous character
Ctrl-H Delete previous character
Ctrl-W Delete previous word
Ctrl-U Delete to start of line
↑↓←→ Move cursor (stay in insert mode)
ESC Return to normal mode

Ex Commands

Command Action
:w Write (save) current file
:w filename Write to named file
:q Quit (fails if unsaved changes)
:q! Quit without saving
:wq Write and quit
:wq! Write and quit (force)
:x Write if modified, then quit
:x! Write and quit (force)
:e filename Abandon current buffer and edit named file
:e! filename Abandon modified buffer and edit named file
:r filename Read file and insert after current line
:N Go to line number N
:$ Go to last line

Count Prefix

Most commands accept a numeric count prefix:

  • 5j — move down 5 lines
  • 3w — move forward 3 words
  • 2dd — delete 2 lines
  • 10G — go to line 10

Status Line

The status line shows:

"filename" [+]
  • [+] appears when the buffer has unsaved changes

Whenever HVI reads a chunk of a large file from disk it briefly shows [Loading...] on the status line, replaced by the normal display once the screen refreshes.


Large File Support

HVI can open and edit files that are larger than available RAM. The in-memory content is a sliding window: only a portion of the file is in memory at any time, and the window shifts as you scroll.

How the window works

At startup HVI allocates the largest contiguous free block in the TPA (up to BUF_MAX bytes of content) and loads as much of the file as fits. It records where loading stopped (tail_offset) and the source filename (tail_file).

As you scroll forward with j, Ctrl-D, or Ctrl-F, HVI loads the next 4 KB chunk from disk automatically. When the buffer is full, the same number of bytes are discarded from the beginning to make room. The discarded content is always before the cursor so the cursor is never lost.

Scrolling backward with Ctrl-B reloads a window from an earlier position in the file via a direct BDOS seek (no sequential scan needed).

Editing across the window boundary

Before shifting the window in either direction, HVI automatically saves all in-memory edits to a swap file (HVISWP.TMP). Subsequent reads come from the swap file, which contains the complete, up-to-date file content. This means edits made at the beginning of a file are never lost when you scroll to the end, and vice versa.

If the gap buffer fills up during editing, HVI saves to the swap file and reloads a smaller window around the cursor — you can keep typing without interruption.

Searching in a large file

/pattern and ?pattern search the entire file, not just the in-memory window. The search proceeds in three phases:

  1. Scan the in-memory buffer from the cursor forward (or backward).
  2. If no unwrapped match is found, scan the unloaded file sections sequentially using direct BDOS reads — the tail (bytes after the buffer) for forward search, the head (bytes before the buffer) for backward search.
  3. When a match is found in an unloaded section, HVI reloads the window around it and places the cursor there.

The search hit BOTTOM, continuing at TOP message is shown only when the match required crossing the true end-of-file boundary, not merely the buffer boundary. A match in the unloaded tail of a forward search is reported without any wrap message because it is genuinely ahead of the cursor in file order.

Navigating to a specific line in a large file

nG (e.g. 1000G) works correctly in large files. HVI scans the source file from the beginning to locate the byte offset of line N, positions the window there, and places the cursor precisely on that line. Navigation speed is proportional to the line number, not the file size.

Saving large files

On every :w save, HVI reconstructs the full file:

  1. Bytes before the current window (from tail_file) are copied verbatim.
  2. The in-memory buffer is written in CR+LF format.
  3. Bytes after the current window (from tail_file) are appended verbatim.
  4. A Ctrl-Z (0x1A) EOF marker is written last.

When saving back to the same file that holds the unloaded portions, HVI writes to HVITMP.TMP first, then renames, so the source is never overwritten before it is fully read.

Temporary files used

File Purpose
HVISWP.TMP Swap file written before any window shift or buffer overflow
HVITMP.TMP Intermediate used when saving back to the tail source file

Terminal Requirements

HVI uses ANSI/VT100 escape sequences. It defaults to 80 columns × 24 rows, which is the standard CP/M terminal size.

At startup HVI always queries the terminal for its actual dimensions by sending ESC[999;999H (cursor to extreme bottom-right) followed by ESC[6n (ANSI cursor-position report). The response is read byte-by-byte via BIOS CONIN, bypassing BDOS canonical buffering. If the terminal does not respond within the polling timeout, HVI silently falls back to the 80 × 24 defaults — it will not hang. No recompilation flag is needed.

Compatible terminals: VT100, VT220, xterm, ANSI.SYS, and most modern terminal emulators connected via a serial port.


File Format

HVI reads files in binary mode, stripping bare CR characters on load. On save, each LF is written as CR+LF per CP/M convention, and the file is terminated with Ctrl-Z (0x1A) per CP/M file format rules.


Performance

HVI is designed for 9600 baud serial terminals. All screen updates are sized to the minimum needed for the operation:

Operation Output sent
h, l, 0, ^, $, f, F, ;, , Cursor reposition only — no text redrawn, no status bar update
j, k, Enter, w, b, e, /, ?, n, N, gg, nG Terminal scroll + one new line (~53 bytes) when viewport shifts by one row, or cursor reposition only when no scroll needed — status bar not updated
r replace character Single visual row redrawn
x, X, D, ~, s, S, C Current logical line redrawn
J, o, O, p, P, u, dw, dd, cw, Enter in insert mode Rows from cursor to bottom redrawn (rows above cursor skipped)
G, Ctrl-F, Ctrl-B, Ctrl-D, Ctrl-U, : commands Full screen redrawn

The "cursor to bottom" tier is the key optimization for editing operations: on a 24-row terminal with the cursor near the middle, it sends roughly half the bytes of a full screen refresh (~600 bytes vs ~1200 bytes at 9600 baud ≈ 0.5 seconds saved per keystroke).

The status bar is not refreshed on every j/k/Enter/nG keypress — it updates on the next edit, search, page scroll, or Ctrl-L.


Known Limitations

  • Single-level undo only (u undoes the most recent change)
  • nG in a large file scans sequentially from byte 0 — navigating to line 1000 reads the first ~1000 lines from disk (fast); navigating to line 29000 in a 30K-line file reads most of the file (slow)
  • No visual/block selection mode
  • No macro recording/playback
  • No window splitting
  • No regex search — plain substring match only (case-insensitive)

License

MIT License. Copyright (c) Juan Orlandini.

About

CP/M Minimal VI Clone in HiTech-C

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors