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

struct VGACursor
  col : Int32 = 0
  row : Int32 = 0
  
  def initialize
    @col = 0
    @row = 0
  end
  
  def col : Int32
    @col
  end
  def col=(c : Int32)
    @col = c
  end
  def col_incr(c : Int32)
    @col += c
  end
  
  def row : Int32
    @row
  end
  def row=(r : Int32)
    @row = r
  end
  def row_incr(r : Int32)
    @row += r
  end
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
    VGA_CURSOR.col = 0
    VGA_CURSOR.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,
    row : Int32
  ) : Nil
    KernelShim
      .set_cursor_pos(row.to_u32, col.to_u32)
  end
  
  # 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
      VGA_CURSOR.col = i
      VGA_CURSOR.row = line
  
      if ch == 0x0A_u8 # \n (not working...)
       # VGA_CURSOR.col = 0
        VGA_CURSOR.row += 1
        if VGA_CURSOR.row >= VGA_HEIGHT
          VGA_CURSOR.row = VGA_HEIGHT - 1
        end
        ch = " ".to_unsafe.value
      end
  
      offset = (VGA_CURSOR.row * VGA_WIDTH + VGA_CURSOR.col) * 2
  
      (VGA_BUFFER + offset).value = ch
      (VGA_BUFFER + offset + 1).value = color
  
      VGA_CURSOR.col += 1
  
      #  Wrap around if past screen
      if VGA_CURSOR.col >= VGA_WIDTH
        VGA_CURSOR.col = 0
        VGA_CURSOR.row += 1
        if VGA_CURSOR.row >= VGA_HEIGHT
          VGA_CURSOR.row = VGA_HEIGHT - 1
        end
      end
    end
  
    # Write a string to VGA
    def self.puts(
      str : String,
      color : UInt8? = Colors::DEFAULT,
      line : Int32? = 0,
    ) : Nil
      i = 0
      while i < str.size - 1
        ch = (str.to_unsafe + i).value
        putchar(ch, i, line, color)
        i += 1
      end
    end

end