Skip to main content

[console]

# =============================================================================
# CONSOLE (FRAMEWORK FILE) VERSION 2026-04-19
# =============================================================================
# The console system prints text strings, scrolls the screen, and blinks
# the cursor. Each letter is a tile that is rendered using tile_layer_a.
# If your program doesn't need to display text, you could delete this file.

# -----------------------------------------------------------------------------
module console
var _input_queue_index: byte

var _row: byte
var _col: byte
var _pos: pair

var _color_addend: pair # (0..9) << 10
var _blink_save: pair
var _blink: bool

var cursor_visible: bool

var scroll_off: bool
var reverse: bool
var matte: bool

var ctrl_key: bool

# ---------------------------------------------------------------------------
func init(io_tilemap: io_tilemap)
# Clear the input queue
console::_input_queue_index <- io::input_queue_end_index

# Start with white because the background is black
console::_color_addend <- 1024

console::_print_char('{cls}')

# Initialize the console tilemap
io_tilemap.col_count <- 64
io_tilemap.row_count <- 32
io_tilemap.tile_codes_address <- to_address(kernel::console_grid)
end func

# ---------------------------------------------------------------------------
func _blink_off()
if console::_blink then
console::_blink <- false
kernel::console_grid[console::_pos] <- console::_blink_save
end if
end func

# ---------------------------------------------------------------------------
# *Assuming* _blink is false, this blinks on unless the cursor is not visible
func _blink_restore()
if console::cursor_visible then
console::_blink <- true
var c: pair
c <- kernel::console_grid[console::_pos]
console::_blink_save <- c

c <- to_pair(math::bit_and(c, $3ff)) # remove the old color
c <- to_pair(c + console::_color_addend) # add the new color
c <- to_pair(math::bit_xor(c, $4000)) # reverse
kernel::console_grid[console::_pos] <- c
end if
end func

# ---------------------------------------------------------------------------
# Specifies the color (Palix system theme) to be used when printing text.
func set_color(color: int)
console::_blink_off()

if color < 0 or color > 15
do kernel::fail_code(20) # function was called with an invalid argument

console::_color_addend <- to_pair(color * 1024)

console::_blink_restore()
end func

# ---------------------------------------------------------------------------
func print(s: string)
console::_print(s, s.size)
end func

# ---------------------------------------------------------------------------
func print_text(text: text)
console::_print(text._chars, text.length)
end func

# ---------------------------------------------------------------------------
func _print(text: char[], size: int)
console::_blink_off()

var i: int
i <- 0

loop
if i >= size
do drop

var c: pair
c <- text[i]

console::_print_char(c)

i <- i + 1
end loop

console::_blink_restore()
end func

# ---------------------------------------------------------------------------
# Print a number
func print_int(n: int)
console::_blink_off()

var c: int
c <- n

# To handle MIN_INT properly, the trick is to use negative math
if n < 0 then
console::print_char('-')
else
c <- -c
end if

# Extract digits from right-to-left, putting them into scratch_bytes[]
var i: int, digit: int
i <- 0
loop
digit <- -(c % 10)
unsafe(kernel::scratch_bytes)[i] <- to_byte(48 + digit)

c <- c / 10
if c = 0
do drop

i <- i + 1
end loop

# Now read scratch_bytes[] in reverse, so digits are left-to-right
loop
console::_print_char(kernel::scratch_bytes[i])

i <- i - 1
if i < 0
do drop
end loop

console::_blink_restore()
end func

# ---------------------------------------------------------------------------
# Print one character that is specified using its HASCII code.
func print_char(c: pair)
console::_blink_off()

console::_print_char(c)

console::_blink_restore()
end func

# ---------------------------------------------------------------------------
func _print_char(c: pair)
var col: byte
var row: byte
var pos: pair

col <- console::_col
row <- console::_row
pos <- console::_pos

if c < 0
do c <- to_pair(c + 256)

if c < 32 then
# Control codes

if c = '{reset}' then
console::scroll_off <- false
console::reverse <- false
console::matte <- false
elsif c = '{soff}' then
# scroll off
console::scroll_off <- true
elsif c = '{sup}' then
# scroll up
console::_scroll_up()
elsif c = '{sdown}' then
# scroll down

# Offsets are double because they are pairs
kernel::copy_memory_bytes(
| to_address(kernel::console_grid) + 128,
| to_address(kernel::console_grid),
| 3456) # 64 * 27 * 2

kernel::set_memory_pairs(
| to_address(kernel::console_grid), # 64 * 27 * 2
| 32, # space
| 40)
elsif c = '{cls}' then
# clear screen
kernel::set_memory_pairs(
| to_address(kernel::console_grid),
| ' ',
| 1792) # 64 * 28

pos <- 0
col <- 0
row <- 0
elsif c = '{cll}' then
# clear line
pos <- to_pair(pos - col)
col <- 0

kernel::set_memory_pairs(
| to_address(kernel::console_grid) + pos * 2,
| ' ',
| 40)
elsif c = '{tab}' then
var tab_amount: int
tab_amount <- 8 - to_byte(math::bit_and(col, 7)) # TODO: improve this
col <- to_byte(col + tab_amount)
pos <- to_pair(pos + tab_amount)

if col >= 40 then
pos <- to_pair(pos - col + 64)
col <- 0
row <- to_byte(row + 1)
end if
elsif c = '{n}' then
# newline (lf)
pos <- to_pair(pos - col + 64)
col <- 0
row <- to_byte(row + 1)
elsif c = '{home}' then
pos <- 0
col <- 0
row <- 0
elsif c = '{end}' then
pos <- 1728 # 27 * 64
col <- 0
row <- 27
elsif c = '{cr}' then
# carriage return
pos <- to_pair(pos - col)
col <- 0
elsif c = '{rev}' then
console::reverse <- true
elsif c = 15 then
# {mat}
console::matte <- true
elsif c >= 16 and c <= 25 then
# color codes
console::_color_addend <- to_pair((c - 16) * 1024)
elsif c = '{u}' then
# up
if row > 0 then
pos <- to_pair(pos - 64)
row <- to_byte(row - 1)
end if
elsif c = '{d}' then
# down
pos <- to_pair(pos + 64)
row <- to_byte(row + 1)
elsif c = '{l}' then
# left
if col > 0 then
pos <- to_pair(pos - 1)
col <- to_byte(col - 1)
else
if row > 0 then
pos <- to_pair(pos - col - (64 - 39))
col <- 39
row <- to_byte(row - 1)
end if
end if
elsif c = '{r}' then
# right
if col < 39 then
pos <- to_pair(pos + 1)
col <- to_byte(col + 1)
else
pos <- to_pair(pos - col + 64)
col <- 0
row <- to_byte(row + 1)
end if
end if
else
# Add in the color bits
var p: pair
p <- to_pair(c + console::_color_addend)
if console::reverse then
p <- to_pair(math::bit_or(p, $4000))
end if

if console::matte then
p <- to_pair(math::bit_or(p, $8000))
end if

kernel::console_grid[pos] <- p

if col < 39 then
pos <- to_pair(pos + 1)
col <- to_byte(col + 1)
else
pos <- to_pair(pos - col + 64)
col <- 0
row <- to_byte(row + 1)
end if
end if

if row >= 28 then
row <- to_byte(row - 1)
pos <- to_pair(pos - 64)

if not console::scroll_off then
console::_scroll_up()
end if
end if

console::_col <- col
console::_row <- row
console::_pos <- pos
end func

# ---------------------------------------------------------------------------
func _scroll_up()
# Offsets are double because they are pairs
kernel::copy_memory_bytes(
| to_address(kernel::console_grid),
| to_address(kernel::console_grid) + 128,
| 3456) # 64 * 27 * 2

kernel::set_memory_pairs(
| to_address(kernel::console_grid) + 3456, # 64 * 27 * 2
| ' ',
| 40)
end func

# ---------------------------------------------------------------------------
# Extracts the next key from the keyboard buffer, returning its HASCII code.
# If no key was pressed, then 0 is returned.
func read_key(): byte
var index: byte, end_index: byte
var result: byte

end_index <- io::input_queue_end_index

index <- console::_input_queue_index
result <- 0

loop
if index = end_index
do drop

# Read at index
var event: ^io_input_event
event <- io::input_queue[index]

index <- to_byte(index + 1)
if index >= 16
do index <- 0

var event_kind: byte, event_keyid: byte
event_kind <- event.kind
event_keyid <- event.keyid

if event_kind = 0 or event_kind = 1 then # key down or key repeat
if event_keyid = 18 or event_keyid = 19 then # left/right ctrl
console::ctrl_key <- true
elsif event.hascii > 0 and event.hascii < 256 then
result <- to_byte(event.hascii)
drop
end if
else
if event_keyid = 18 or event_keyid = 19 then # left/right ctrl
console::ctrl_key <- false
end if
end if
end loop

console::_input_queue_index <- index

return result
end func

# ---------------------------------------------------------------------------
# Saves the current location of the cursor for use with goto_bookmark().
func get_bookmark(): pair
return console::_pos
end func

# ---------------------------------------------------------------------------
# Move the cursor to a bookmark that was made using get_bookmark()
func goto_bookmark(bookmark: pair)
if bookmark >= 1792 # 64 * 28
do kernel::fail_code(20) # function was called with an invalid argument

console::_blink_off()

# divide by 64
console::_row <- to_byte(math::shift_right_unsigned(bookmark, 6))
# remainder
console::_col <- to_byte(math::bit_and(bookmark, 63))
console::_pos <- bookmark

console::_blink_restore()
end func

# ---------------------------------------------------------------------------
# The main loop should call this to blink the cursor.
func think()
var blink_counter: int

blink_counter <- math::bit_and(io::frame_counter, 31)

if not console::cursor_visible or blink_counter >= 16 then
console::_blink_off()
else
if not console::_blink then
console::_blink <- true
console::_blink_restore()
end if
end if
end func
end module