Try to simulate keyboard action on Windows

I read the win32 api and tried to write the following code but sadly it didn t work. Maybe someone can come in to help me. :face_with_peeking_eye:

@[Link("User32")]
lib LibUser32
  alias WORD = UInt16
  alias UINT = UInt32
  alias DWORD = UInt32
  alias ULONG_PTR = LibC::ULong

  union LPINPUT
    ki : KEYBDINPUT
  end

  struct INPUT
    type : DWORD
    input : LPINPUT
  end

  struct KEYBDINPUT
    wVk : WORD
    wScan : WORD
    dwFlags : DWORD
    time : DWORD
    dwExtraInfo : ULONG_PTR
  end

  fun SendInput = "SendInput"(cInputs : UINT, pInputs : INPUT*, cbSize : Int32) : UINT
end

def press_key(key : UInt16)
  inputs = [] of LibUser32::INPUT

  # Key down
  inputs << LibUser32::INPUT.new(
    type: 1,
    input: LibUser32::LPINPUT.new(
      ki: LibUser32::KEYBDINPUT.new(
        wVk: key,
        wScan: 0,
        dwFlags: 0,
        time: 0,
        dwExtraInfo: 0
      )
    )
  )

  # Key up
  inputs << LibUser32::INPUT.new(
    type: 1,
    input: LibUser32::LPINPUT.new(
      ki: LibUser32::KEYBDINPUT.new(
        wVk: key,
        wScan: 0,
        dwFlags: 2, # KEYEVENTF_KEYUP
        time: 0,
        dwExtraInfo: 0
      )
    )
  )

  # Send inputs
  LibUser32.SendInput(inputs.size.to_u32, inputs.to_unsafe, sizeof(LibUser32::INPUT))
end

keys = [87_u16, 83_u16, 65_u16, 68_u16] # Virtual key codes for 'w', 's', 'a', 'd'

10.times do
  keys.each do |key|
    press_key(key)
    sleep 0.1
  end
end

You should add MOUSEINPUT and HAREWAREINPUT definitions to get correct size of LPINPUT

@[Link("User32")]
lib LibUser32
  alias WORD = UInt16
  alias UINT = UInt32
  alias DWORD = UInt32
  alias ULONG_PTR = LibC::ULONG_PTR
  alias LONG = LibC::LONG

  @[Extern]
  union LPINPUT
    ki : KEYBDINPUT
    mi : MOUSEINPUT
    hi : HARDWAREINPUT
  end

  @[Extern]
  struct INPUT
    type : DWORD
    input : LPINPUT
  end

  @[Extern]
  struct KEYBDINPUT
    wVk : WORD
    wScan : WORD
    dwFlags : DWORD
    time : DWORD
    dwExtraInfo : ULONG_PTR
  end

  @[Extern]
  struct MOUSEINPUT
    dx : LONG
    dy : LONG
    mouseData : DWORD
    dwFlags : DWORD
    time : DWORD
    dwExtraInfo : ULONG_PTR
  end

  @[Extern]
  struct HARDWAREINPUT
    uMsg : DWORD
    uParaml : WORD
    uParamH : WORD
  end

  fun SendInput = "SendInput"(cInputs : UINT, pInputs : INPUT*, cbSize : Int32) : UINT
end

def press_key(key : UInt16)
  inputs = [] of LibUser32::INPUT

  # Key down
  inputs << LibUser32::INPUT.new(
    type: 1,
    input: LibUser32::LPINPUT.new(
      ki: LibUser32::KEYBDINPUT.new(
        wVk: key,
        wScan: 0,
        dwFlags: 0,
        time: 0,
        dwExtraInfo: 0
      )
    )
  )

  # Key up
  inputs << LibUser32::INPUT.new(
    type: 1,
    input: LibUser32::LPINPUT.new(
      ki: LibUser32::KEYBDINPUT.new(
        wVk: key,
        wScan: 0,
        dwFlags: 2, # KEYEVENTF_KEYUP
        time: 0,
        dwExtraInfo: 0
      )
    )
  )

  # Send inputs
  LibUser32.SendInput(inputs.size.to_u32, inputs.to_unsafe, sizeof(LibUser32::INPUT))
end

keys = [87_u16, 83_u16, 65_u16, 68_u16] # Virtual key codes for 'w', 's', 'a', 'd'

10.times do
  keys.each do |key|
    puts "press #{key}"
    puts "return #{press_key(key)}"
    puts "last error: #{LibC.GetLastError}"
    sleep 0.1
  end
end
1 Like

Cool, that works. I really appreciate it.

I ran into a new issue when trying to simplify press_key. The error code is 258(WAIT_TIMEOUT), what could be the reason?

def click_key(key : UInt16)
  # inputs = Array.new(2, LibUser32::INPUT.new)
  inputs = Slice.new(2, LibUser32::INPUT.new)

  # Key down event
  inputs[0].type = 1
  inputs[0].input.ki.wVk = key

  # Key up event
  inputs[1].type = 1
  inputs[1].input.ki.wVk = key
  inputs[1].input.ki.dwFlags = 2 # Key up flag (KEYEVENTF_KEYUP)

  # LibUser32.SendInput(2, inputs.to_unsafe, sizeof(LibUser32::INPUT))
  LibUser32.SendInput(2, inputs, sizeof(LibUser32::INPUT))
end

you can add pp! inputs to check if correct

def click_key(key : UInt16)
  inputs = uninitialized LibUser32::INPUT[2]
  inputs[0] =
    LibUser32::INPUT.new(
      type: 1,
      input: LibUser32::LPINPUT.new(
        ki: LibUser32::KEYBDINPUT.new(
          wVk: key,
          wScan: 0,
          dwFlags: 0,
          time: 0,
          dwExtraInfo: 0
        )
      )
    )

  inputs[1] =
    LibUser32::INPUT.new(
      type: 1,
      input: LibUser32::LPINPUT.new(
        ki: LibUser32::KEYBDINPUT.new(
          wVk: key,
          wScan: 0,
          dwFlags: 2, # Key up flag (KEYEVENTF_KEYUP)
          time: 0,
          dwExtraInfo: 0
        )))

  LibUser32.SendInput(2, inputs, sizeof(LibUser32::INPUT))
end

Oh, thanks again. For some reason, I couldn’t just modify the assigned inputs[0].type, but I did find a way to make it work without having to define the entire structure.

def click_key(key : UInt16)
  input = LibUser32::INPUT.new
  input.type = 1

  # Key down event
  input.input.ki.wVk = key
  LibUser32.SendInput(1, pointerof(input), sizeof(LibUser32::INPUT))

  # Key up event
  input.input.ki.wVk = key
  input.input.ki.dwFlags = 2 # Key up flag (KEYEVENTF_KEYUP)
  LibUser32.SendInput(1, pointerof(input), sizeof(LibUser32::INPUT))
end
1 Like

You’re dealing with structs that are returned by value (unlike classes that always return a reference aka pointer). This means that inputs[0].type = 1 returns a copy of the input at index 0, then sets copy.type to 1. The original struct in inputs at index 0 is left unchanged.

In cases such as these, we must use pointers and enter unsafe land:

input = inputs.to_unsafe + index
input.value.type = 1

The Pointer#value method is special: it does dereference the pointer but it won’t return a copy, which allows us to operate on the actual value pointed by the pointer.

Now I’m surprised that input.input.ki.wVk = key is working :face_with_raised_eyebrow:

2 Likes