Skip to content

Commit

Permalink
Add keyboard shortcuts to the GUI (redballoonsecurity#210)
Browse files Browse the repository at this point in the history
  • Loading branch information
rbs-jacob authored and marczalik committed Feb 14, 2023
1 parent 6c5f045 commit f47721d
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 44 deletions.
17 changes: 17 additions & 0 deletions docs/user-guide/key-concepts/gui/keybindings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Keybindings

The GUI supports a number of keybindings for reverse engineering at the speed
of thought.

- <kbd>i</kbd> – identify the current resource
- <kbd>u</kbd> – unpack the current resource
- <kbd>a</kbd> – analyze the current resource
- <kbd>p</kbd> – pack the current resource
- <kbd>n</kbd> – new resource
- <kbd>Shift</kbd>+<kbd>u</kbd> – recursively unpack the current resource
- <kbd>Shift</kbd>+<kbd>p</kbd> – recursively pack the current resource
- <kbd>g</kbd> – focus on the "go to offset" input
- <kbd>j</kbd> OR <kbd>↓</kbd> – focus one resource down in the tree
- <kbd>k</kbd> OR <kbd>↑</kbd> – focus one resource up in the tree
- <kbd>h</kbd> OR <kbd>←</kbd> – collapse the current resource tree node
- <kbd>l</kbd> OR <kbd>→</kbd> – expand the current resource tree node
23 changes: 21 additions & 2 deletions frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import CarouselSelector from "./CarouselSelector.svelte";
import EntropyView from "./EntropyView.svelte";
import HexView from "./HexView.svelte";
import JumpToOffset from "./JumpToOffset.svelte";
import LoadingAnimation from "./LoadingAnimation.svelte";
import MagnitudeView from "./MagnitudeView.svelte";
import Pane from "./Pane.svelte";
Expand All @@ -46,9 +47,9 @@
import { printConsoleArt } from "./console-art.js";
import { selected, selectedResource } from "./stores.js";
import { keyEventToString, shortcuts } from "./keyboard.js";
import { writable } from "svelte/store";
import JumpToOffset from "./JumpToOffset.svelte";
printConsoleArt();
Expand Down Expand Up @@ -100,6 +101,24 @@
}
}
function handleShortcut(e) {
// Don't handle keypresses from within text inputs.
if (
["input", "textarea"].includes(e.target?.tagName.toLocaleLowerCase()) ||
e.target.isContentEditable
) {
return;
}
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const keyString = keyEventToString(e);
const callback = shortcuts[keyString];
if (callback) {
callback();
}
}
window.riddle = {
ask: () => {
console.log(`Answer the following riddle for a special Easter egg surprise:
Expand All @@ -125,7 +144,7 @@ Answer by running riddle.answer('your answer here') from the console.`);
window.riddle.ask();
</script>
<svelte:window on:popstate="{backButton}" />
<svelte:window on:popstate="{backButton}" on:keyup="{handleShortcut}" />
{#if showRootResource}
{#await rootResourceLoadPromise}
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/JumpToOffset.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<script>
import { calculator } from "./helpers";
import { onMount, tick } from "svelte";
import { shortcuts } from "./keyboard";
export let dataPromise, scrollY;
let startOffset,
Expand All @@ -33,6 +34,12 @@
mounted = true;
});
$: shortcuts["g"] = () => {
if (input) {
input.focus();
}
};
$: if (mounted) {
startOffset = Math.max(
Math.ceil((dataLength * $scrollY.top) / alignment) * alignment,
Expand Down
92 changes: 69 additions & 23 deletions frontend/src/ResourceTreeNode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -100,61 +100,94 @@
import LoadingText from "./LoadingText.svelte";
import { selected } from "./stores.js";
import { shortcuts } from "./keyboard";
export let rootResource,
resourceNodeDataMap,
collapsed = true;
selectNextSibling = () => {},
selectPreviousSibling = () => {},
collapsed = true,
childrenCollapsed = true;
let self,
firstChild,
childrenPromise,
commentsPromise,
childrenCollapsed = false,
self_id = rootResource.get_id(),
kiddoChunksize = 512;
$: {
if (resourceNodeDataMap[self?.id] === undefined) {
resourceNodeDataMap[self?.id] = {};
if (resourceNodeDataMap[self_id] === undefined) {
resourceNodeDataMap[self_id] = {};
}
if (resourceNodeDataMap[self?.id].collapsed === undefined) {
resourceNodeDataMap[self?.id].collapsed = collapsed;
if (resourceNodeDataMap[self_id].collapsed === undefined) {
resourceNodeDataMap[self_id].collapsed = collapsed;
}
if (resourceNodeDataMap[self?.id].childrenPromise === undefined) {
resourceNodeDataMap[self?.id].childrenPromise =
if (resourceNodeDataMap[self_id].childrenPromise === undefined) {
resourceNodeDataMap[self_id].childrenPromise =
rootResource.get_children();
}
if (resourceNodeDataMap[self?.id].commentsPromise === undefined) {
resourceNodeDataMap[self?.id].commentsPromise =
if (resourceNodeDataMap[self_id].commentsPromise === undefined) {
resourceNodeDataMap[self_id].commentsPromise =
rootResource.get_comments();
}
childrenPromise = resourceNodeDataMap[self?.id].childrenPromise;
commentsPromise = resourceNodeDataMap[self?.id].commentsPromise;
collapsed = resourceNodeDataMap[self?.id].collapsed;
childrenPromise = resourceNodeDataMap[self_id].childrenPromise;
commentsPromise = resourceNodeDataMap[self_id].commentsPromise;
collapsed = resourceNodeDataMap[self_id].collapsed;
}
$: childrenPromise?.then((children) => {
if (children?.length > 0) {
firstChild = children[0];
}
});
$: if ($selected === self_id) {
shortcuts["h"] = () => {
resourceNodeDataMap[self_id].collapsed = true;
};
shortcuts["l"] = () => {
resourceNodeDataMap[self_id].collapsed = false;
};
shortcuts["j"] = () => {
if (!collapsed && firstChild) {
$selected = firstChild?.resource_id;
} else {
selectNextSibling();
}
};
shortcuts["k"] = selectPreviousSibling;
shortcuts["arrowleft"] = shortcuts["h"];
shortcuts["arrowdown"] = shortcuts["j"];
shortcuts["arrowup"] = shortcuts["k"];
shortcuts["arrowright"] = shortcuts["l"];
}
async function onClick(e) {
// https://stackoverflow.com/a/53939059
if (e.detail > 1) {
return;
}
if ($selected === self?.id) {
if ($selected === self_id) {
$selected = undefined;
} else {
$selected = self?.id;
$selected = self_id;
}
}
function onDoubleClick(e) {
resourceNodeDataMap[self?.id].collapsed = !collapsed;
resourceNodeDataMap[self_id].collapsed = !collapsed;
// Expand children recursively on double click
if (!collapsed) {
childrenCollapsed = false;
}
$selected = self?.id;
$selected = self_id;
}
async function onDeleteClick(optional_range) {
// Delete the selected comment.
// As a side effect, the corresponding resource gets selected.
$selected = self?.id;
$selected = self_id;
await rootResource.delete_comment(optional_range);
resourceNodeDataMap[$selected].commentsPromise =
rootResource.get_comments();
Expand All @@ -165,8 +198,7 @@
{#if children?.length > 0}
<button
on:click="{() => {
resourceNodeDataMap[self?.id].collapsed = !collapsed;
childrenCollapsed = !collapsed;
resourceNodeDataMap[self_id].collapsed = !collapsed;
}}"
>
{#if collapsed}
Expand All @@ -179,8 +211,8 @@
on:click="{onClick}"
on:dblclick="{onDoubleClick}"
bind:this="{self}"
class:selected="{$selected === self?.id}"
id="{rootResource.get_id()}"
class:selected="{$selected === self_id}"
id="{self_id}"
>
{rootResource.get_caption()}
</button>
Expand All @@ -207,12 +239,26 @@
{:then children}
{#if !collapsed && children.length > 0}
<ul>
{#each children.slice(0, kiddoChunksize) as child}
{#each children.slice(0, kiddoChunksize) as child, i}
<li>
<div>
<svelte:self
rootResource="{child}"
collapsed="{childrenCollapsed}"
childrenCollapsed="{childrenCollapsed}"
selectNextSibling="{i ===
Math.min(kiddoChunksize, children.length) - 1
? selectNextSibling
: () => {
$selected = children[i + 1]?.resource_id;
}}"
selectPreviousSibling="{i === 0
? () => {
$selected = self_id;
}
: () => {
$selected = children[i - 1]?.resource_id;
}}"
bind:resourceNodeDataMap="{resourceNodeDataMap}"
/>
</div>
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/ResourceTreeToolbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
{
text: "Identify",
iconUrl: "/icons/identify.svg",
shortcut: "i",
onclick: async (e) => {
await rootResource.identify();
resourceNodeDataMap[$selected] = {
Expand All @@ -37,6 +38,7 @@
{
text: "Unpack",
iconUrl: "/icons/unpack.svg",
shortcut: "u",
onclick: async (e) => {
await rootResource.unpack();
resourceNodeDataMap[$selected] = {
Expand All @@ -58,6 +60,7 @@
{
text: "Analyze",
iconUrl: "/icons/analyze.svg",
shortcut: "a",
onclick: async (e) => {
await rootResource.analyze();
resourceNodeDataMap[$selected] = {
Expand All @@ -79,6 +82,7 @@
{
text: "Pack",
iconUrl: "/icons/pack.svg",
shortcut: "p",
onclick: async (e) => {
await rootResource.pack();
resourceNodeDataMap[$selected] = {
Expand Down Expand Up @@ -149,6 +153,7 @@
{
text: "New",
iconUrl: "/icons/new.svg",
shortcut: "n",
onclick: (e) => {
// Clear the URL fragment
window.location.replace("/");
Expand All @@ -158,6 +163,7 @@
{
text: "Unpack Recursively",
iconUrl: "/icons/unpack_r.svg",
shortcut: "u+Shift",
onclick: async (e) => {
await rootResource.unpack_recursively();
resourceNodeDataMap[$selected] = {
Expand All @@ -171,6 +177,7 @@
{
text: "Pack Recursively",
iconUrl: "/icons/pack_r.svg",
shortcut: "p+Shift",
onclick: async (e) => {
await rootResource.pack_recursively();
resourceNodeDataMap[$selected] = {
Expand Down
63 changes: 44 additions & 19 deletions frontend/src/Toolbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -38,31 +38,56 @@
<script>
import Icon from "./Icon.svelte";
import { shortcuts } from "./keyboard.js";
export let toolbarButtons;
/***
* Show the loading spinner while an async onclick function does its thing.
*/
function wrapOnCick(button) {
return async (e) => {
const oldIcon = button.iconUrl;
button.iconUrl = "/icons/loading.svg";
toolbarButtons = toolbarButtons;
await button
.onclick(e)
.then((_) => {
button.iconUrl = oldIcon;
toolbarButtons = toolbarButtons;
})
.catch((e) => {
button.iconUrl = "/icons/error.svg";
toolbarButtons = toolbarButtons;
try {
let errorObject = JSON.parse(e.message);
alert(`${errorObject.type}: ${errorObject.message}`);
} catch {
alert(e);
}
console.error(e);
});
};
}
$: Array.from(toolbarButtons).forEach((button) => {
if (!button.shortcut) {
return;
}
shortcuts[button.shortcut] = wrapOnCick(button);
});
</script>

<div class="vbox">
{#each toolbarButtons as button}
<button
on:click="{async (e) => {
const oldIcon = button.iconUrl;
button.iconUrl = '/icons/loading.svg';
await button
.onclick(e)
.then((_) => {
button.iconUrl = oldIcon;
})
.catch((e) => {
button.iconUrl = '/icons/error.svg';
try {
let errorObject = JSON.parse(e.message);
alert(`${errorObject.type}: ${errorObject.message}`);
} catch {
alert(e);
}
console.error(e);
});
}}"
on:click="{wrapOnCick(button)}"
title="{button.text +
(button.shortcut
? ' (Shortcut key: ' +
button.shortcut.split('+').reverse().join(' + ') +
')'
: '')}"
>
{#if button.iconUrl}
<Icon url="{button.iconUrl}" />
Expand Down
Loading

0 comments on commit f47721d

Please sign in to comment.