pymobiledevice3 5.0.4__py3-none-any.whl → 7.0.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. misc/understanding_idevice_protocol_layers.md +10 -5
  2. pymobiledevice3/__main__.py +171 -46
  3. pymobiledevice3/_version.py +2 -2
  4. pymobiledevice3/bonjour.py +22 -21
  5. pymobiledevice3/cli/activation.py +24 -22
  6. pymobiledevice3/cli/afc.py +49 -41
  7. pymobiledevice3/cli/amfi.py +13 -18
  8. pymobiledevice3/cli/apps.py +71 -65
  9. pymobiledevice3/cli/backup.py +134 -93
  10. pymobiledevice3/cli/bonjour.py +31 -29
  11. pymobiledevice3/cli/cli_common.py +175 -232
  12. pymobiledevice3/cli/companion_proxy.py +12 -12
  13. pymobiledevice3/cli/crash.py +95 -52
  14. pymobiledevice3/cli/developer/__init__.py +62 -0
  15. pymobiledevice3/cli/developer/accessibility/__init__.py +65 -0
  16. pymobiledevice3/cli/developer/accessibility/settings.py +43 -0
  17. pymobiledevice3/cli/developer/arbitration.py +50 -0
  18. pymobiledevice3/cli/developer/condition.py +33 -0
  19. pymobiledevice3/cli/developer/core_device.py +294 -0
  20. pymobiledevice3/cli/developer/debugserver.py +244 -0
  21. pymobiledevice3/cli/developer/dvt/__init__.py +438 -0
  22. pymobiledevice3/cli/developer/dvt/core_profile_session.py +295 -0
  23. pymobiledevice3/cli/developer/dvt/simulate_location.py +56 -0
  24. pymobiledevice3/cli/developer/dvt/sysmon/__init__.py +69 -0
  25. pymobiledevice3/cli/developer/dvt/sysmon/process.py +188 -0
  26. pymobiledevice3/cli/developer/fetch_symbols.py +108 -0
  27. pymobiledevice3/cli/developer/simulate_location.py +51 -0
  28. pymobiledevice3/cli/diagnostics/__init__.py +75 -0
  29. pymobiledevice3/cli/diagnostics/battery.py +47 -0
  30. pymobiledevice3/cli/idam.py +42 -0
  31. pymobiledevice3/cli/lockdown.py +70 -75
  32. pymobiledevice3/cli/mounter.py +99 -57
  33. pymobiledevice3/cli/notification.py +38 -26
  34. pymobiledevice3/cli/pcap.py +36 -20
  35. pymobiledevice3/cli/power_assertion.py +15 -16
  36. pymobiledevice3/cli/processes.py +11 -17
  37. pymobiledevice3/cli/profile.py +120 -75
  38. pymobiledevice3/cli/provision.py +27 -26
  39. pymobiledevice3/cli/remote.py +109 -100
  40. pymobiledevice3/cli/restore.py +134 -129
  41. pymobiledevice3/cli/springboard.py +50 -50
  42. pymobiledevice3/cli/syslog.py +145 -65
  43. pymobiledevice3/cli/usbmux.py +66 -27
  44. pymobiledevice3/cli/version.py +2 -5
  45. pymobiledevice3/cli/webinspector.py +232 -156
  46. pymobiledevice3/exceptions.py +6 -2
  47. pymobiledevice3/lockdown.py +5 -1
  48. pymobiledevice3/lockdown_service_provider.py +5 -0
  49. pymobiledevice3/remote/remote_service_discovery.py +18 -10
  50. pymobiledevice3/restore/device.py +28 -4
  51. pymobiledevice3/restore/restore.py +2 -2
  52. pymobiledevice3/service_connection.py +15 -12
  53. pymobiledevice3/services/afc.py +731 -220
  54. pymobiledevice3/services/device_link.py +45 -31
  55. pymobiledevice3/services/idam.py +20 -0
  56. pymobiledevice3/services/lockdown_service.py +12 -9
  57. pymobiledevice3/services/mobile_config.py +1 -0
  58. pymobiledevice3/services/mobilebackup2.py +6 -3
  59. pymobiledevice3/services/os_trace.py +97 -55
  60. pymobiledevice3/services/remote_fetch_symbols.py +13 -8
  61. pymobiledevice3/services/screenshot.py +2 -2
  62. pymobiledevice3/services/web_protocol/alert.py +8 -8
  63. pymobiledevice3/services/web_protocol/automation_session.py +87 -79
  64. pymobiledevice3/services/web_protocol/cdp_screencast.py +2 -1
  65. pymobiledevice3/services/web_protocol/driver.py +71 -70
  66. pymobiledevice3/services/web_protocol/element.py +58 -56
  67. pymobiledevice3/services/web_protocol/selenium_api.py +47 -47
  68. pymobiledevice3/services/web_protocol/session_protocol.py +3 -2
  69. pymobiledevice3/services/web_protocol/switch_to.py +23 -19
  70. pymobiledevice3/services/webinspector.py +42 -67
  71. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/METADATA +5 -3
  72. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/RECORD +76 -61
  73. pymobiledevice3/cli/completions.py +0 -50
  74. pymobiledevice3/cli/developer.py +0 -1539
  75. pymobiledevice3/cli/diagnostics.py +0 -110
  76. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/WHEEL +0 -0
  77. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/entry_points.txt +0 -0
  78. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/licenses/LICENSE +0 -0
  79. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/top_level.txt +0 -0
@@ -4,18 +4,20 @@ import logging
4
4
  import plistlib
5
5
  import tempfile
6
6
  import traceback
7
- from collections.abc import Generator
7
+ from collections.abc import Iterator
8
8
  from pathlib import Path
9
- from typing import IO, Optional, Union
9
+ from typing import IO, Annotated, Optional
10
10
  from zipfile import ZipFile
11
11
 
12
12
  import click
13
13
  import IPython
14
14
  import requests
15
+ import typer
15
16
  from pygments import formatters, highlight, lexers
17
+ from typer_injector import Depends, InjectingTyper
16
18
 
17
19
  from pymobiledevice3 import usbmux
18
- from pymobiledevice3.cli.cli_common import is_invoked_for_completion, print_json, prompt_selection, set_verbosity
20
+ from pymobiledevice3.cli.cli_common import is_invoked_for_completion, print_json, prompt_selection
19
21
  from pymobiledevice3.exceptions import ConnectionFailedError, ConnectionFailedToUsbmuxdError, IncorrectModeError
20
22
  from pymobiledevice3.irecv import IRecv
21
23
  from pymobiledevice3.lockdown import LockdownClient, create_using_usbmux
@@ -25,55 +27,66 @@ from pymobiledevice3.restore.restore import Restore
25
27
  from pymobiledevice3.services.diagnostics import DiagnosticsService
26
28
  from pymobiledevice3.utils import file_download
27
29
 
30
+ logger = logging.getLogger(__name__)
31
+
32
+
28
33
  SHELL_USAGE = """
29
34
  # use `irecv` variable to access Restore mode API
30
35
  # for example:
31
36
  print(irecv.getenv('build-version'))
32
37
  """
33
-
34
- logger = logging.getLogger(__name__)
35
38
  IPSWME_API = "https://api.ipsw.me/v4/device/"
36
39
 
37
40
 
38
- class Command(click.Command):
39
- def __init__(self, *args, **kwargs):
40
- super().__init__(*args, **kwargs)
41
- self.params[:0] = [
42
- click.Option(("device", "--ecid"), type=click.INT, callback=self.device),
43
- click.Option(("verbosity", "-v", "--verbose"), count=True, callback=set_verbosity, expose_value=False),
44
- ]
45
-
46
- @staticmethod
47
- def device(ctx, param, value) -> Optional[Union[LockdownClient, IRecv]]:
48
- if is_invoked_for_completion():
49
- # prevent lockdown connection establishment when in autocomplete mode
50
- return
51
-
52
- ecid = value
53
- logger.debug("searching among connected devices via lockdownd")
54
- devices = [dev for dev in usbmux.list_devices() if dev.connection_type == "USB"]
55
- if len(devices) > 1:
56
- raise click.ClickException("Multiple device detected")
57
- try:
58
- for device in devices:
59
- try:
60
- lockdown = create_using_usbmux(serial=device.serial, connection_type="USB")
61
- except (ConnectionFailedError, IncorrectModeError):
62
- continue
63
- if (ecid is None) or (lockdown.ecid == value):
64
- logger.debug("found device")
65
- return lockdown
66
- else:
67
- continue
68
- except ConnectionFailedToUsbmuxdError:
69
- pass
70
-
71
- logger.debug("waiting for device to be available in Recovery mode")
72
- return IRecv(ecid=ecid)
41
+ cli = InjectingTyper(
42
+ name="restore",
43
+ help="Restore/erase IPSWs, fetch blobs, and manage devices in Recovery/DFU.",
44
+ no_args_is_help=True,
45
+ )
46
+
47
+
48
+ def device_dependency(
49
+ ecid: Annotated[
50
+ Optional[str],
51
+ typer.Option(
52
+ help="Target device ECID; defaults to the first connected USB device or waits for Recovery/DFU.",
53
+ ),
54
+ ] = None,
55
+ ) -> Optional[Device]:
56
+ if is_invoked_for_completion():
57
+ # prevent lockdown connection establishment when in autocomplete mode
58
+ return None
59
+
60
+ logger.debug("searching among connected devices via lockdownd")
61
+ devices = [dev for dev in usbmux.list_devices() if dev.connection_type == "USB"]
62
+ if len(devices) > 1:
63
+ raise click.ClickException("Multiple device detected")
64
+ try:
65
+ for device in devices:
66
+ try:
67
+ lockdown = create_using_usbmux(serial=device.serial, connection_type="USB")
68
+ except (ConnectionFailedError, IncorrectModeError):
69
+ continue
70
+ if (ecid is None) or (lockdown.ecid == ecid):
71
+ logger.debug("found device")
72
+ return Device(lockdown=lockdown)
73
+ else:
74
+ continue
75
+ except ConnectionFailedToUsbmuxdError:
76
+ pass
77
+
78
+ logger.debug("waiting for device to be available in Recovery mode")
79
+ return Device(irecv=IRecv(ecid=ecid))
80
+
81
+
82
+ DeviceDep = Annotated[
83
+ Device,
84
+ Depends(device_dependency),
85
+ ]
73
86
 
74
87
 
75
88
  @contextlib.contextmanager
76
- def tempzip_download_ctx(url: str) -> Generator[ZipFile, None, None]:
89
+ def tempzip_download_ctx(url: str) -> Iterator[ZipFile]:
77
90
  with tempfile.TemporaryDirectory() as tmpdir:
78
91
  tmpzip = Path(tmpdir) / url.split("/")[-1]
79
92
  file_download(url, tmpzip)
@@ -81,33 +94,52 @@ def tempzip_download_ctx(url: str) -> Generator[ZipFile, None, None]:
81
94
 
82
95
 
83
96
  @contextlib.contextmanager
84
- def zipfile_ctx(path: str) -> Generator[ZipFile, None, None]:
97
+ def zipfile_ctx(path: str) -> Iterator[ZipFile]:
85
98
  yield ZipFile(path)
86
99
 
87
100
 
88
- class IPSWCommand(Command):
89
- def __init__(self, *args, **kwargs):
90
- super().__init__(*args, **kwargs)
91
- self.params.extend([
92
- click.Option(("ipsw_ctx", "-i", "--ipsw"), required=False, callback=self.ipsw_ctx, help="local IPSW file"),
93
- click.Option(("tss", "--tss"), type=click.File("rb"), callback=self.tss),
94
- ])
101
+ def ipsw_ctx_dependency(
102
+ device: DeviceDep,
103
+ ipsw: Annotated[
104
+ Optional[str],
105
+ typer.Option(
106
+ "--ipsw",
107
+ "-i",
108
+ help="Path or URL to an IPSW. If omitted, choose a signed build interactively.",
109
+ ),
110
+ ] = None,
111
+ ) -> contextlib.AbstractContextManager[ZipFile]:
112
+ if ipsw and not ipsw.startswith(("http://", "https://")):
113
+ return zipfile_ctx(ipsw)
114
+
115
+ url = ipsw
116
+ if url is None:
117
+ url = query_ipswme(device.product_type)
118
+ return tempzip_download_ctx(url)
119
+
120
+
121
+ IPSWCtxDep = Annotated[
122
+ contextlib.AbstractContextManager[ZipFile],
123
+ Depends(ipsw_ctx_dependency),
124
+ ]
95
125
 
96
- @staticmethod
97
- def ipsw_ctx(ctx, param, value) -> Generator[ZipFile, None, None]:
98
- if value and not value.startswith(("http://", "https://")):
99
- return zipfile_ctx(value)
100
126
 
101
- url = value
102
- if url is None:
103
- url = query_ipswme(ctx.params["device"].product_type)
104
- return tempzip_download_ctx(url)
127
+ def tss_dependency(
128
+ tss: Annotated[
129
+ Optional[Path],
130
+ typer.Option(help="Path to SHSH blob plist to use for signing requests."),
131
+ ] = None,
132
+ ) -> None:
133
+ if tss is None:
134
+ return
135
+ with tss.open("rb") as tss_file:
136
+ return plistlib.load(tss_file)
105
137
 
106
- @staticmethod
107
- def tss(ctx, param, value) -> Optional[IO]:
108
- if value is None:
109
- return
110
- return plistlib.load(value)
138
+
139
+ TSSDep = Annotated[
140
+ Optional[dict],
141
+ Depends(tss_dependency),
142
+ ]
111
143
 
112
144
 
113
145
  def query_ipswme(identifier: str) -> str:
@@ -118,15 +150,9 @@ def query_ipswme(identifier: str) -> str:
118
150
  return firmwares[idx]["url"]
119
151
 
120
152
 
121
- async def restore_update_task(device: Device, ipsw: ZipFile, tss: Optional[IO], erase: bool, ignore_fdr: bool) -> None:
122
- lockdown = None
123
- irecv = None
124
- if isinstance(device, LockdownClient):
125
- lockdown = device
126
- elif isinstance(device, IRecv):
127
- irecv = device
128
- device = Device(lockdown=lockdown, irecv=irecv)
129
-
153
+ async def restore_update_task(
154
+ device: Device, ipsw: ZipFile, tss: Optional[dict], erase: bool, ignore_fdr: bool
155
+ ) -> None:
130
156
  behavior = Behavior.Update
131
157
  if erase:
132
158
  behavior = Behavior.Erase
@@ -139,19 +165,8 @@ async def restore_update_task(device: Device, ipsw: ZipFile, tss: Optional[IO],
139
165
  raise
140
166
 
141
167
 
142
- @click.group()
143
- def cli() -> None:
144
- pass
145
-
146
-
147
- @cli.group()
148
- def restore() -> None:
149
- """Restore an IPSW or access device in recovery mode"""
150
- pass
151
-
152
-
153
- @restore.command("shell", cls=Command)
154
- def restore_shell(device):
168
+ @cli.command("shell")
169
+ def restore_shell(device: DeviceDep) -> None:
155
170
  """create an IPython shell for interacting with iBoot"""
156
171
  IPython.embed(
157
172
  header=highlight(SHELL_USAGE, lexers.PythonLexer(), formatters.Terminal256Formatter(style="native")),
@@ -161,40 +176,34 @@ def restore_shell(device):
161
176
  )
162
177
 
163
178
 
164
- @restore.command("enter", cls=Command)
165
- def restore_enter(device):
179
+ @cli.command("enter")
180
+ def restore_enter(device: DeviceDep) -> None:
166
181
  """enter Recovery mode"""
167
182
  if isinstance(device, LockdownClient):
168
183
  device.enter_recovery()
169
184
 
170
185
 
171
- @restore.command("exit")
172
- def restore_exit():
186
+ @cli.command("exit")
187
+ def restore_exit() -> None:
173
188
  """exit Recovery mode"""
174
189
  irecv = IRecv()
175
190
  irecv.set_autoboot(True)
176
191
  irecv.reboot()
177
192
 
178
193
 
179
- @restore.command("restart", cls=Command)
180
- def restore_restart(device):
194
+ @cli.command("restart")
195
+ def restore_restart(device: DeviceDep) -> None:
181
196
  """restarts device"""
182
- if isinstance(device, LockdownClient):
183
- with DiagnosticsService(device) as diagnostics:
197
+ if device.is_lockdown:
198
+ with DiagnosticsService(device.lockdown) as diagnostics:
184
199
  diagnostics.restart()
185
200
  else:
186
- device.reboot()
201
+ device.irecv.reboot()
187
202
 
188
203
 
189
- async def restore_tss_task(device: Device, ipsw_ctx: Generator, tss: IO, out: Optional[IO]) -> None:
190
- lockdown = None
191
- irecv = None
192
- if isinstance(device, LockdownClient):
193
- lockdown = device
194
- elif isinstance(device, IRecv):
195
- irecv = device
196
-
197
- device = Device(lockdown=lockdown, irecv=irecv)
204
+ async def restore_tss_task(
205
+ device: Device, ipsw_ctx: contextlib.AbstractContextManager[ZipFile], out: Optional[IO]
206
+ ) -> None:
198
207
  with ipsw_ctx as ipsw:
199
208
  tss = await Recovery(ipsw, device).fetch_tss_record()
200
209
  if out:
@@ -202,46 +211,42 @@ async def restore_tss_task(device: Device, ipsw_ctx: Generator, tss: IO, out: Op
202
211
  print_json(tss)
203
212
 
204
213
 
205
- @restore.command("tss", cls=IPSWCommand)
206
- @click.argument("out", type=click.File("wb"), required=False)
207
- def restore_tss(device: Device, ipsw_ctx: Generator, tss: IO, out: Optional[IO]) -> None:
214
+ @cli.command("tss")
215
+ def restore_tss(device: DeviceDep, ipsw_ctx: IPSWCtxDep, out: Optional[Path] = None) -> None:
208
216
  """query SHSH blobs"""
209
- asyncio.run(restore_tss_task(device, ipsw_ctx, tss, out), debug=True)
217
+ with out.open("wb") if out else contextlib.nullcontext() as out_file:
218
+ asyncio.run(restore_tss_task(device, ipsw_ctx, out_file), debug=True)
210
219
 
211
220
 
212
- async def restore_ramdisk_task(device: Device, ipsw_ctx: Generator) -> None:
213
- lockdown = None
214
- irecv = None
215
- if isinstance(device, LockdownClient):
216
- lockdown = device
217
- elif isinstance(device, IRecv):
218
- irecv = device
219
- device = Device(lockdown=lockdown, irecv=irecv)
220
-
221
+ async def restore_ramdisk_task(device: Device, ipsw_ctx: contextlib.AbstractContextManager[ZipFile]) -> None:
221
222
  with ipsw_ctx as ipsw:
222
223
  await Recovery(ipsw, device).boot_ramdisk()
223
224
 
224
225
 
225
- @restore.command("ramdisk", cls=IPSWCommand)
226
- def restore_ramdisk(device: Device, ipsw_ctx: Generator, tss: IO) -> None:
226
+ @cli.command("ramdisk")
227
+ def restore_ramdisk(device: DeviceDep, ipsw_ctx: IPSWCtxDep) -> None:
227
228
  """
228
- don't perform an actual restore. just enter the update ramdisk
229
-
230
- ipsw can be either a filename or an url
229
+ Boot only the update ramdisk without performing a restore (IPSW path or URL accepted).
231
230
  """
232
231
  asyncio.run(restore_ramdisk_task(device, ipsw_ctx), debug=True)
233
232
 
234
233
 
235
- @restore.command("update", cls=IPSWCommand)
236
- @click.option("--erase", is_flag=True, help="use the Erase BuildIdentity (full factory-reset)")
237
- @click.option(
238
- "--ignore-fdr", is_flag=True, help="only establish an FDR service connection, but don't proxy any traffic"
239
- )
240
- def restore_update(device: Device, ipsw_ctx: Generator, tss: IO, erase: bool, ignore_fdr: bool) -> None:
234
+ @cli.command("update")
235
+ def restore_update(
236
+ device: DeviceDep,
237
+ ipsw_ctx: IPSWCtxDep,
238
+ tss: TSSDep,
239
+ erase: Annotated[
240
+ bool,
241
+ typer.Option(help="Erase and restore (factory reset) instead of updating in place."),
242
+ ] = False,
243
+ ignore_fdr: Annotated[
244
+ bool,
245
+ typer.Option(help="Connect to the FDR service only (debug mode; no traffic proxying)."),
246
+ ] = False,
247
+ ) -> None:
241
248
  """
242
- perform an update
243
-
244
- ipsw can be either a filename or an url
249
+ Update or restore the device using an IPSW (local path or URL).
245
250
  """
246
251
  with ipsw_ctx as ipsw:
247
252
  asyncio.run(restore_update_task(device, ipsw, tss, erase, ignore_fdr), debug=True)
@@ -1,11 +1,11 @@
1
- from typing import IO
1
+ from pathlib import Path
2
+ from typing import Annotated, Literal
2
3
 
3
- import click
4
4
  import IPython
5
+ import typer
6
+ from typer_injector import InjectingTyper
5
7
 
6
- from pymobiledevice3.cli.cli_common import Command, print_json
7
- from pymobiledevice3.lockdown import LockdownClient
8
- from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
8
+ from pymobiledevice3.cli.cli_common import ServiceProviderDep, print_json
9
9
  from pymobiledevice3.services.springboard import SpringBoardServicesService
10
10
 
11
11
  SHELL_USAGE = """
@@ -13,32 +13,28 @@ Use `service` to access the service features
13
13
  """
14
14
 
15
15
 
16
- @click.group()
17
- def cli():
18
- pass
16
+ cli = InjectingTyper(
17
+ name="springboard",
18
+ help="Interact with SpringBoard UI (icons, wallpapers, orientation, shell).",
19
+ no_args_is_help=True,
20
+ )
21
+ state_cli = InjectingTyper(
22
+ name="state",
23
+ help="Icon state operations.",
24
+ no_args_is_help=True,
25
+ )
26
+ cli.add_typer(state_cli)
19
27
 
20
28
 
21
- @cli.group()
22
- def springboard():
23
- """Access device UI"""
24
- pass
25
-
26
-
27
- @springboard.group()
28
- def state():
29
- """icons state options"""
30
- pass
31
-
32
-
33
- @state.command("get", cls=Command)
34
- def state_get(service_provider: LockdownClient):
35
- """get icon state"""
29
+ @state_cli.command("get")
30
+ def state_get(service_provider: ServiceProviderDep) -> None:
31
+ """Fetch the current icon layout/state."""
36
32
  print_json(SpringBoardServicesService(lockdown=service_provider).get_icon_state())
37
33
 
38
34
 
39
- @springboard.command("shell", cls=Command)
40
- def springboard_shell(service_provider: LockdownClient):
41
- """open a shell to communicate with SpringBoardServicesService"""
35
+ @cli.command("shell")
36
+ def springboard_shell(service_provider: ServiceProviderDep) -> None:
37
+ """Open an IPython shell bound to SpringBoardServicesService."""
42
38
  service = SpringBoardServicesService(lockdown=service_provider)
43
39
  IPython.embed(
44
40
  header=SHELL_USAGE,
@@ -48,43 +44,47 @@ def springboard_shell(service_provider: LockdownClient):
48
44
  )
49
45
 
50
46
 
51
- @springboard.command("icon", cls=Command)
52
- @click.argument("bundle_id")
53
- @click.argument("out", type=click.File("wb"))
54
- def springboard_icon(service_provider: LockdownClient, bundle_id, out):
55
- """get application's icon"""
56
- out.write(SpringBoardServicesService(lockdown=service_provider).get_icon_pngdata(bundle_id))
47
+ @cli.command("icon")
48
+ def springboard_icon(service_provider: ServiceProviderDep, bundle_id: str, out: Path) -> None:
49
+ """Save an app's icon PNG to the given path."""
50
+ out.write_bytes(SpringBoardServicesService(lockdown=service_provider).get_icon_pngdata(bundle_id))
57
51
 
58
52
 
59
- @springboard.command("orientation", cls=Command)
60
- def springboard_orientation(service_provider: LockdownClient):
61
- """get screen orientation"""
53
+ @cli.command("orientation")
54
+ def springboard_orientation(service_provider: ServiceProviderDep) -> None:
55
+ """Print current screen orientation."""
62
56
  print(SpringBoardServicesService(lockdown=service_provider).get_interface_orientation())
63
57
 
64
58
 
65
- @springboard.command("wallpaper-home-screen", cls=Command)
66
- @click.argument("out", type=click.File("wb"))
67
- def springboard_wallpaper_home_screen(service_provider: LockdownClient, out: IO) -> None:
68
- """get homescreen wallpaper"""
69
- out.write(SpringBoardServicesService(lockdown=service_provider).get_wallpaper_pngdata())
59
+ @cli.command("wallpaper-home-screen")
60
+ def springboard_wallpaper_home_screen(service_provider: ServiceProviderDep, out: Path) -> None:
61
+ """Save the homescreen wallpaper PNG to the given path."""
62
+ out.write_bytes(SpringBoardServicesService(lockdown=service_provider).get_wallpaper_pngdata())
70
63
 
71
64
 
72
- @springboard.command("wallpaper-preview-image", cls=Command)
73
- @click.argument("wallpaper-name", type=click.Choice(["homescreen", "lockscreen"]))
74
- @click.argument("out", type=click.File("wb"))
75
- @click.option("-r", "--reload", is_flag=True, help="reload icon state before fetching image")
65
+ @cli.command("wallpaper-preview-image")
76
66
  def springboard_wallpaper_preview_image(
77
- service_provider: LockdownClient, wallpaper_name: str, out: IO, reload: bool
67
+ service_provider: ServiceProviderDep,
68
+ wallpaper_name: Literal["homescreen", "lockscreen"],
69
+ out: Path,
70
+ reload: Annotated[
71
+ bool,
72
+ typer.Option(
73
+ "--reload",
74
+ "-r",
75
+ help="reload icon state before fetching image",
76
+ ),
77
+ ] = False,
78
78
  ) -> None:
79
- """get the preview image of either the homescreen or the lockscreen"""
79
+ """Save the preview image for the homescreen or lockscreen wallpaper (optionally reload state first)."""
80
80
  with SpringBoardServicesService(lockdown=service_provider) as springboard_service:
81
81
  if reload:
82
82
  springboard_service.reload_icon_state()
83
- out.write(springboard_service.get_wallpaper_preview_image(wallpaper_name))
83
+ out.write_bytes(springboard_service.get_wallpaper_preview_image(wallpaper_name))
84
84
 
85
85
 
86
- @springboard.command("homescreen-icon-metrics", cls=Command)
87
- def springboard_homescreen_icon_metrics(service_provider: LockdownServiceProvider) -> None:
88
- """Get homescreen icon metrics"""
86
+ @cli.command("homescreen-icon-metrics")
87
+ def springboard_homescreen_icon_metrics(service_provider: ServiceProviderDep) -> None:
88
+ """Print homescreen icon spacing/metrics."""
89
89
  with SpringBoardServicesService(lockdown=service_provider) as springboard_service:
90
90
  print_json(springboard_service.get_homescreen_icon_metrics())