Cursor & Scroll
Automatic Scrolling
When text reaches row 24 (the last row), the screen scrolls up automatically. The implementation copies each row to the row above it, then clears the last row:
#![allow(unused)]
fn main() {
fn scroll(&mut self) {
unsafe {
for row in 1..VGA_HEIGHT {
for col in 0..VGA_WIDTH {
let src_offset = (row * VGA_WIDTH + col) * 2;
let dst_offset = ((row - 1) * VGA_WIDTH + col) * 2;
*VGA_BUFFER.add(dst_offset) = *VGA_BUFFER.add(src_offset);
*VGA_BUFFER.add(dst_offset + 1) = *VGA_BUFFER.add(src_offset + 1);
}
}
// Clear the last line
for col in 0..VGA_WIDTH {
let offset = ((VGA_HEIGHT - 1) * VGA_WIDTH + col) * 2;
*VGA_BUFFER.add(offset) = b' ';
*VGA_BUFFER.add(offset + 1) = self.color;
}
}
self.row = VGA_HEIGHT - 1;
self.column = 0;
}
}
This is software scrolling — we copy the entire buffer. The alternative (hardware scrolling via the VGA start address register) is more efficient but more complex to implement.
Hardware Cursor
The VGA card maintains a blinking cursor position in hardware registers, separate from our software cursor tracking. If we don’t keep these in sync, the cursor appears in the wrong place on screen.
Control is via port I/O to two registers:
| Port | Role |
|---|---|
0x3D4 | Index register — select which internal register to access |
0x3D5 | Data register — read or write the selected register |
Register 0x0E holds the high byte of cursor position, 0x0F holds the low byte. Position is a flat index: row * 80 + col.
#![allow(unused)]
fn main() {
fn update_cursor(&self) {
let pos = self.row * VGA_WIDTH + self.column;
unsafe {
outb(VGA_CTRL_PORT, 0x0E);
outb(VGA_DATA_PORT, (pos >> 8) as u8);
outb(VGA_CTRL_PORT, 0x0F);
outb(VGA_DATA_PORT, pos as u8);
}
}
}
update_cursor() is called at the end of every write_byte() call, so the hardware cursor always tracks the software state.
Hiding and Showing the Cursor
#![allow(unused)]
fn main() {
pub fn set_cursor_visible(&self, visible: bool) {
unsafe {
outb(VGA_CTRL_PORT, 0x0A);
let cursor_start = if visible { 0x0E } else { 0x20 };
outb(VGA_DATA_PORT, cursor_start);
}
}
}
Register 0x0A controls cursor start scanline. Setting bit 5 (0x20) disables the cursor. Setting it to 0x0E re-enables it with a default underline style.
Backspace
Implemented in KFS_2 for the shell. It moves the column back by one and overwrites with a space:
#![allow(unused)]
fn main() {
pub fn backspace(&mut self) {
if self.column == 0 {
return; // Can't backspace at start of line
}
self.column -= 1;
let offset = (self.row * VGA_WIDTH + self.column) * 2;
unsafe {
*VGA_BUFFER.add(offset) = b' ';
*VGA_BUFFER.add(offset + 1) = self.color;
}
self.update_cursor();
}
}
Note: This doesn’t handle backspacing across a newline (going from column 0 to the previous row). That’s left as a future improvement.