Skip to main content

[console]

# =============================================================================
# CONSOLE (FRAMEWORK FILE) VERSION 2025-11-29
# =============================================================================
# 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 buffer: byte[]

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

var ctrl_key: bool

# ---------------------------------------------------------------------------
func init()
new byte[](10) -> console::buffer
console::buffer.resize(10)

# Clear the input queue
io::input_queue_end_index -> console::_input_queue_index

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

console::_print_char(5) # {cls} clear screen

if art::fonts.size > 0
do engine::tile_layer_a.load_tileset(art::fonts[0])

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

# ---------------------------------------------------------------------------
func _blink_off()
if console::_blink then
false -> console::_blink
console::_blink_save -> kernel::console_grid[console::_pos]
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
true -> console::_blink
var c: pair
kernel::console_grid[console::_pos] -> c
c -> console::_blink_save

to_pair(math::bit_and(c, $3ff)) -> c # remove the old color
to_pair(c + console::_color_addend) -> c # add the new color
to_pair(math::bit_xor(c, $4000)) -> c # reverse
c -> kernel::console_grid[console::_pos]
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("set_color() invalid color")

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

console::_blink_restore()
end func

# ---------------------------------------------------------------------------
# Print a text string on the console.
func print(text: string)
console::_blink_off()

var size: int
text.size -> size

var i: int
0 -> i

loop
if i >= size
do drop

var c: pair
text[i] -> c

console::_print_char(c)

i + 1 -> i
end loop

console::_blink_restore()
end func

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

var c: int
n -> c

if c < 0 then
console::_print_char(45) # "-"
-c -> c
end if

# Extract the digits from right-to-left, putting them in buffer[]
var i: int, d: int
0 -> i
loop
c % 10 -> d
to_byte(48+d) -> console::buffer[i]

if c < 10
do drop

i + 1 -> i

c / 10 -> c
end loop

# Now print buffer[] in reverse order, so digits are left-to-right
loop
console::_print_char(console::buffer[i])
i - 1 -> i
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

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

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

if c < 32 then
# Control codes

if c = 1 then
# {reset}
false -> console::scroll_off
false -> console::reverse
false -> console::matte
elsif c = 2 then
# {soff} scroll off
true -> console::scroll_off
elsif c = 3 then
# {sup} scroll up
console::_scroll_up()
elsif c = 4 then
# {sdown} scroll down

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

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

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

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

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

p -> kernel::console_grid[pos]

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

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

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

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

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

kernel::memset_pair(
| to_address(kernel::console_grid) + 3456, # 64 * 27 * 2
| 32, # space
| 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

io::input_queue_end_index -> end_index

console::_input_queue_index -> index
0 -> result

loop
if index = end_index
do drop

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

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

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
true -> console::ctrl_key
elsif event.hascii > 0 and event.hascii < 256 then
to_byte(event.hascii) -> result
drop
end if
else
if event_keyid = 18 or event_keyid = 19 then # left/right ctrl
false -> console::ctrl_key
end if
end if
end loop

index -> console::_input_queue_index

return result
end func

# ---------------------------------------------------------------------------
# Like read_key() but does not extract the key
func check_key(): byte
var result: byte

if console::_input_queue_index = io::input_queue_end_index then
0 -> result
else
1 -> result
end if

return result
end func

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

# ---------------------------------------------------------------------------
# Move the cursor to a bookmark that was made using get_bookmark()
func goto_bookmark(bookmark: pair)
if bookmark >= 3200 # 64 * 25 * 2
do kernel::fail("invalid bookmark")

console::_blink_off()

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

console::_blink_restore()
end func

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

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

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