~kernel/vga.cr
.cr
Crystal
(text/x-crystal)
# Definitions from kernel/entry.asm
lib KernelShim
  fun hide_cursor()
  fun show_cursor()
  fun set_cursor_pos(
    row : UInt32,
    col : UInt32
  )
end

struct VGACursor
  @col : Int32 = 0
  property col
  
  @row : Int32 = 0
  property row
end

module VGA
  # VGA text mode buffer address (0xB8000)
  VGA_ADDRESS = 0xB8000_u64
  VGA_BUFFER = Pointer(UInt8).new(VGA_ADDRESS)

  # Buffer size
  VGA_WIDTH = 80
  VGA_HEIGHT = 25
  VGA_SIZE = VGA_WIDTH * VGA_HEIGHT
  
  # Cursor state
  VGA_CURSOR = VGACursor.new

  # Default color: white text on black background
  DEFAULT_COLOR = 0x07_u8
  
  module Colors
    DEFAULT       = 0x07_u8
    BLACK_ON_CYAN = 0xB0_u8
    BLACK_ON_LIME = 0xA0_u8
  end

  # Initialize VGA subsystem (clear screen)
  def self.init : Nil
    clear
  end

  # Clear the entire VGA screen
  def self.clear : Nil
    i = 0
    while i < VGA_SIZE
      (VGA_BUFFER + (i * 2)).value = " ".to_unsafe.value
      (VGA_BUFFER + (i * 2) + 1).value = Colors::DEFAULT
      i += 1
    end
    col = 0
    row = 0
  end
  
  # Hide cursor
  def self.hide_cursor
    KernelShim.hide_cursor
  end
  
  # Show cursor
  def self.show_cursor
    KernelShim.show_cursor
  end

  # Set typing cursor position
  def self.set_cursor(
    col : Int32,
    next_row : Int32
  ) : Nil
    KernelShim
      .set_cursor_pos(next_row.to_u32, col.to_u32)
    @@row = next_row
  end
  
  @@puts_count = 0
  property puts_count
  
  @@col = 0
  class_property col
  
  @@row = 0
  class_property row
  
  # Write a single character to the current cursor position
  def self.putchar(
    ch : UInt8,
    i : Int32? = 0,
    line : Int32? = 0,
    color : UInt8? = Colors::DEFAULT
  ) : Nil
    @@col ||= i || 0
    @@row ||= line || 0

    # somehow doesn't work...
    if ch == 0x0A_u8 # \n handler
      @@col = 0
      @@row += 1
      if @@row >= VGA_HEIGHT
        @@row = VGA_HEIGHT - 1
      end
    end

    offset = (@@row * VGA_WIDTH + @@col) * 2

    (VGA_BUFFER + offset).value = ch
    (VGA_BUFFER + offset + 1).value = color

    @@col += 1

    #  Wrap around if past screen
    if @@col >= VGA_WIDTH
      @@col = 0
      @@row += 1
      if @@row >= VGA_HEIGHT
        @@row = VGA_HEIGHT - 1
      end
    end
  end

  # Write a string to VGA
  def self.puts(
    str : String,
    color : UInt8? = Colors::DEFAULT,
    line : Int32? = nil,
  ) : Nil
    i = 0
    @@row ||= (line || 0)
    while i < str.size
      ch = (str.to_unsafe + i).value
      if ch == 0x0A_u8
        @@row += 1
        @@col = 0
        i += 1
        next
      end
      putchar(ch, i, @@row, color)
      i += 1
    end
    
    VGA.set_cursor(0, @@row)
  end

end