~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

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

  # Misc
  TAB_SIZE = 2

  # Default color: white text on black background
  DEFAULT_COLOR = 0x07_u8
  
  def self.color_code(
    fg : VGA::Color,
    bg : VGA::Color,
    #char : UInt8
  ) : UInt16
    (bg.value << 4) | fg.value
    # (attrib << 8) | char.to_u8
  end
  
  enum Color : UInt16
    Black      =  0
    Blue       =  1
    Green      =  2
    Cyan       =  3
    Red        =  4
    Magenta    =  5
    Brown      =  6
    LightGray  =  7
    DarkGray   =  8
    LightBlue  =  9
    LightGreen = 10
    LightCyan  = 11
    LightRed   = 12
    Pink       = 13
    Yellow     = 14
    White      = 15
  end
  
  module Colors
    DEFAULT        = 0x07_u8
    BLACK_ON_CYAN  = 0xB0_u8
    BLACK_ON_LIME  = 0xA0_u8
    RED_ON_BLACK   = 0x04_u8
    GREEN_ON_BLACK = 0x02_u8
    CYAN_ON_BLACK  = 0x03_u8
    GREEN_ON_WHITE = 0xF2_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(
    next_col : Int32,
    next_row : Int32
  ) : Nil
    KernelShim
      .set_cursor_pos(next_row.to_u32, col.to_u32)
    @@row = next_row
    @@col = next_col
  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

    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
    @@col ||= 0
    @@row ||= (line || 0)
    i = 0
    while i < str.size
      ch = (str.to_unsafe + i).value

      # todo: support more control chars
      # - \b - 0x08 : Backspace (col - 1)
      # - \a - 0x07 : Bell (ring sound) ?
      # - \0 - 0x00 : NUL string terminator
      # - \r - 0x0D : Cariage Return (col = 0)
      # - \f - 0x0C : Form Feed (row += VGA_HEIGHT)
      # - ESC - 0x1B : Escape (colors, etc)
      if ch == 0x0A_u8 # \n
        @@row += 1
        @@col = 0
        i += 1
        next
      elsif ch == 0x09_u8 # \t
        @@col += TAB_SIZE
        i += 1
        next
      elsif ch == 0x1B_u8 # \ESC
        # i.e. reset all: \x1b[0m
        # i.e. fg green: \x1b[32m
        # i.e. bg green: \x1b[42m
        # read next chars til' "m"
        # convert args to color
        # set cell color (as default)
        # skip args (don't print)
        j = 0
        # args = [] of String
        is_reset = false
        while ((str.to_unsafe + i + j).value) != 0x6D_u8 # m letter
          # do something with args
          # i + 1    = [ start
          # i + 2..; = fg color
          # i + ;..; = bg color (optional)
          # i + ;..; = style bit (optional)
          # i + ;..m = end
          if str[i+j+1] == '[' && str[i+j+2] == '0' && str[i+j+3] == 'm'
            is_reset = true
          else
            is_reset = false
          end
          j += 1
        end
        #(str.to_unsafe + i + j).value = 0xB0_u8
        i += j # skip ESC sequence length chars
        puts "[" if is_reset == false
        puts "]" if is_reset
        i += 1
        next
      end

      putchar(ch, @@col, @@row, color)
      i += 1
    end
    
    VGA.set_cursor(@@col, @@row)
  end

end