Gaining Bitwisdom from Keyboard Shortcuts
February 20, 2022Managing keyboard shortcuts in JavaScript/TypeScript is a real shitshow. Let's say you have the following keyboard shortcuts (assuming you're on macOS):
- Do Thing A:
Command
+L
- Do Thing B:
Command
+Control
+L
- Do Thing C:
Command
+Control
+Shift
+L
Here's one way to implement it:
document.addEventListener("keydown", (event: KeyboardEvent) => {
if (
event.metaKey &&
!event.altKey &&
!event.ctrlKey &&
!event.shiftKey &&
event.code === "KeyL"
) {
doThingA();
}
if (
event.metaKey &&
event.ctrlKey &&
!event.altKey &&
!event.shiftKey &&
event.code === "KeyL"
) {
doThingB();
}
if (
event.metaKey &&
event.ctrlKey &&
event.shiftKey &&
!event.altKey &&
event.code === "KeyL"
) {
doThingC();
}
});
Here's another way, which is less code but more confusing (in my opinion):
document.addEventListener("keydown", (event: KeyboardEvent) => {
if (event.metaKey && !event.altKey && event.code === "KeyL") {
if (event.ctrlKey) {
if (event.shiftKey) {
doThingC();
} else {
doThingB();
}
} else {
doThingA();
}
}
});
So your choices out-of-the-box are to make it confusing or make it verbose. Good news! It turns out there's a third option: bit flags.
I needed to add quite a few keyboard shortcuts to a side project I'm working on, so I implemented my own little keyboard shortcut library using bit flags.
My esteemed colleague, Matt Mayhem (of Bad Shadows and No Tomorrow Boys fame),
was reviewing my pull request and wanted to know what the hell was going on with all the &
and |
operators, so that is why this post exists.
Before I dig into bit shifting, bit flags, and bitwise operators, let's all hop on the trolley to Binary Junction to talk about base-2 numbers.
What's the Deal with Binary?
Most of us operate in the base-10 (or decimal) universe, which I'm too lazy to explain, so I'll let someone else do it.
Binary numbers are expressed in the base-2 numeral system, which is just a fancy way of saying it only uses two symbols: 0
and 1
.
- Note
- I'll be using the terms base-10 and decimal interchangeably, but they refer to the same thing.
Counting in binary is a little weird, so let's talk about that weirdness.
We're going to use JavaScript's parseInt()
function to get the decimal representation of a binary number.
The first argument is the binary number string and the second is the radix. To get the decimal value, I'm using a radix of 2
(for base-2 number system).
What do you think will get logged out here?
console.log(parseInt("101", 2));
If you guessed 5
, buy yourself a drink! If 101
= 5
sounds like bullshit to you, bear with me.
You count binary from right to left. As you move from right to left, the decimal representation of that bit is:
Bit Value * (2 ^ Position Index from Right)
You find the decimal value for each bit and add them all up. So here's how you turn 101
into 5
:
Bit | Position | Math | Decimal Value |
---|---|---|---|
1 |
0 | 1 * 2⁰ | 1 |
0 |
1 | 0 * 2¹ | 0 |
1 |
2 | 1 * 2² | 4 |
Total | 5 |
That right-most column represents the decimal value of each bit.
Since the 0th and 2nd bits are 1
, you multiply 1
by 2⁰ (1
) and 2² (4
) respectively and add them up to get 5
.
JavaScript uses 32-bit signed integers for bitwise operations. That means that any bit-twiddling under the hood operates on binary numbers that look like this:
11111111111111111111111111111111
I can save you some time spent counting and assure you that there are 32x 1
s in that number.
Each 1
represents a bit, which is where "32-bit" comes from.
A 32-bit signed integer has a minimum value of -2147483648
and a maximum value of 2147483647
.
But if you were to run this:
console.log(parseInt("11111111111111111111111111111111", 2));
It logs out 4294967295
. How the hell can you get all the way up to 4294967295
?
Well that's easy, the very first bit is the "sign bit" (and the most significant bit), so flipping it to 1
makes the number unsigned.
Since we're no longer dealing with negative numbers, we just shimmy everything over by 2147483648
.
The minimum of -2147483648
becomes 0
, and the maximum of 2147483647
becomes 4294967295
.
See for yourself:
console.log(parseInt("11111111111111111111111111111111", 2)); // 4294967295
// ^ Unsigned!
console.log(parseInt("01111111111111111111111111111111", 2)); // 2147483647
// ^ Signed!
Now that you have a better understanding of binary numbers (hopefully), let's rap about bit shifting and bit flags.
Bit Shifting and Bit Flags
We need to represent the four modifier keys as bit flags. I'm using TypeScript here, so I'm going to use an enum
.
I'm also using Electron and this application only runs on macOS, so I'm going to represent each modifier in the macOS parlance.
enum Mod {
Command = 1 << 1, // 2 in base-10
Control = 1 << 2, // 4 in base-10
Option = 1 << 3, // 8 in base-10
Shift = 1 << 4, // 16 in base-10
}
The <<
is the bitwise left shift operator.
From the MDN documentation:
The left shift operator (
<<
) shifts the first operand the specified number of bits to the left. Excess bits shifted off to the left are discarded. Zero bits are shifted in from the right.
So if we were to log out each of those modifiers in their base-2 representation, here's what we'd get:
console.log(
Mod.Command.toString(2), // "10"
Mod.Control.toString(2), // "100"
Mod.Option.toString(2), // "1000"
Mod.Shift.toString(2), // "10000"
);
- Note
-
Like
parseInt()
, JavaScript'stoString()
function can take an optional radix argument that converts the number to the specified base.
You can see that the number of 0
s directly corresponds to the number to the right of the <<
operator in the Mod
enum.
So what's the point of all this? Well, let's switch to base-10 for a minute. You need to check for multiple true or false conditions.
The nice thing about using bit flags is any combination of those Mod
values will never add up to the same number:
Mod.Command
+Mod.Control
=6
Mod.Command
+Mod.Option
=10
Mod.Command
+Mod.Shift
=18
Mod.Control
+Mod.Option
=12
Mod.Control
+Mod.Shift
=20
Mod.Control
+Mod.Option
+Mod.Shift
=28
The list goes on (just trust me). Each possible combination of those flags will never overlap with another combination.
For example, you'll never run into an issue where the value could be either (Mod.Command
+ Mod.Option
) or
(Mod.Option
+ Mod.Shift
).
- Note
-
There's actually an issue with the amount of bits we're shifting (i.e.
1
,2
,3
, and4
), but we'll cover that later.
Now that we've got our modifiers set up, let's cover how to use them with bitwise operators.
Bitwise Operators
JavaScript has four binary bitwise operators: AND (&
),
OR (|
), XOR (^
),
and NOT (~
).
I'm only concerned with the AND, OR, and NOT operators for my purposes, but XOR is there if you need it. Bitwise operators can be described by something called a truth table. If you don't feel like clicking through on that link, here's a quick rundown:
A truth table is a mathematical table used to carry out logical operations in Maths. It includes boolean algebra or boolean functions. It is primarily used to determine whether a compound statement is true or false based on the input values.
The truth table on the linked site is a bit difficult to grasp, so let's go through the truth table for each operator we're concerned with.
Assuming you have two bits a and b. The truth table for the AND bitwise operator looks like this:
a | b | a & b |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
If you swapped out 0
for false
, 1
for true, and &
for &&
, it would map to:
console.log(false && false); // false
console.log(false && true); // false
console.log(true && false); // false
console.log(true && true); // true
Here's the truth table for the OR bitwise operator:
a | b | a | b |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
If you swapped out 0
for false
, 1
for true, and |
for ||
, it would map to:
console.log(false || false); // false
console.log(false || true); // true
console.log(true || false); // true
console.log(true || true); // true
The truth table for the NOT operator is pretty cut and dried:
a | ~a |
---|---|
0 | 1 |
1 | 0 |
So we have a Mod
bit flag enum and some operators we can use to shimmy bits around.
Let's apply that to keyboard shortcuts.
Checking for a Modifier with Bits
For the time being, we're going to focus exclusively on modifiers and worry about non-modifier keys (e.g. letters, numbers, symbols) later.
Let's create a function called areKeysDown
with an event
and a combo
argument.
The event
is a KeyboardEvent
and the combo
is a number that corresponds to our bit flags.
As far as desired functionality goes, we want to check if a specific combination of keys are down.
It's important to note that the function returns true
if and only if those exact keys are down.
So areKeysDown(event, Mod.Command)
returns true
if the Command
key is down, but false
if Command
+ Control
is down.
Here's a snippet of what the function looks like:
function areKeysDown(event: KeyboardEvent, combo: number): boolean {
let keyCode = combo;
if ((combo & Mod.Command) === Mod.Command) {
if (!event.metaKey) {
return false;
} else {
keyCode = keyCode & ~Mod.Command;
}
} else {
if (event.metaKey) {
return false;
}
}
// ... rest of Mod handlers ...
// This will change when we add non-modifier keys:
return keyCode === 0;
}
- Note
-
On macOS,
event.metaKey
indicates theCommand
key (⌘
) is down,event.altKey
indicates theOption
key (⌥
) is down,event.ctrlKey
indicates theControl
key (⌃
) is down, andevent.shiftKey
indicates theShift
key is down.
And here's how you use that function:
document.addEventListener("keydown", (event: KeyboardEvent) => {
// Logs out true if event.metaKey = true, other modifiers are false,
// and no other keys are pressed:
console.log(areKeysDown(event, Mod.Command));
});
Let's talk this out. On the very first line I create a local variable, keyCode
which is assigned the value of combo
.
The idea is that we step through each modifier and clear the bit flag from the keyCode
until we get down to the bottom of the function.
After getting rid of all the Mod
flags, you're left with the keyCode
which represents the letter or number that was pressed.
So after I have my keyCode
variable, I perform this check:
if ((combo & Mod.Command) === Mod.Command) {
// ...
}
Hooray! We've encountered our first bitwise operator, the AND. So what is this line doing? Well, according to the MDN documentation for the AND operator:
The bitwise AND operator (
&
) returns a1
in each bit position for which the corresponding bits of both operands are1
s.
Let's use the example that MDN provides to explain this:
const a = 5; // 00000000000000000000000000000101
const b = 3; // 00000000000000000000000000000011
// Only bit that's a 1 in both values ^
console.log(a & b); // 00000000000000000000000000000001
// Which is why it only logs out 1 ^
And applying this to the areKeysDown
function:
const combo = Mod.Command; // 00000000000000000000000000000010
const compare = combo & Mod.Command; // 00000000000000000000000000000010
// This translates to:
// if 00000000000000000000000000000010
// == 00000000000000000000000000000010
// Which is true! So we know the Command modifier was pressed.
if (compare === Mod.Command) {
// ...
}
So if the Command
modifier is down, that condition is true
. Let's keep going:
if ((combo & Mod.Command) === Mod.Command) {
if (!event.metaKey) {
return false;
} else {
keyCode = keyCode & ~Mod.Command;
}
}
The if (!event.metaKey)
statement is pretty obvious. If we're checking for the Command
modifier, and it isn't pressed, the function returns false
.
The else
is where things get spicy. I'm using the AND plus the NOT operator to clear Mod.Command
from keyCode
.
Here's what MDN has to say about NOT:
The bitwise NOT operator (
~
) inverts the bits of its operand.
Dafuq does that mean? Well for a 32-bit integer, it turns all the 1
s to 0
s and 0
s to 1
s.
const combo = Mod.Command; // 00000000000000000000000000000010
const not = ~Mod.Command; // 11111111111111111111111111111101
// ^ Boop
But hang on a second, wouldn't that mean the value would be huge after we NOT it? You are correct, but don't forget that this is a two-step operation:
const combo = Mod.Command; // 00000000000000000000000000000010
const not = ~Mod.Command; // 11111111111111111111111111111101
console.log((combo & not).toString(2)); // 00000000000000000000000000000000
// ^ Don't forget about the AND!
Remember the definition for bitwise AND?:
The bitwise AND operator (
&
) returns a1
in each bit position for which the corresponding bits of both operands are1
s.
There are no positions in the combo
and not
variables in which both bits are 1
, so the output of that is 0
.
The final else
statement enforces the requirement that the function return true
if and only if the exact combo specified is pressed.
If you're checking if Mod.Control
is down and the user is pressing Control
+ Command
, the function returns false
.
function areKeysDown(event: KeyboardEvent, combo: number): boolean {
let keyCode = combo;
if ((combo & Mod.Command) === Mod.Command) {
// ...
} else {
// The Command key is depressed, but we're explicitly checking
// if it is _not_ down, so we return false:
if (event.metaKey) {
return false;
}
}
// ... rest of Mod handlers ...
// This will change when we add non-modifier keys:
return keyCode === 0;
}
So this is all well and good, but not very useful. How do you check for multiple modifiers? I'm glad you asked!
Handling Multiple Modifiers
Let's add a second modifier if
statement to our areKeysDown
function that checks for Control
:
function areKeysDown(event: KeyboardEvent, combo: number): boolean {
let keyCode = combo;
if ((combo & Mod.Command) === Mod.Command) {
if (!event.metaKey) {
return false;
} else {
keyCode = keyCode & ~Mod.Command;
}
} else {
if (event.metaKey) {
return false;
}
}
if ((combo & Mod.Control) === Mod.Control) {
if (!event.ctrlKey) {
return false;
} else {
keyCode = keyCode & ~Mod.Control;
}
} else {
if (event.ctrlKey) {
return false;
}
}
// ... rest of Mod handlers ...
// This will change when we add non-modifier keys:
return keyCode === 0;
}
The only difference from the Mod.Command
statement is we're checking for Mod.Control
and event.ctrlKey
.
In order to check for multiple modifiers, we're going to bring out the final bitwise operator: OR. So our function call looks like this:
document.addEventListener("keydown", (event: KeyboardEvent) => {
// Logs out true if event.metaKey = true, event.ctrlKey = true,
// other modifiers are false, and no other keys are pressed:
console.log(areKeysDown(event, Mod.Command | Mod.Control));
});
Here's the definition of OR from the MDN documentation:
The bitwise OR operator (
|
) returns a1
in each bit position for which the corresponding bits of either or both operands are1
s.
Let's use the example that MDN provides to explain this:
const a = 5; // 00000000000000000000000000000101
const b = 3; // 00000000000000000000000000000011
// These are either 1 or 0 ^^^
console.log(a | b); // 00000000000000000000000000000111
// So it turns them all into 1s ^^^
And applying this to the areKeysDown
function:
document.addEventListener("keydown", (event: KeyboardEvent) => {
// Logs out true if event.metaKey = true, event.ctrlKey = true,
// other modifiers are false, and no other keys are pressed:
console.log(areKeysDown(event, Mod.Command | Mod.Control));
});
function areKeysDown(event: KeyboardEvent, combo: number): boolean {
// Mod.Command = 00000000000000000000000000000010
// Mod.Control = 00000000000000000000000000000100
// combo = 00000000000000000000000000000110
// Set bits with a 1 in either number to 1 ^^
if ((combo & Mod.Command) === Mod.Command) {
// Mod.Command = 00000000000000000000000000000010
// combo = 00000000000000000000000000000110
// combo & Mod.Command = 00000000000000000000000000000010
// Only position where Mod.Command _and_ combo is 1 ^
// Does 00000000000000000000000000000010
// == 00000000000000000000000000000010?
// Yep! So we know that Mod.Command is in the combo argument
}
}
So what about that keyCode
variable? Well, here's how that works:
document.addEventListener("keydown", (event: KeyboardEvent) => {
console.log(areKeysDown(event, Mod.Command | Mod.Control));
});
function areKeysDown(event: KeyboardEvent, combo: number): boolean {
// Mod.Command = 00000000000000000000000000000010
// Mod.Control = 00000000000000000000000000000100
// combo = 00000000000000000000000000000110
// Set bits with a 1 in either number to 1 ^^
let keyCode = combo;
if ((combo & Mod.Command) === Mod.Command) {
// See previous example (we know it's true)...
if (!event.metaKey) {
return false;
} else {
// ~Mod.Command = 11111111111111111111111111111101
// keyCode is still combo which = 00000000000000000000000000000110
keyCode = keyCode & ~Mod.Command; // 00000000000000000000000000000100
// Only bit position in both numbers with value of 1 ^
}
} else {
// ...
}
if ((combo & Mod.Control) === Mod.Control) {
// Mod.Control = 00000000000000000000000000000100
// combo = 00000000000000000000000000000110
// combo & Mod.Control = 00000000000000000000000000000100
// Only position where command _and_ combo is 1 ^
// Does 00000000000000000000000000000100
// == 00000000000000000000000000000100?
// Yep! So we know that Mod.Control is in the combo argument
if (!event.ctrlKey) {
return false;
} else {
// ~Mod.Control = 11111111111111111111111111111011
// keyCode = combo w/o Mod.Command = 00000000000000000000000000000100
keyCode = keyCode & ~Mod.Control; // 00000000000000000000000000000000
}
}
// The keyCode is now 0, so we return true!
return keyCode === 0;
}
So that's how we handle multiple modifiers. But that still doesn't get us all the functionality we need. What about letters, numbers, arrow keys, etc.? Let's cover that next.
Checking for Non-Modifier Keys
This is where things start to get a little hairy.
The KeyboardEvent
has a keyCode
property that has been deprecated for a while and is no longer recommended.
Per the MDN documentation on keyCode
:
The deprecated
KeyboardEvent.keyCode
read-only property represents a system and implementation dependent numerical code identifying the unmodified value of the pressed key.
It's handy for bit flags because it's a number, so the letter A
has a keyCode
of 65
.
You could check if Command
+ A
is pressed by doing this:
document.addEventListener("keydown", (event: KeyboardEvent) => {
// Logs out 65 if "A" is pressed:
console.log(event.keyCode);
// Logs out true if event.metaKey = true and "A" is pressed
// (and only if those keys are pressed):
console.log(areKeysDown(event, Mod.Command | 65));
});
I imagine the primary reason for the deprecation was due to issues with international keyboard layouts.
MDN recommends you use KeyboardEvent.code
instead, which is fine, but it just requires a little extra work.
I created an enum
with each non-modifier key to make the areKeysDown
function more readable.
If the keyCode
property wasn't deprecated, that would look something like this:
enum Key {
LetterA = 65,
LetterB = 66,
LetterC = 67,
LetterD = 68,
// ... and so on ...
}
That would allow us to do the following:
document.addEventListener("keydown", (event: KeyboardEvent) => {
console.log(areKeysDown(event, Mod.Command | Key.LetterA));
});
function areKeysDown(event: KeyboardEvent, combo: number): boolean {
let keyCode = combo;
if ((combo & Mod.Command) === Mod.Command) {
// ...
keyCode = keyCode & ~Mod.Command; // keyCode = 65 = Key.LetterA
// ...
}
// event.keyCode = 65 = Key.LetterA = keyCode, so we return true:
return event.keyCode === keyCode;
}
But since keyCode
is deprecated, let's do this instead:
enum Key {
LetterA = 1,
LetterB,
LetterC,
LetterD,
// ... and so on ...
}
I started at 1
instead of 0
, because 0
would be the result of clearing all the modifiers.
There would be no way to differentiate between "only modifiers are pressed" and "some modifiers plus the letter A
was pressed".
Now we just need a table to map the Key
enum to the corresponding KeyboardEvent.code
values:
const codeByKeyTable: Record<Key, string> = {
[Key.LetterA]: "KeyA",
[Key.LetterB]: "KeyB",
[Key.LetterC]: "KeyC",
[Key.LetterD]: "KeyD",
// ... and so on ...
}
That "KeyA"
, "KeyB"
, etc. is the value of KeyboardEvent.code
for each key:
document.addEventListener("keydown", (event: KeyboardEvent) => {
// Logs out "KeyA" when you press the letter "A":
console.log(event.code);
});
- Note
-
If you're wondering why I used
KeyboardEvent.code
instead ofKeyboardEvent.key
, it's becauseKeyboardEvent.code
returns a value that isn't altered by keyboard layout or the state of the modifier keys.KeyboardEvent.key
returns a different value depending on the state of the modifier keys, which would pretty much break all our code.
Let's tweak the previous example to accommodate for the codeByKeyTable
lookup table:
document.addEventListener("keydown", (event: KeyboardEvent) => {
console.log(areKeysDown(event, Mod.Command | Key.LetterA));
});
function areKeysDown(event: KeyboardEvent, combo: number): boolean {
let keyCode = combo;
if ((combo & Mod.Command) === Mod.Command) {
// ...
keyCode = keyCode & ~Mod.Command;
// ...
}
// keyCode = 1, which is "KeyA" in the lookup table:
const codeForKeyCode = codeByKeyTable[keyCode];
// event.code = "KeyA", codeForKeyCode = "KeyA", so return true:
return event.code === codeForKeyCode;
}
There is, however, a problem that I alluded to earlier in this post.
The function will return the wrong value given the current Mod
enum values because some of the Key
enum values are the same:
enum Mod {
Command = 1 << 1, // 2 === Key.LetterB
Control = 1 << 2, // 4 === Key.LetterD
Option = 1 << 3, // 8 === Key.LetterH
Shift = 1 << 4, // 16 === Key.LetterP
}
So you lose the whole "impossible collision" advantage of bit flags. Fortunately, the fix is super simple.
Just increase the number of bits shifted so the base-10 equivalent exceeds the maximum value of the Key
enum.
You also need to make sure the gap between the Mod
enum values is wide enough to accommodate the amount of entries in the Key
enum.
If we start with 4096
in base-10 (1 << 12
), that should cover our bases:
enum Mod {
Command = 1 << 12, // 4096 in base-10
Control = 1 << 13, // 8192 in base-10
Option = 1 << 14, // 16384 in base-10
Shift = 1 << 15, // 32768 in base-10
}
That's a big enough starting point and a more than big enough gap to avoid any collisions. If you were to OR all those together, you end up with:
console.log(
Mod.Command | Mod.Control | Mod.Option | Mod.Shift
);
// base-2 : 00000000000000001111000000000000
// base-10 : 61440
Which is still plenty of wiggle room from the maximum 32-bit integer value.
I'm not quite done yet, though. I added a few extra features to really give this library the old razzle-dazzle. Strap in!
The Old Razzle Dazzle
There are circumstances in which I need to check for multiple possible key combinations because I want them to map to the same operation.
So I did a little refactoring. I renamed areKeysDown
to isComboDown
and changed the areKeysDown
function to the following:
function areKeysDown(event: KeyboardEvent, ...combos: number[]): boolean {
for (const combo of combos) {
if (isComboDown(event, combo)) {
return true;
}
}
return false;
}
function isComboDown(event: KeyboardEvent, combo: number): boolean {
// Same implementation as the areKeysDown function from prior examples.
}
I'm treating the combos
as an "or" condition, which is why I'm returning true
as soon as one of the combos
is hit.
I'm using JavaScript's spread syntax, so I can pass a variable amount of key combinations in to the function. Now you can call the function like this:
// Still works as expected the old way:
areKeysDown(event, Mod.Shift | Key.LetterB);
// I can also pass as many additional key combinations as I want:
areKeysDown(
event,
Mod.Shift | Key.LetterB,
Mod.Shift | Key.LetterC,
Mod.Shift | Key.LetterD,
);
I know what some of you might be thinking:
Why not just make it an array?
Because 80% of the time, I'm only checking for a single key combination.
Why not just make it an array or a single value?
That's a bunch of extra work for zero additional clarity.
I don't agree.
Well, go start your own blog.
But I digress. I added another handy little tidbit to this library.
In most cases, you want to handle several keyboard shortcuts in a single event listener.
We can leverage JavaScript's square bracket notation to really gussy things up.
I created a listenForKeys
function that lets you define an object with the key combination OR expression as the key and handler function as the value:
export function listenForKeys(
event: KeyboardEvent,
handlers: Record<number, () => void>,
): void {
for (const [combo, func] of Object.entries(handlers)) {
// The "+" in front of "combo" converts the value to a number.
// If we didn't do this, TypeScript would complain that combo is a string:
if (areKeysDown(event, +combo)) {
func();
}
}
}
In use, that looks like this:
document.addEventListener("keydown", (event: KeyboardEvent) => {
listenForKeys(event, {
[Mod.Command | Key.LetterA]() {
// Do something when Command + A is pressed.
},
[Mod.Command | Key.LetterB]() {
// Do something when Command + B is pressed.
},
[Mod.Control | Mod.Shift | Key.LetterC]() {
// Do something when Control + Shift + C is pressed.
},
});
});
If that looks super weird to you, after you evaluate the |
and convert it to
the decimal representation, it ends up looking like this:
document.addEventListener("keydown", (event: KeyboardEvent) => {
listenForKeys(event, {
4097() {
// Do something when Command + A is pressed.
},
4098() {
// Do something when Command + B is pressed.
},
16842755() {
// Do something when Control + Shift + C is pressed.
}
});
});
Pretty cool, eh? Maybe not, but it's better than a slap in the chest with a wet fish.
Whelp, that's all I've got. Thanks for following along on my journey to the weird and wacky world of bits. Hopefully you learned a bit (heh) and have a better understanding of binary numbers, bit flags, bit shifting, and bitwise operators. I also hope I didn't put Matt to sleep. Sayonara!