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