Skip to content

Commit a455748

Browse files
authored
Add keyboard shortcuts for various actions (#21)
* don't unselect monitor on second click * Add keyboard shortcuts * add keyboard shortcut modal
1 parent 5b959a7 commit a455748

File tree

6 files changed

+262
-6
lines changed

6 files changed

+262
-6
lines changed

Diff for: src/app/components/Content/index.tsx

+51-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward'
44
import ClearAllIcon from '@material-ui/icons/ClearAll'
55
import Link from '../Link'
66

7-
import Store from '../../Store'
7+
import Store, { RUNNING } from '../../Store'
88
import './index.css'
99

1010
export interface IProps {
@@ -15,6 +15,54 @@ export interface IProps {
1515
class Content extends React.Component<IProps, {}> {
1616
private el: HTMLDivElement | null = null
1717
private atBottom: boolean = true
18+
private keydownListener: ((event: KeyboardEvent) => void) | null = null
19+
20+
public getKeydownListener(store: Store) {
21+
if (this.keydownListener == null) {
22+
this.keydownListener = (event: KeyboardEvent) => {
23+
const selectedMonitor = store.monitors.get(store.selectedMonitorId);
24+
const isSelectedMonitorRunning = selectedMonitor?.status === RUNNING
25+
// shift + alt to avoid system shortcuts
26+
if (event.shiftKey && event.altKey) {
27+
switch (event.code) {
28+
case 'KeyK':
29+
store.clearOutput(store.selectedMonitorId)
30+
break
31+
case 'KeyS':
32+
this.scrollToBottom()
33+
break
34+
case 'Comma':
35+
if (!isSelectedMonitorRunning) {
36+
this.scrollToBottom()
37+
}
38+
store.toggleMonitor(store.selectedMonitorId)
39+
break
40+
case 'Period':
41+
if (!isSelectedMonitorRunning) {
42+
store.clearOutput(store.selectedMonitorId)
43+
}
44+
store.toggleMonitor(store.selectedMonitorId)
45+
break
46+
}
47+
}
48+
}
49+
}
50+
return this.keydownListener
51+
}
52+
53+
public componentDidMount() {
54+
document.addEventListener(
55+
'keydown',
56+
this.getKeydownListener(this.props.store),
57+
)
58+
}
59+
60+
public componentWillUnmount() {
61+
document.removeEventListener(
62+
'keydown',
63+
this.getKeydownListener(this.props.store),
64+
)
65+
}
1866

1967
public componentWillUpdate() {
2068
if (this.el) {
@@ -64,13 +112,13 @@ class Content extends React.Component<IProps, {}> {
64112
</span>
65113
<span>
66114
<button
67-
title="Clear output"
115+
title="Clear output (shift + alt + K)"
68116
onClick={() => store.clearOutput(store.selectedMonitorId)}
69117
>
70118
<ClearAllIcon />
71119
</button>
72120
<button
73-
title="Scroll to bottom"
121+
title="Scroll to bottom (shift + alt + S)"
74122
onClick={() => this.scrollToBottom()}
75123
>
76124
<ArrowDownwardIcon />

Diff for: src/app/components/KeyboardShortcutsModal/index.css

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
.modal-bg {
2+
position: fixed;
3+
top: 0;
4+
right: 0;
5+
bottom: 0;
6+
left: 0;
7+
display: flex;
8+
align-items: center;
9+
justify-content: center;
10+
cursor: pointer;
11+
background-color: rgba(0, 0, 0, 0.25);
12+
}
13+
14+
.modal {
15+
padding: 1rem;
16+
color: black;
17+
cursor: initial;
18+
background-color: white;
19+
border-radius: 0.5rem;
20+
}
21+
22+
.modal .header {
23+
display: flex;
24+
align-items: center;
25+
justify-content: space-between;
26+
margin-bottom: 1rem;
27+
}
28+
29+
.modal h3 {
30+
margin: 0;
31+
}
32+
33+
.modal ul {
34+
justify-content: flex-start;
35+
min-width: 40rem;
36+
margin-bottom: 0;
37+
}
38+
39+
.modal ul li {
40+
height: unset;
41+
margin-bottom: 8px;
42+
}
43+
44+
.modal button {
45+
padding: 8px 12px;
46+
font-family: "Roboto", sans-serif;
47+
color: black;
48+
}
49+
50+
.modal button:hover {
51+
background-color: #bfbfbf;
52+
}

Diff for: src/app/components/KeyboardShortcutsModal/index.tsx

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as React from "react";
2+
import "./index.css";
3+
4+
function makeShortcutText(key: string): string {
5+
return `alt + shift + ${key}:`;
6+
}
7+
8+
type Props = {
9+
visible: boolean;
10+
hide: () => void;
11+
};
12+
13+
export default function KeyboardShortcutsModal({ visible, hide }: Props) {
14+
return visible ? (
15+
<div className="modal-bg" onClick={hide}>
16+
<div className="modal" onClick={(e) => e.stopPropagation()}>
17+
<div className="header">
18+
<h3>Keyboard Shortcuts</h3>
19+
<button onClick={hide}>Done</button>
20+
</div>
21+
<ul>
22+
<li>
23+
<b>{makeShortcutText("up")}</b> Select previous monitor
24+
</li>
25+
<li>
26+
<b>{makeShortcutText("down")}</b> Select next monitor
27+
</li>
28+
<li>
29+
<b>{makeShortcutText("comma")}</b> Toggle current monitor, scroll to
30+
bottom
31+
</li>
32+
<li>
33+
<b>{makeShortcutText("period")}</b> Toggle current monitor, clear
34+
output
35+
</li>
36+
<li>
37+
<b>{makeShortcutText("k")}</b> Clear current output
38+
</li>
39+
<li>
40+
<b>{makeShortcutText("s")}</b> Scroll to bottom of current output
41+
</li>
42+
<li>
43+
<b>{makeShortcutText("?")}</b> Show keyboard shortcuts
44+
</li>
45+
</ul>
46+
</div>
47+
</div>
48+
) : null;
49+
}

Diff for: src/app/components/Nav/index.css

+17
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,30 @@
66
}
77

88
header {
9+
display: flex;
10+
align-items: center;
11+
justify-content: space-between;
912
padding: 1rem;
1013
font-size: 1rem;
1114
line-height: 1rem;
1215
text-transform: capitalize;
1316
border-bottom: 1px solid var(--primary);
1417
}
1518

19+
.keyboard-shortcuts-button {
20+
margin-top: -1rem;
21+
margin-right: -1rem;
22+
margin-bottom: -1rem;
23+
}
24+
25+
.keyboard-shortcuts-button.active {
26+
background-color: var(--primary-light);
27+
}
28+
29+
.keyboard-shortcuts-button.active:hover {
30+
background-color: var(--primary);
31+
}
32+
1633
.menu {
1734
flex: 1;
1835
overflow-y: scroll;

Diff for: src/app/components/Nav/index.tsx

+92-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import * as classNames from "classnames";
22
import { observer } from "mobx-react";
3+
import KeyboardIcon from "@material-ui/icons/Keyboard";
34
import * as React from "react";
45
import Store, { RUNNING } from "../../Store";
56
import Link from "../Link";
67
import Switch from "../Switch";
78
import "./index.css";
9+
import KeyboardShortcutsModal from "../KeyboardShortcutsModal";
810

911
const examples = `~/app$ chalet add 'cmd'
1012
~/app$ chalet add 'cmd -p $PORT'
@@ -16,9 +18,93 @@ export interface IProps {
1618

1719
function Nav({ store }: IProps) {
1820
const { isLoading, selectedMonitorId, monitors, proxies } = store;
21+
22+
const [
23+
showKeyboardShortcutsModal,
24+
setShowKeyboardShortcutsModal,
25+
] = React.useState(false);
26+
27+
React.useEffect(() => {
28+
function getCurrentMonitorState() {
29+
const monitorsArray = Array.from(store.monitors);
30+
const currentIndex = monitorsArray.findIndex(([id, monitor]) => {
31+
return store.selectedMonitorId === id;
32+
});
33+
34+
if (currentIndex === -1) {
35+
return {
36+
current: null,
37+
next: monitorsArray[0],
38+
prev: monitorsArray[monitorsArray.length - 1],
39+
};
40+
}
41+
42+
return {
43+
current: monitorsArray[currentIndex],
44+
next:
45+
currentIndex < monitorsArray.length - 1
46+
? monitorsArray[currentIndex + 1]
47+
: null,
48+
prev: currentIndex > 0 ? monitorsArray[currentIndex - 1] : null,
49+
};
50+
}
51+
52+
function selectPrevMonitor() {
53+
const { prev } = getCurrentMonitorState();
54+
if (prev != null) {
55+
store.selectMonitor(prev[0]);
56+
}
57+
}
58+
59+
function selectNextMonitor() {
60+
const { next } = getCurrentMonitorState();
61+
if (next != null) {
62+
store.selectMonitor(next[0]);
63+
}
64+
}
65+
66+
const listener = (event: KeyboardEvent) => {
67+
// shift + alt to avoid system shortcuts
68+
if (event.shiftKey && event.altKey) {
69+
switch (event.code) {
70+
case "BracketLeft":
71+
case "ArrowUp":
72+
selectPrevMonitor();
73+
break;
74+
case "BracketRight":
75+
case "ArrowDown":
76+
selectNextMonitor();
77+
break;
78+
case "Slash":
79+
setShowKeyboardShortcutsModal((old) => !old);
80+
break;
81+
}
82+
}
83+
84+
if (event.code === "Escape" && showKeyboardShortcutsModal) {
85+
setShowKeyboardShortcutsModal(false);
86+
}
87+
};
88+
document.addEventListener("keydown", listener);
89+
return () => {
90+
document.addEventListener("keydown", listener);
91+
};
92+
}, [store]);
93+
1994
return (
2095
<div className="nav">
21-
<header>chalet</header>
96+
<header>
97+
<span>chalet</span>
98+
<button
99+
title="View keyboard shortcuts (shift + alt + ?)"
100+
onClick={() => setShowKeyboardShortcutsModal(true)}
101+
className={classNames("keyboard-shortcuts-button", {
102+
active: showKeyboardShortcutsModal,
103+
})}
104+
>
105+
<KeyboardIcon />
106+
</button>
107+
</header>
22108
<div className={classNames("menu", { hidden: isLoading })}>
23109
{monitors.size === 0 && proxies.size === 0 && (
24110
<div>
@@ -39,7 +125,7 @@ function Nav({ store }: IProps) {
39125
key={id}
40126
className={classNames("monitor", {
41127
running: monitor.status === RUNNING,
42-
selected: id === selectedMonitorId
128+
selected: id === selectedMonitorId,
43129
})}
44130
onClick={() => store.selectMonitor(id)}
45131
>
@@ -81,6 +167,10 @@ function Nav({ store }: IProps) {
81167
README
82168
</a>
83169
</footer>
170+
<KeyboardShortcutsModal
171+
visible={showKeyboardShortcutsModal}
172+
hide={() => setShowKeyboardShortcutsModal(false)}
173+
/>
84174
</div>
85175
);
86176
}

Diff for: src/app/components/Switch/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function Switch({ onClick = () => null, checked }: IProps) {
1717
}}
1818
>
1919
<input type="checkbox" checked={checked} />
20-
<span className="slider" />
20+
<span className="slider" title="Toggle monitor (shift + alt + ,)" />
2121
</label>
2222
)
2323
}

0 commit comments

Comments
 (0)