Skip to content

Commit 458fc9c

Browse files
committed
Add screenshot button
1 parent 7f7b6e6 commit 458fc9c

12 files changed

Lines changed: 227 additions & 44 deletions

File tree

src-tauri/Cargo.lock

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

src-tauri/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ tauri-plugin-store = "2.4.0"
2121
tauri-plugin-opener = "2.5.0"
2222
tauri-plugin-os = "2.3.1"
2323
tauri-plugin-process = "2.3.0"
24-
idevice = { version = "0.1.40", features = ["usbmuxd", "syslog_relay", "core_device_proxy", "tunnel_tcp_stack", "rsd", "core_device", "dvt", "mobile_image_mounter", "tss"] }
24+
idevice = { version = "0.1.42", features = ["usbmuxd", "syslog_relay", "core_device_proxy", "tunnel_tcp_stack", "rsd", "core_device", "dvt", "mobile_image_mounter", "tss"] }
2525
futures = "0.3.31"
2626
keyring = "2"
2727
once_cell = "1.21.3"
@@ -30,7 +30,7 @@ hex = "0.4.3"
3030
openssl = { version = "0.10", features = ["vendored"] }
3131
plist = { version = "1.7.4" }
3232
zip = { version = "4.5", default-features = false, features = ["deflate"] }
33-
isideload = { version = "0.1.9", features = ["vendored-botan", "vendored-openssl"] }
33+
isideload = { version = "0.1.10", features = ["vendored-botan", "vendored-openssl"] }
3434
uuid = "1.18.0"
3535
walkdir = "2.5.0"
3636
dircpy = "0.3.19"

src-tauri/src/builder/crossplatform.rs

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -118,30 +118,31 @@ pub fn linux_path(path: &str) -> Result<String, String> {
118118
}
119119
}
120120

121+
#[cfg(target_os = "windows")]
121122
pub fn is_linux_dir(path: &str) -> Result<bool, String> {
122-
#[cfg(not(target_os = "windows"))]
123-
{
124-
let p = Path::new(path);
125-
return Ok(p.is_dir());
123+
// #[cfg(not(target_os = "windows"))]
124+
// {
125+
// let p = Path::new(path);
126+
// return Ok(p.is_dir());
127+
// }
128+
// #[cfg(target_os = "windows")]
129+
// {
130+
if !has_wsl() {
131+
return Err("WSL is not available".to_string());
126132
}
127-
#[cfg(target_os = "windows")]
128-
{
129-
if !has_wsl() {
130-
return Err("WSL is not available".to_string());
131-
}
132-
let output = Command::new("wsl")
133-
.arg("test")
134-
.arg("-d")
135-
.arg(&path)
136-
.creation_flags(CREATE_NO_WINDOW)
137-
.output()
138-
.expect("failed to execute process");
139-
if output.status.success() {
140-
Ok(true)
141-
} else {
142-
Ok(false)
143-
}
133+
let output = Command::new("wsl")
134+
.arg("test")
135+
.arg("-d")
136+
.arg(&path)
137+
.creation_flags(CREATE_NO_WINDOW)
138+
.output()
139+
.expect("failed to execute process");
140+
if output.status.success() {
141+
Ok(true)
142+
} else {
143+
Ok(false)
144144
}
145+
//}
145146
}
146147

147148
pub fn linux_temp_dir() -> Result<PathBuf, String> {

src-tauri/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use sideloader::{
3131
reset_anisette, revoke_certificate,
3232
},
3333
device::{is_ddi_mounted, mount_ddi},
34+
screenshot::take_screenshot,
3435
sideload::refresh_idevice,
3536
stdout::{is_streaming_stdout, start_stream_stdout, stop_stream_stdout, StdoutStream},
3637
syslog::{is_streaming_syslog, start_stream_syslog, stop_stream_syslog, SyslogStream},
@@ -146,6 +147,7 @@ fn main() {
146147
install_wsl,
147148
is_ddi_mounted,
148149
mount_ddi,
150+
take_screenshot,
149151
])
150152
.run(tauri::generate_context!())
151153
.expect("error while running tauri application");

src-tauri/src/sideloader/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod apple;
22
pub mod apple_commands;
33
pub mod device;
4+
pub mod screenshot;
45
pub mod sideload;
56
pub mod stdout;
67
pub mod syslog;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use idevice::{
2+
core_device_proxy::CoreDeviceProxy,
3+
dvt::{remote_server::RemoteServerClient, screenshot::ScreenshotClient},
4+
rsd::RsdHandshake,
5+
IdeviceService,
6+
};
7+
8+
use crate::sideloader::device::{get_provider, DeviceInfo};
9+
10+
#[tauri::command]
11+
pub async fn take_screenshot(device: DeviceInfo) -> Result<Vec<u8>, String> {
12+
let provider = get_provider(&device).await?;
13+
14+
let proxy = CoreDeviceProxy::connect(&provider)
15+
.await
16+
.map_err(|e| format!("Failed to connect to device proxy: {}", e))?;
17+
let rsd_port = proxy.handshake.server_rsd_port;
18+
19+
let adapter = proxy
20+
.create_software_tunnel()
21+
.map_err(|e| format!("Failed to create software tunnel: {}", e))?;
22+
let mut adapter = adapter.to_async_handle();
23+
24+
let rsd_stream = adapter
25+
.connect(rsd_port)
26+
.await
27+
.map_err(|e| format!("Failed to connect to RSD: {}", e))?;
28+
29+
let handshake = RsdHandshake::new(rsd_stream)
30+
.await
31+
.map_err(|e| format!("Failed to create RSD handshake: {}", e))?;
32+
33+
let instruments_service = handshake
34+
.services
35+
.get("com.apple.instruments.dtservicehub")
36+
.ok_or("Instruments service not found")?;
37+
38+
let ts_client_stream = adapter
39+
.connect(instruments_service.port)
40+
.await
41+
.map_err(|e| format!("Failed to connect to remote server: {}", e))?;
42+
let mut ts_client = RemoteServerClient::new(ts_client_stream);
43+
ts_client
44+
.read_message(0)
45+
.await
46+
.map_err(|e| format!("Failed to read message: {}", e))?;
47+
let mut sc = ScreenshotClient::new(&mut ts_client)
48+
.await
49+
.map_err(|e| format!("Failed to create screenshot client: {}", e))?;
50+
51+
let screenshot_data = sc
52+
.take_screenshot()
53+
.await
54+
.map_err(|e| format!("Failed to take screenshot: {}", e))?;
55+
56+
Ok(screenshot_data)
57+
}

src/components/CommandButton.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export interface CommandButtonProps {
1414
clearConsole?: boolean;
1515
validate?: () => boolean;
1616
validateAsync?: () => Promise<boolean>;
17-
after?: () => void;
17+
after?: (data: any) => void;
1818
disabled?: boolean;
1919
useMenuItem?: boolean;
2020
shortcut?: React.ReactNode;
@@ -33,7 +33,7 @@ export default function CommandButton({
3333
clearConsole = true,
3434
validate = () => true,
3535
validateAsync = () => Promise.resolve(true),
36-
after = () => {},
36+
after = (_: any) => {},
3737
disabled = false,
3838
useMenuItem = false,
3939
size = "md",

src/components/Menu/MenuBar.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
PhonelinkSetup,
1616
Refresh,
1717
CleaningServices,
18+
CameraAlt,
1819
} from "@mui/icons-material";
1920
import { useParams } from "react-router-dom";
2021
import { Divider, Option, Select } from "@mui/joy";
@@ -34,14 +35,31 @@ export default function MenuBar({ callbacks, editor }: MenuBarProps) {
3435

3536
const resetMenuIndex = useCallback(() => setMenuIndex(null), []);
3637
const { path } = useParams<"path">();
37-
const { devices, selectedToolchain, selectedDevice, setSelectedDevice } =
38-
useIDE();
38+
const {
39+
devices,
40+
selectedToolchain,
41+
selectedDevice,
42+
setSelectedDevice,
43+
setScreenshot,
44+
mountDdi,
45+
} = useIDE();
3946
const [anisetteServer] = useStore<string>(
4047
"apple-id/anisette-server",
4148
"ani.sidestore.io"
4249
);
4350
const { addToast } = useToast();
4451

52+
const updateScreenshot = useCallback(
53+
(data: number[]) => {
54+
const blob = new Blob([new Uint8Array(data)], {
55+
type: "image/png",
56+
});
57+
const url = URL.createObjectURL(blob);
58+
setScreenshot(url);
59+
},
60+
[setScreenshot]
61+
);
62+
4563
useEffect(() => {
4664
const items: {
4765
shortcut: Shortcut;
@@ -312,6 +330,25 @@ export default function MenuBar({ callbacks, editor }: MenuBarProps) {
312330
}}
313331
sx={{ marginRight: 0 }}
314332
/>
333+
<CommandButton
334+
disabled={!selectedDevice}
335+
tooltip="Take Screenshot"
336+
variant="plain"
337+
command="take_screenshot"
338+
icon={<CameraAlt />}
339+
parameters={{
340+
device: selectedDevice,
341+
}}
342+
after={updateScreenshot}
343+
validateAsync={async () => {
344+
if (!selectedDevice) {
345+
addToast.error("Please select a device to take a screenshot of.");
346+
return false;
347+
}
348+
return await mountDdi(true);
349+
}}
350+
sx={{ marginRight: 0, marginLeft: 0 }}
351+
/>
315352
</div>
316353
</List>
317354
);

src/pages/IDE.css

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,41 @@
77
.file-explorer-tile {
88
padding-right: 0.5rem;
99
}
10+
11+
.screenshot-tile {
12+
padding: 0;
13+
margin: var(--padding-md);
14+
height: 100%;
15+
display: flex;
16+
flex-direction: column;
17+
overflow: hidden;
18+
align-items: center;
19+
box-sizing: border-box;
20+
width: calc(100% - var(--padding-md) * 2 - var(--padding-xs));
21+
}
22+
23+
.screenshot-tile > div {
24+
flex-shrink: 0;
25+
display: flex;
26+
justify-content: center;
27+
align-items: center;
28+
gap: var(--padding-lg);
29+
margin-bottom: var(--padding-lg);
30+
}
31+
32+
.screenshot-img-container {
33+
flex: 1 1 0;
34+
display: flex;
35+
justify-content: center;
36+
align-items: center;
37+
min-height: 0;
38+
min-width: 0;
39+
/* No padding or margin here */
40+
}
41+
42+
.screenshot-img-container > img {
43+
max-width: 100%;
44+
max-height: calc(100% - var(--padding-md) * 2);
45+
object-fit: contain;
46+
display: block;
47+
}

src/pages/IDE.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ import { ErrorIcon, useToast, WarningIcon } from "react-toast-plus";
2323
import SwiftMenu from "../components/SwiftMenu";
2424
import { restartServer } from "../utilities/lsp-client";
2525
import BottomBar from "../components/Tiles/BottomBar";
26-
import { open as openFileDialog } from "@tauri-apps/plugin-dialog";
26+
import { open as openFileDialog, save } from "@tauri-apps/plugin-dialog";
2727
import { IStandaloneCodeEditor } from "@codingame/monaco-vscode-api/vscode/vs/editor/standalone/browser/standaloneCodeEditor";
2828
import { DARWIN_SDK_VERSION } from "../utilities/constants";
29+
import { writeFile } from "@tauri-apps/plugin-fs";
2930

3031
export interface IDEProps {}
3132

@@ -54,6 +55,8 @@ export default () => {
5455
initialized,
5556
ready,
5657
darwinSDKVersion,
58+
screenshot,
59+
setScreenshot,
5760
} = useIDE();
5861
const [sourcekitStartup, setSourcekitStartup] = useStore<boolean | null>(
5962
"sourcekit/startup",
@@ -206,7 +209,7 @@ export default () => {
206209
<Splitter
207210
gutterTheme={theme === "dark" ? GutterTheme.Dark : GutterTheme.Light}
208211
direction={SplitDirection.Horizontal}
209-
initialSizes={[20, 80]}
212+
initialSizes={screenshot ? [20, 50, 30] : [20, 80]}
210213
>
211214
<Tile className="file-explorer-tile">
212215
<FileExplorer openFolder={path} setOpenFile={openNewFile} />
@@ -228,6 +231,40 @@ export default () => {
228231
/>
229232
<BottomBar />
230233
</Splitter>
234+
{screenshot && (
235+
<div className="screenshot-tile">
236+
<div>
237+
<Typography level="h3">Screenshot</Typography>
238+
<Button
239+
variant="outlined"
240+
onClick={async () => {
241+
const blob = await (await fetch(screenshot)).blob();
242+
const arrayBuffer = await blob.arrayBuffer();
243+
const uint8Array = new Uint8Array(arrayBuffer);
244+
const savePath = await save({
245+
title: "Save Screenshot",
246+
defaultPath: "screenshot.png",
247+
filters: [
248+
{ name: "PNG Image", extensions: ["png"] },
249+
{ name: "All Files", extensions: ["*"] },
250+
],
251+
});
252+
if (!savePath) return;
253+
await writeFile(savePath, uint8Array);
254+
addToast.success("Saved screenshot to " + savePath);
255+
}}
256+
>
257+
Save
258+
</Button>
259+
<Button variant="outlined" onClick={() => setScreenshot(null)}>
260+
Close
261+
</Button>
262+
</div>
263+
<div className="screenshot-img-container">
264+
<img src={screenshot} alt="screenshot" />
265+
</div>
266+
</div>
267+
)}
231268
</Splitter>
232269
{initialized &&
233270
selectedToolchain !== null &&

0 commit comments

Comments
 (0)