Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active January 15, 2023 16:32
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save PlugFox/f233613aca7d5f998b53f52229d35b36 to your computer and use it in GitHub Desktop.
Save PlugFox/f233613aca7d5f998b53f52229d35b36 to your computer and use it in GitHub Desktop.
Dart Virtual Key Codes table and KeyboardObserver for win32 package, hotkey
import 'dart:async';
import 'dart:ffi' show Uint8, Uint8Pointer;
import 'package:ffi/ffi.dart' show calloc; // , malloc
import 'package:win32/win32.dart'
show GetKeyboardState, GetKeyState; // , GetAsyncKeyState;
import 'virtual_key_codes.dart';
/// Key - Virtual Key codes
/// Value - KeyStatus pair
//@immutable
class KeyboardObserver extends Stream<Map<int, KeyStatus>>
implements Sink<int> {
final StreamController<Map<int, KeyStatus>> _controller;
late final StreamSubscription<void> _periodicSub;
static const int _kDefaultObserverTickPeriodInMS = 15;
final int _observerTickPeriodInMS;
final Set<int> _observableKeys;
/// TODO: [GetKeyboardState] return same values after first run
/// ISSUE: https://github.com/timsneath/win32/issues/153
KeyboardObserver._({
int tickPeriodInMS = _kDefaultObserverTickPeriodInMS,
}) : _observableKeys = <int>{for (int i = 1; i < 256; i++) i},
_observerTickPeriodInMS = tickPeriodInMS,
_controller = StreamController<Map<int, KeyStatus>>.broadcast() {
final lpKeyboardState = calloc.allocate<Uint8>(256);
_periodicSub =
Stream<void>.periodic(Duration(milliseconds: _observerTickPeriodInMS))
.listen(
(_) {
if (_observableKeys.isEmpty) return;
GetKeyboardState(lpKeyboardState);
final statuses =
Map<int, int>.of(lpKeyboardState.asTypedList(256).asMap())
.map<int, KeyStatus>(
(key, value) => MapEntry(
key,
KeyStatus(key: key, value: value != 0 && value != 1),
),
)..removeWhere((key, _) => key < 1 || key > 255);
if (_observableKeys.length < 255) {
statuses..removeWhere((key, _) => !_observableKeys.contains(key));
}
_controller.add(statuses);
},
onDone: () {
calloc.free(lpKeyboardState);
},
cancelOnError: false,
);
}
KeyboardObserver.withKeys({
Iterable<int>? observableKeys,
int tickPeriodInMS = _kDefaultObserverTickPeriodInMS,
}) : _observableKeys = observableKeys == null
? <int>{}
: Set<int>.from(observableKeys.where((e) => e > 0 && e < 256)),
_observerTickPeriodInMS = tickPeriodInMS,
_controller = StreamController<Map<int, KeyStatus>>.broadcast() {
_periodicSub =
Stream<void>.periodic(Duration(milliseconds: _observerTickPeriodInMS))
.listen(
(_) {
if (_observableKeys.isEmpty) return;
final statuses = _observableKeys.map<KeyStatus>(
(e) => KeyStatus(
key: e,
value: GetKeyState(e) != 0 &&
GetKeyState(e) != 1, // GetAsyncKeyState(e) < 0,
),
);
_controller.add(
Map.fromEntries(
statuses.map<MapEntry<int, KeyStatus>>((s) => MapEntry(s.key, s)),
),
);
},
cancelOnError: false,
);
}
@override
bool get isBroadcast => true;
@override
StreamSubscription<Map<int, KeyStatus>> listen(
void Function(Map<int, KeyStatus> event)? onData,
{Function? onError,
void Function()? onDone,
bool? cancelOnError}) =>
_controller.stream.listen(
onData,
onError: onError,
cancelOnError: cancelOnError,
);
KeyboardObserver operator +(Object object) {
if (object is int && object > 0 && object < 256) {
_observableKeys..add(object);
} else if (object is KeyboardObserver) {
_observableKeys..addAll(object._observableKeys);
} else if (object is String) {
_observableKeys..addAll(object.keyCodes);
}
return this;
}
KeyboardObserver operator -(Object object) {
if (object is int && object > 0 && object < 256) {
_observableKeys..remove(object);
} else if (object is KeyboardObserver) {
_observableKeys..removeAll(object._observableKeys);
} else if (object is String) {
_observableKeys..removeAll(object.keyCodes);
}
return this;
}
/// Add a key to watch
@override
void add(int data) =>
data > 0 && data < 256 ? _observableKeys.add(data) : null;
/// Remove key from observation
void remove(int data) => _observableKeys.remove(data);
@override
void close() {
_periodicSub.cancel();
_controller.close();
}
}
//@immutable
class KeyStatus implements Comparable<KeyStatus>, MapEntry<int, bool> {
/// Virtual key code
@override
final int key;
/// Pressed state
/// true - pressed
/// false - released
@override
final bool value;
//@literal
const KeyStatus({
required this.key,
required this.value,
});
KeyStatusResult where<KeyStatusResult extends Object>({
required KeyStatusResult Function() pressed,
required KeyStatusResult Function() released,
}) =>
value ? pressed() : released();
@override
String toString() =>
'<Virtual key code $key is ${value ? 'pressed' : 'released'}>';
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is KeyStatus && other.key == key && other.value == value);
@override
int compareTo(KeyStatus other) =>
key.compareTo(other.key) * 2 +
(value == other.value ? 0 : (value && !other.value ? 1 : 0));
}
import 'dart:async';
import 'dart:io';
import 'keyboard_observer.dart';
import 'virtual_key_codes.dart';
void main(List<String> arguments) => runZonedGuarded(_body, _onError);
Future<void> _body() async {
print('BEGIN');
try {
final observer = KeyboardObserver.withKeys(observableKeys: 'WSDZTy'.keyCodes) +
VK_LSHIFT +
'QCRF';
observer
..add(VK_A)
..remove(VK_Z);
observer + VK_LCONTROL - VK_SPACE - 'ty';
await observer
.map<KeyStatus>((event) => event[VK_LSHIFT]!)
.distinct()
.skip(1)
.take(50)
.forEach(print); // first 50 statuses of "Shift" key
observer.close();
} on TimeoutException {
print('END with TimeoutException');
exit(2);
}
print('END');
}
void _onError(Object error, StackTrace stack) => print(' * ERROR: $error');
/*
* [V]irtual [K]ey codes table
* Symbolic constant name = decimal value 1..255
*/
/// Left mouse button
const int VK_LBUTTON = 1;
/// Right mouse button
const int VK_RBUTTON = 2;
/// Control-break processing
const int VK_CANCEL = 3;
/// Middle mouse button (three-button mouse)
const int VK_MBUTTON = 4;
/// Windows 2000: X1 mouse button
const int VK_XBUTTON1 = 5;
/// Windows 2000: X2 mouse button
const int VK_XBUTTON2 = 6;
/// BACKSPACE key
const int VK_BACK = 8;
/// TAB key
const int VK_TAB = 9;
/// CLEAR key
const int VK_CLEAR = 12;
/// ENTER key
const int VK_RETURN = 13;
/// SHIFT key
const int VK_SHIFT = 16;
/// CTRL key
const int VK_CONTROL = 17;
/// ALT key
const int VK_MENU = 18;
/// PAUSE key
const int VK_PAUSE = 19;
/// CAPS LOCK key
const int VK_CAPITAL = 20;
/// IME Kana mode
const int VK_KANA = 21;
/// IME Hanguel mode (maintained for compatibility; use VK_HANGUL)
const int VK_HANGUEL = 21;
/// IME Hangul mode
const int VK_HANGUL = 21;
/// IME Junja mode
const int VK_JUNJA = 23;
/// IME final mode
const int VK_FINAL = 24;
/// IME Hanja mode
const int VK_HANJA = 25;
/// IME Kanji mode
const int VK_KANJI = 25;
/// ESC key
const int VK_ESCAPE = 27;
/// IME convert
const int VK_CONVERT = 28;
/// IME nonconvert
const int VK_NONCONVERT = 29;
/// IME accept
const int VK_ACCEPT = 30;
/// IME mode change request
const int VK_MODECHANGE = 31;
/// SPACEBAR
const int VK_SPACE = 32;
/// PAGE UP key
const int VK_PRIOR = 33;
/// PAGE DOWN key
const int VK_NEXT = 34;
/// END key
const int VK_END = 35;
/// HOME key
const int VK_HOME = 36;
/// LEFT ARROW key
const int VK_LEFT = 37;
/// UP ARROW key
const int VK_UP = 38;
/// RIGHT ARROW key
const int VK_RIGHT = 39;
/// DOWN ARROW key
const int VK_DOWN = 40;
/// SELECT key
const int VK_SELECT = 41;
/// PRINT key
const int VK_PRINT = 42;
/// EXECUTE key
const int VK_EXECUTE = 43;
/// PRINT SCREEN key
const int VK_SNAPSHOT = 44;
/// INS key
const int VK_INSERT = 45;
/// DEL key
const int VK_DELETE = 46;
/// HELP key
const int VK_HELP = 47;
/// 0 key
const int VK_0 = 48;
/// 1 key
const int VK_1 = 49;
/// 2 key
const int VK_2 = 50;
/// 3 key
const int VK_3 = 51;
/// 4 key
const int VK_4 = 52;
/// 5 key
const int VK_5 = 53;
/// 6 key
const int VK_6 = 54;
/// 7 key
const int VK_7 = 55;
/// 8 key
const int VK_8 = 56;
/// 9 key
const int VK_9 = 57;
/// A key
const int VK_A = 65;
/// B key
const int VK_B = 66;
/// C key
const int VK_C = 67;
/// D key
const int VK_D = 68;
/// E key
const int VK_E = 69;
/// F key
const int VK_F = 70;
/// G key
const int VK_G = 71;
/// H key
const int VK_H = 72;
/// I key
const int VK_I = 73;
/// J key
const int VK_J = 74;
/// K key
const int VK_K = 75;
/// L key
const int VK_L = 76;
/// M key
const int VK_M = 77;
/// N key
const int VK_N = 78;
/// O key
const int VK_O = 79;
/// P key
const int VK_P = 80;
/// Q key
const int VK_Q = 81;
/// R key
const int VK_R = 82;
/// S key
const int VK_S = 83;
/// T key
const int VK_T = 84;
/// U key
const int VK_U = 85;
/// V key
const int VK_V = 86;
/// W key
const int VK_W = 87;
/// X key
const int VK_X = 88;
/// Y key
const int VK_Y = 89;
/// Z key
const int VK_Z = 90;
/// Left Windows key (Microsoft® Natural® keyboard)
const int VK_LWIN = 91;
/// Right Windows key (Natural keyboard)
const int VK_RWIN = 92;
/// Applications key (Natural keyboard)
const int VK_APPS = 93;
/// Computer Sleep key
const int VK_SLEEP = 95;
/// Numeric keypad 0 key
const int VK_NUMPAD0 = 96;
/// Numeric keypad 1 key
const int VK_NUMPAD1 = 97;
/// Numeric keypad 2 key
const int VK_NUMPAD2 = 98;
/// Numeric keypad 3 key
const int VK_NUMPAD3 = 99;
/// Numeric keypad 4 key
const int VK_NUMPAD4 = 100;
/// Numeric keypad 5 key
const int VK_NUMPAD5 = 101;
/// Numeric keypad 6 key
const int VK_NUMPAD6 = 102;
/// Numeric keypad 7 key
const int VK_NUMPAD7 = 103;
/// Numeric keypad 8 key
const int VK_NUMPAD8 = 104;
/// Numeric keypad 9 key
const int VK_NUMPAD9 = 105;
/// Multiply key
const int VK_MULTIPLY = 106;
/// Add key
const int VK_ADD = 107;
/// Separator key
const int VK_SEPARATOR = 108;
/// Subtract key
const int VK_SUBTRACT = 109;
/// Decimal key
const int VK_DECIMAL = 110;
/// Divide key
const int VK_DIVIDE = 111;
/// F1 key
const int VK_F1 = 112;
/// F2 key
const int VK_F2 = 113;
/// F3 key
const int VK_F3 = 114;
/// F4 key
const int VK_F4 = 115;
/// F5 key
const int VK_F5 = 116;
/// F6 key
const int VK_F6 = 117;
/// F7 key
const int VK_F7 = 118;
/// F8 key
const int VK_F8 = 119;
/// F9 key
const int VK_F9 = 120;
/// F10 key
const int VK_F10 = 121;
/// F11 key
const int VK_F11 = 122;
/// F12 key
const int VK_F12 = 123;
/// F13 key
const int VK_F13 = 124;
/// F14 key
const int VK_F14 = 125;
/// F15 key
const int VK_F15 = 126;
/// F16 key
const int VK_F16 = 127;
/// F17 key
const int VK_F17 = 128;
/// F18 key
const int VK_F18 = 129;
/// F19 key
const int VK_F19 = 130;
/// F20 key
const int VK_F20 = 131;
/// F21 key
const int VK_F21 = 132;
/// F22 key
const int VK_F22 = 133;
/// F23 key
const int VK_F23 = 134;
/// F24 key
const int VK_F24 = 135;
/// NUM LOCK key
const int VK_NUMLOCK = 144;
/// SCROLL LOCK key
const int VK_SCROLL = 145;
/// Left SHIFT key
const int VK_LSHIFT = 160;
/// Right SHIFT key
const int VK_RSHIFT = 161;
/// Left CONTROL key
const int VK_LCONTROL = 162;
/// Right CONTROL key
const int VK_RCONTROL = 163;
/// Left MENU key
const int VK_LMENU = 164;
/// Right MENU key
const int VK_RMENU = 165;
/// Windows 2000: Browser Back key
const int VK_BROWSER_BACK = 166;
/// Windows 2000: Browser Forward key
const int VK_BROWSER_FORWARD = 167;
/// Windows 2000: Browser Refresh key
const int VK_BROWSER_REFRESH = 168;
/// Windows 2000: Browser Stop key
const int VK_BROWSER_STOP = 169;
/// Windows 2000: Browser Search key
const int VK_BROWSER_SEARCH = 170;
/// Windows 2000: Browser Favorites key
const int VK_BROWSER_FAVORITES = 171;
/// Windows 2000: Browser Start and Home key
const int VK_BROWSER_HOME = 172;
/// Windows 2000: Volume Mute key
const int VK_VOLUME_MUTE = 173;
/// Windows 2000: Volume Down key
const int VK_VOLUME_DOWN = 174;
/// Windows 2000: Volume Up key
const int VK_VOLUME_UP = 175;
/// Windows 2000: Next Track key
const int VK_MEDIA_NEXT_TRACK = 176;
/// Windows 2000: Previous Track key
const int VK_MEDIA_PREV_TRACK = 177;
/// Windows 2000: Stop Media key
const int VK_MEDIA_STOP = 178;
/// Windows 2000: Play/Pause Media key
const int VK_MEDIA_PLAY_PAUSE = 179;
/// Windows 2000: Start Mail key
const int VK_LAUNCH_MAIL = 180;
/// Windows 2000: Select Media key
const int VK_LAUNCH_MEDIA_SELECT = 181;
/// Windows 2000: Start Application 1 key
const int VK_LAUNCH_APP1 = 182;
/// Windows 2000: Start Application 2 key
const int VK_LAUNCH_APP2 = 183;
/// Windows 2000: For the US standard keyboard, the ';:' key
const int VK_OEM_1 = 186;
/// Windows 2000: For any country/region, the '+' key
const int VK_OEM_PLUS = 187;
/// Windows 2000: For any country/region, the ',' key
const int VK_OEM_COMMA = 188;
/// Windows 2000: For any country/region, the '-' key
const int VK_OEM_MINUS = 189;
/// Windows 2000: For any country/region, the '.' key
const int VK_OEM_PERIOD = 190;
/// Windows 2000: For the US standard keyboard, the '/?' key
const int VK_OEM_2 = 191;
/// Windows 2000: For the US standard keyboard, the '`~' key
const int VK_OEM_3 = 192;
/// Windows 2000: For the US standard keyboard, the '[{' key
const int VK_OEM_4 = 219;
/// Windows 2000: For the US standard keyboard, the '\|' key
const int VK_OEM_5 = 220;
/// Windows 2000: For the US standard keyboard, the ']}' key
const int VK_OEM_6 = 221;
/// Windows 2000: For the US standard keyboard,
/// the 'single-quote/double-quote' key
const int VK_OEM_7 = 222;
///
const int VK_OEM_8 = 223;
/// Windows 2000: Either the angle bracket key
/// or the backslash key on the RT 102-key keyboard
const int VK_OEM_102 = 226;
/// Windows 95/98, Windows NT 4.0, Windows 2000: IME PROCESS key
const int VK_PROCESSKEY = 229;
/// // Windows 2000: Used to pass Unicode characters
/// as if they were keystrokes. The VK_PACKET key is
/// the low word of a 32-bit Virtual Key value used
/// for non-keyboard input methods. For more information,
/// see Remark in KEYBDINPUT, SendInput, WM_KEYDOWN, and WM_KEYUP
const int VK_PACKET = 231;
/// Attn key
const int VK_ATTN = 246;
/// CrSel key
const int VK_CRSEL = 247;
/// ExSel key
const int VK_EXSEL = 248;
/// Erase EOF key
const int VK_EREOF = 249;
/// Play key
const int VK_PLAY = 250;
/// Zoom key
const int VK_ZOOM = 251;
/// Reserved for future use
const int VK_NONAME = 252;
/// PA1 key
const int VK_PA1 = 253;
/// Clear key
const int VK_OEM_CLEAR = 254;
extension VirtualKeyCodesX on String {
/// Convert [String] to [Iterable] of Keyboard virtual key codes
Iterable<int> get keyCodes => codeUnits
.map<int?>((e) => _codeUnitToVirtualKeyCode[e])
.where((element) => element != null)
.whereType<int>();
}
const Map<int, int> _codeUnitToVirtualKeyCode = <int, int>{
// Spaces
9: VK_TAB,
32: VK_SPACE,
// Math
42: VK_MULTIPLY,
43: VK_ADD,
45: VK_SUBTRACT,
46: VK_DECIMAL,
47: VK_DIVIDE,
// Digits
48: VK_0,
49: VK_1,
50: VK_2,
51: VK_3,
52: VK_4,
53: VK_5,
54: VK_6,
55: VK_7,
56: VK_8,
57: VK_9,
// Upper case
65: VK_A,
66: VK_B,
67: VK_C,
68: VK_D,
69: VK_E,
70: VK_F,
71: VK_G,
72: VK_H,
73: VK_I,
74: VK_J,
75: VK_K,
76: VK_L,
77: VK_M,
78: VK_N,
79: VK_O,
80: VK_P,
81: VK_Q,
82: VK_R,
83: VK_S,
84: VK_T,
85: VK_U,
86: VK_V,
87: VK_W,
88: VK_X,
89: VK_Y,
90: VK_Z,
// Lower case
97: VK_A,
98: VK_B,
99: VK_C,
100: VK_D,
101: VK_E,
102: VK_F,
103: VK_G,
104: VK_H,
105: VK_I,
106: VK_J,
107: VK_K,
108: VK_L,
109: VK_M,
110: VK_N,
111: VK_O,
112: VK_P,
113: VK_Q,
114: VK_R,
115: VK_S,
116: VK_T,
117: VK_U,
118: VK_V,
119: VK_W,
120: VK_X,
121: VK_Y,
122: VK_Z,
// Other
124: VK_SEPARATOR,
};
@PlugFox
Copy link
Author

PlugFox commented Feb 9, 2021

Example:

final observer = KeyboardObserver(observableKeys: 'WASD'.keyCodes);
await observer
  .map<KeyStatus>((event) => event[VK_W]!)
  .distinct()
  .skip(1)
  .take(50)
  .forEach(print); // first 50 statuses of "W" key
observer.close();

@PlugFox
Copy link
Author

PlugFox commented Feb 9, 2021

TODO: GetKeyboardState vs GetKeyState benchmark

@PlugFox
Copy link
Author

PlugFox commented Feb 9, 2021

TODO: BlockInput();

@PlugFox
Copy link
Author

PlugFox commented Feb 9, 2021

TODO: SendInput()

Example:

const VK_A = 65;
final kbd = calloc<INPUT>()
  ..ref.type = INPUT_KEYBOARD
  ..ki.wVk = VK_A;
final result = SendInput(1, kbd, sizeOf<INPUT>());
if (result != TRUE) throw UnsupportedError('Can\'t send key $VK_A');
print(result == TRUE ? 'success' : 'fail');
calloc.free(kbd);

@PlugFox
Copy link
Author

PlugFox commented Feb 9, 2021

Example mouse click

void mouseLClick() {
  final kbd = calloc<INPUT>();
  try {
    kbd
      ..mi.time = 0
      ..ref.type = INPUT_MOUSE
      ..mi.dwFlags = MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP;
    final resultDown = SendInput(1, kbd, sizeOf<INPUT>());
    if (resultDown != 1) throw UnsupportedError('Can\'t click');
    print('success');
  } finally {
    calloc.free(kbd);
  }
}

Future<void> mouseClickAsync(
    [Duration delay = const Duration(milliseconds: 0)]) async {
  final kbd = calloc<INPUT>(1);
  try {
    kbd
      ..ref.type = INPUT_MOUSE
      ..mi.dwFlags = MOUSEEVENTF_RIGHTDOWN;
    final resultDown = SendInput(1, kbd, sizeOf<INPUT>());
    if (resultDown != 1) throw UnsupportedError('Can\'t send keys $VK_RBUTTON');
    await Future<void>.delayed(delay);
    kbd
      ..ref.type = INPUT_MOUSE
      ..mi.dwFlags = MOUSEEVENTF_RIGHTUP;
    final resultUp = SendInput(1, kbd, sizeOf<INPUT>());
    if (resultUp != 1) throw UnsupportedError('Can\'t send keys $VK_RBUTTON');
    print('success');
  } finally {
    calloc.free(kbd);
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment