Skip to content

Commit 18b3f22

Browse files
committed
feat: Add disconnect and settings reset UI.
* Use a drop down menu from the device name to access disconnect and settings reset actions. * Add confirm dialog for destructive settings reset action.
1 parent e7a25c0 commit 18b3f22

File tree

9 files changed

+191
-21
lines changed

9 files changed

+191
-21
lines changed

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"@heroicons/react": "^2.1.4",
1717
"@tauri-apps/api": "^2.0.0-beta.16",
1818
"@tauri-apps/plugin-cli": "^2.0.0-beta.8",
19-
"@zmkfirmware/zmk-studio-ts-client": "^0.0.14",
19+
"@zmkfirmware/zmk-studio-ts-client": "^0.0.16",
2020
"emittery": "^1.0.3",
2121
"immer": "^10.1.1",
2222
"react": "^18.2.0",

src-tauri/src/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
use futures::lock::Mutex;
55

66
mod transport;
7-
use transport::commands::{transport_send_data, ActiveConnection};
7+
use transport::commands::{transport_close, transport_send_data, ActiveConnection};
88

99
use transport::gatt::{gatt_connect, gatt_list_devices};
1010
use transport::serial::{serial_connect, serial_list_devices};
@@ -17,6 +17,7 @@ fn main() {
1717
})
1818
.invoke_handler(tauri::generate_handler![
1919
transport_send_data,
20+
transport_close,
2021
gatt_list_devices,
2122
gatt_connect,
2223
serial_list_devices,

src-tauri/src/transport/commands.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,13 @@ pub async fn transport_send_data(
3838

3939
Ok(())
4040
}
41+
42+
#[command]
43+
pub async fn transport_close(
44+
req: Request<'_>,
45+
state: State<'_, ActiveConnection<'_>>,
46+
) -> Result<(), ()> {
47+
*state.conn.lock().await = None;
48+
49+
Ok(())
50+
}

src-tauri/src/transport/gatt.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ pub async fn gatt_connect(
5858
});
5959

6060
let ah2 = app_handle.clone();
61-
tauri::async_runtime::spawn(async move {
61+
let disconnect_handle = tauri::async_runtime::spawn(async move {
6262
// Need to keep adapter from being dropped while active/connected
6363
let a = adapter;
6464

@@ -75,7 +75,7 @@ pub async fn gatt_connect(
7575
println!("ERROR RAISING! {:?}", e);
7676
}
7777

78-
notify_handle.abort();
78+
*state.conn.lock().await = None;
7979
}
8080
}
8181
};
@@ -87,6 +87,9 @@ pub async fn gatt_connect(
8787
while let Some(data) = recv.next().await {
8888
c.write(&data).await.expect("Write uneventfully");
8989
}
90+
91+
disconnect_handle.abort();
92+
notify_handle.abort();
9093
});
9194

9295
Ok(true)

src/App.tsx

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,15 @@ const TRANSPORTS: TransportFactory[] = [
6666
].filter((t) => t !== undefined);
6767

6868
async function listen_for_notifications(
69-
notification_stream: ReadableStream<Notification>
69+
notification_stream: ReadableStream<Notification>,
70+
signal: AbortSignal
7071
): Promise<void> {
7172
let reader = notification_stream.getReader();
73+
const onAbort = () => {
74+
reader.cancel();
75+
reader.releaseLock();
76+
};
77+
signal.addEventListener("abort", onAbort, { once: true });
7278
do {
7379
let pub = usePub();
7480

@@ -104,20 +110,24 @@ async function listen_for_notifications(
104110

105111
pub(topic, eventData);
106112
} catch (e) {
113+
signal.removeEventListener("abort", onAbort);
107114
reader.releaseLock();
108115
throw e;
109116
}
110117
} while (true);
111118

119+
signal.removeEventListener("abort", onAbort);
112120
reader.releaseLock();
121+
notification_stream.cancel();
113122
}
114123

115124
async function connect(
116125
transport: RpcTransport,
117126
setConn: Dispatch<ConnectionState>,
118-
setConnectedDeviceName: Dispatch<string | undefined>
127+
setConnectedDeviceName: Dispatch<string | undefined>,
128+
signal: AbortSignal
119129
) {
120-
let conn = await create_rpc_connection(transport);
130+
let conn = await create_rpc_connection(transport, { signal });
121131

122132
let details = await Promise.race([
123133
call_rpc(conn, { core: { getDeviceInfo: true } })
@@ -135,7 +145,7 @@ async function connect(
135145
return;
136146
}
137147

138-
listen_for_notifications(conn.notification_readable)
148+
listen_for_notifications(conn.notification_readable, signal)
139149
.then(() => {
140150
setConnectedDeviceName(undefined);
141151
setConn({ conn: null });
@@ -157,6 +167,7 @@ function App() {
157167
const [doIt, undo, redo, canUndo, canRedo, reset] = useUndoRedo();
158168
const [showAbout, setShowAbout] = useState(false);
159169
const [showLicenseNotice, setShowLicenseNotice] = useState(false);
170+
const [connectionAbort, setConnectionAbort] = useState(new AbortController());
160171

161172
const [lockState, setLockState] = useState<LockState>(
162173
LockState.ZMK_STUDIO_CORE_LOCK_STATE_LOCKED
@@ -225,6 +236,49 @@ function App() {
225236
doDiscard();
226237
}, [conn]);
227238

239+
const resetSettings = useCallback(() => {
240+
async function doReset() {
241+
if (!conn.conn) {
242+
return;
243+
}
244+
245+
let resp = await call_rpc(conn.conn, {
246+
core: { resetSettings: true },
247+
});
248+
if (!resp.core?.resetSettings) {
249+
console.error("Failed to settings reset", resp);
250+
}
251+
252+
reset();
253+
setConn({ conn: conn.conn });
254+
}
255+
256+
doReset();
257+
}, [conn]);
258+
259+
const disconnect = useCallback(() => {
260+
async function doDisconnect() {
261+
if (!conn.conn) {
262+
return;
263+
}
264+
265+
await conn.conn.request_writable.close();
266+
connectionAbort.abort("User disconnected");
267+
setConnectionAbort(new AbortController());
268+
}
269+
270+
doDisconnect();
271+
}, [conn]);
272+
273+
const onConnect = useCallback(
274+
(t: RpcTransport) => {
275+
const ac = new AbortController();
276+
setConnectionAbort(ac);
277+
connect(t, setConn, setConnectedDeviceName, ac.signal);
278+
},
279+
[setConn, setConnectedDeviceName, setConnectedDeviceName]
280+
);
281+
228282
return (
229283
<ConnectionContext.Provider value={conn}>
230284
<LockStateContext.Provider value={lockState}>
@@ -233,9 +287,7 @@ function App() {
233287
<ConnectModal
234288
open={!conn.conn}
235289
transports={TRANSPORTS}
236-
onTransportCreated={(t) =>
237-
connect(t, setConn, setConnectedDeviceName)
238-
}
290+
onTransportCreated={onConnect}
239291
/>
240292
<AboutModal open={showAbout} onClose={() => setShowAbout(false)} />
241293
<LicenseNoticeModal
@@ -251,6 +303,8 @@ function App() {
251303
onRedo={redo}
252304
onSave={save}
253305
onDiscard={discard}
306+
onDisconnect={disconnect}
307+
onResetSettings={resetSettings}
254308
/>
255309
<Keyboard />
256310
<AppFooter

src/AppHeader.tsx

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
1+
import {
2+
Button,
3+
Menu,
4+
MenuItem,
5+
MenuTrigger,
6+
Popover,
7+
} from "react-aria-components";
18
import { useConnectedDeviceData } from "./rpc/useConnectedDeviceData";
29
import { useSub } from "./usePubSub";
310
import {
411
ArrowUturnLeftIcon,
512
ArrowUturnRightIcon,
613
} from "@heroicons/react/24/solid";
14+
import { useContext, useEffect, useState } from "react";
15+
import { useModalRef } from "./misc/useModalRef";
16+
import { LockStateContext } from "./rpc/LockStateContext";
17+
import { LockState } from "@zmkfirmware/zmk-studio-ts-client/core";
18+
import { ConnectionContext } from "./rpc/ConnectionContext";
719

820
export interface AppHeaderProps {
921
connectedDeviceLabel?: string;
1022
onSave?: () => void | Promise<void>;
1123
onDiscard?: () => void | Promise<void>;
1224
onUndo?: () => Promise<void>;
1325
onRedo?: () => Promise<void>;
26+
onResetSettings?: () => void | Promise<void>;
27+
onDisconnect?: () => void | Promise<void>;
1428
canUndo?: boolean;
1529
canRedo?: boolean;
1630
}
@@ -23,7 +37,25 @@ export const AppHeader = ({
2337
onUndo,
2438
onSave,
2539
onDiscard,
40+
onDisconnect,
41+
onResetSettings,
2642
}: AppHeaderProps) => {
43+
const [showSettingsReset, setShowSettingsReset] = useState(false);
44+
45+
const lockState = useContext(LockStateContext);
46+
const connectionState = useContext(ConnectionContext);
47+
48+
useEffect(() => {
49+
if (
50+
(!connectionState.conn ||
51+
lockState != LockState.ZMK_STUDIO_CORE_LOCK_STATE_UNLOCKED) &&
52+
showSettingsReset
53+
) {
54+
setShowSettingsReset(false);
55+
}
56+
}, [lockState, showSettingsReset]);
57+
58+
const showSettingsRef = useModalRef(showSettingsReset);
2759
const [unsaved, setUnsaved] = useConnectedDeviceData<boolean>(
2860
{ keymap: { checkUnsavedChanges: true } },
2961
(r) => r.keymap?.checkUnsavedChanges
@@ -36,7 +68,51 @@ export const AppHeader = ({
3668
return (
3769
<header className="top-0 left-0 right-0 grid grid-cols-[1fr_auto_1fr] items-center justify-between border-b border-text-base">
3870
<p className="px-3">ZMK Studio</p>
39-
<p className="text-center">{connectedDeviceLabel}</p>
71+
<dialog
72+
ref={showSettingsRef}
73+
className="p-5 rounded-lg border-text-base border max-w-[50vw]"
74+
>
75+
<h2 className="my-2 text-lg">Settings Reset</h2>
76+
<div>
77+
<p>
78+
Settings reset will remove any customizations previously made in ZMK
79+
Studio and restore the stock keymap
80+
</p>
81+
<p>Continue?</p>
82+
<div className="flex justify-end my-2 gap-3">
83+
<button onClick={() => setShowSettingsReset(false)}>Cancel</button>
84+
<button
85+
onClick={() => {
86+
setShowSettingsReset(false);
87+
onResetSettings?.();
88+
}}
89+
>
90+
Reset Settings
91+
</button>
92+
</div>
93+
</div>
94+
</dialog>
95+
<MenuTrigger>
96+
<Button
97+
className="text-center enabled:after:content-['⏷'] after:relative after:left-2 pr-3"
98+
isDisabled={!connectedDeviceLabel}
99+
>
100+
{connectedDeviceLabel}
101+
</Button>
102+
<Popover>
103+
<Menu className="border rounded bg-bg-base">
104+
<MenuItem className="p-1" onAction={onDisconnect}>
105+
Disconnect
106+
</MenuItem>
107+
<MenuItem
108+
className="p-1"
109+
onAction={() => setShowSettingsReset(true)}
110+
>
111+
Settings Reset
112+
</MenuItem>
113+
</Menu>
114+
</Popover>
115+
</MenuTrigger>
40116
<div className="flex justify-end">
41117
{onUndo && (
42118
<button

src/tauri/ble.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export async function connect(dev: AvailableDevice): Promise<RpcTransport> {
1313
throw new Error("Failed to connect");
1414
}
1515

16+
let abortController = new AbortController();
17+
1618
let writable = new WritableStream({
1719
async write(chunk, _controller) {
1820
await invoke("transport_send_data", new Uint8Array(chunk));
@@ -39,5 +41,16 @@ export async function connect(dev: AvailableDevice): Promise<RpcTransport> {
3941
}
4042
);
4143

42-
return { label: dev.label, readable, writable };
44+
let signal = abortController.signal;
45+
46+
let abort_cb = async (_reason: any) => {
47+
unlisten_data();
48+
unlisten_disconnected();
49+
await invoke("transport_close");
50+
signal.removeEventListener("abort", abort_cb);
51+
};
52+
53+
signal.addEventListener("abort", abort_cb);
54+
55+
return { label: dev.label, abortController, readable, writable };
4356
}

src/tauri/serial.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export async function connect(dev: AvailableDevice): Promise<RpcTransport> {
1313
throw new Error("Failed to connect");
1414
}
1515

16+
let abortController = new AbortController();
17+
1618
let writable = new WritableStream({
1719
async write(chunk, _controller) {
1820
await invoke("transport_send_data", new Uint8Array(chunk));
@@ -39,5 +41,16 @@ export async function connect(dev: AvailableDevice): Promise<RpcTransport> {
3941
}
4042
);
4143

42-
return { label: dev.label, readable, writable };
44+
let signal = abortController.signal;
45+
46+
let abort_cb = async (_reason: any) => {
47+
unlisten_data();
48+
unlisten_disconnected();
49+
await invoke("transport_close");
50+
signal.removeEventListener("abort", abort_cb);
51+
};
52+
53+
signal.addEventListener("abort", abort_cb);
54+
55+
return { label: dev.label, abortController, readable, writable };
4356
}

0 commit comments

Comments
 (0)