-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlauncher.py
More file actions
731 lines (644 loc) · 32 KB
/
Copy pathlauncher.py
File metadata and controls
731 lines (644 loc) · 32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
#!/usr/bin/env python3
# WatchROM Launcher — ensure PYTHONPATH includes toolkit root
import sys, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
"""
WatchROM — Interactive Terminal Launcher
Full TUI menu system for users without developer experience
Auto-runs: scan → backup → interactive menu
"""
import os
import sys
import subprocess
import time
import json
from pathlib import Path
# ── Ensure toolkit is on path ─────────────────────────────────────────────────
TOOLKIT_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(TOOLKIT_DIR))
try:
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.prompt import Prompt, Confirm
from rich.text import Text
from rich.live import Live
from rich.align import Align
from rich import box
from rich.columns import Columns
from rich.rule import Rule
except ImportError:
# Install to user site-packages — avoids breaking system packages
print("WatchROM needs the 'rich' library for the TUI menu.")
print("Attempting auto-install...")
ret = subprocess.run([sys.executable, "-m", "pip", "install",
"--user", "rich", "-q"])
if ret.returncode != 0:
print("Auto-install failed. Install manually:")
print(f" {sys.executable} -m pip install --user rich")
sys.exit(1)
# Re-import after install
try:
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.prompt import Prompt, Confirm
from rich.text import Text
from rich import box
from rich.rule import Rule
except ImportError as e:
print(f"Failed to import rich after install: {e}")
print("Install manually:")
print(f" {sys.executable} -m pip install --user rich")
sys.exit(1)
console = Console()
BANNER = """[bold cyan]
██╗ ██╗ █████╗ ████████╗ ██████╗██╗ ██╗██████╗ ██████╗ ███╗ ███╗
██║ ██║██╔══██╗╚══██╔══╝██╔════╝██║ ██║██╔══██╗██╔═══██╗████╗ ████║
██║ █╗ ██║███████║ ██║ ██║ ███████║██████╔╝██║ ██║██╔████╔██║
██║███╗██║██╔══██║ ██║ ██║ ██╔══██║██╔══██╗██║ ██║██║╚██╔╝██║
╚███╔███╔╝██║ ██║ ██║ ╚██████╗██║ ██║██║ ██║╚██████╔╝██║ ╚═╝ ██║
╚══╝╚══╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
[/bold cyan]"""
SESSION_FILE = TOOLKIT_DIR / ".session.json"
# ══════════════════════════════════════════════════════════════════════════════
# Helpers
# ══════════════════════════════════════════════════════════════════════════════
def run_cmd(args: list, interactive=False) -> tuple:
"""Run a watchrom sub-command."""
cmd = [sys.executable, str(TOOLKIT_DIR / "main.py")] + args
if interactive:
subprocess.run(cmd)
return 0, ""
try:
r = subprocess.run(cmd, capture_output=False, text=True, timeout=600)
return r.returncode, ""
except subprocess.TimeoutExpired:
return 1, "timeout"
def run_cmd_captured(args: list) -> tuple:
cmd = [sys.executable, str(TOOLKIT_DIR / "main.py")] + args
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
return r.returncode, r.stdout, r.stderr
except Exception as e:
return 1, "", str(e)
def adb_devices():
try:
r = subprocess.run(["adb", "devices"], capture_output=True, text=True, timeout=10)
devices = []
for line in r.stdout.splitlines()[1:]:
parts = line.split()
if len(parts) == 2 and parts[1] == "device":
devices.append(parts[0])
return devices
except Exception:
return []
def fastboot_devices():
try:
r = subprocess.run(["fastboot", "devices"], capture_output=True, text=True, timeout=5)
return [l.split()[0] for l in r.stdout.splitlines() if l.strip()]
except Exception:
return []
def get_device_info(serial=None):
"""Quick device info pull."""
try:
cmd = ["adb"]
if serial:
cmd += ["-s", serial]
r = subprocess.run(
cmd + ["shell", "getprop ro.product.model; getprop ro.product.device; "
"getprop ro.board.platform; getprop ro.build.version.release; "
"getprop ro.build.version.security_patch"],
capture_output=True, text=True, timeout=10
)
lines = [l.strip() for l in r.stdout.splitlines() if l.strip()]
return {
"model": lines[0] if len(lines) > 0 else "?",
"device": lines[1] if len(lines) > 1 else "?",
"platform": lines[2] if len(lines) > 2 else "?",
"android": lines[3] if len(lines) > 3 else "?",
"patch": lines[4] if len(lines) > 4 else "?",
}
except Exception:
return {}
def load_session():
if SESSION_FILE.exists():
try:
return json.loads(SESSION_FILE.read_text())
except Exception:
pass
return {}
def save_session(data: dict):
SESSION_FILE.write_text(json.dumps(data, indent=2))
def header(session: dict):
"""Print the top status bar."""
console.print(BANNER)
devs = adb_devices()
fbs = fastboot_devices()
if devs:
info = get_device_info(devs[0])
device_str = (f"[green]● {info.get('model','?')}[/green] "
f"[dim]{info.get('device','?')} | "
f"Android {info.get('android','?')} | "
f"{info.get('platform','?')}[/dim]")
elif fbs:
device_str = f"[yellow]● Fastboot: {fbs[0]}[/yellow]"
else:
device_str = "[red]○ No device connected[/red]"
backed_up = session.get("last_backup", None)
backup_str = (f"[green]✓ {backed_up}[/green]" if backed_up
else "[yellow]! No backup yet[/yellow]")
status_table = Table(box=box.SIMPLE, show_header=False,
padding=(0,2), expand=True)
status_table.add_column(style="dim")
status_table.add_column()
status_table.add_row("Device :", device_str)
status_table.add_row("Backup :", backup_str)
status_table.add_row("Session :", f"[dim]{SESSION_FILE.parent}[/dim]")
console.print(Panel(status_table, border_style="cyan", padding=(0,1)))
# ══════════════════════════════════════════════════════════════════════════════
# First-Run: Scan + Backup
# ══════════════════════════════════════════════════════════════════════════════
def first_run_scan(session: dict) -> dict:
"""Auto-scan device and offer backup on first run."""
console.print(Rule("[bold yellow]★ First Run — Device Scan[/bold yellow]"))
console.print()
devs = adb_devices()
if not devs:
console.print(Panel(
"[yellow]No device detected.[/yellow]\n\n"
"Please connect your watch via USB and enable:\n"
" Settings → About → Tap Build Number 7×\n"
" Settings → Developer Options → USB Debugging ON\n\n"
"Then press [bold]Enter[/bold] to retry.",
title="Connect Your Device",
border_style="yellow"
))
input()
devs = adb_devices()
if not devs:
console.print("[red]Still no device. Continuing without scan.[/red]")
return session
serial = devs[0]
console.print(f"[green]✓ Device found:[/green] [bold]{serial}[/bold]\n")
# Run device info
console.print("[cyan]→ Scanning device...[/cyan]")
run_cmd(["device", "info", "-s", serial], interactive=True)
console.print()
console.print(Panel(
"[bold yellow]⚠ IMPORTANT — Create a Backup Before Modifying Anything[/bold yellow]\n\n"
"WatchROM will now back up ALL partitions from your device.\n"
"This lets you restore to factory state if anything goes wrong.\n\n"
"[dim]This may take 5–20 minutes depending on device storage.[/dim]",
border_style="yellow",
padding=(1,2)
))
if Confirm.ask("\n [bold]Create a full backup now?[/bold]", default=True):
console.print("\n[cyan]→ Starting full backup (please wait)...[/cyan]\n")
rc, _ = run_cmd(["backup", "full", "-s", serial], interactive=True)
if rc == 0:
session["last_backup"] = time.strftime("%Y-%m-%d %H:%M")
session["device_serial"] = serial
save_session(session)
console.print(f"\n[bold green]✓ Backup complete![/bold green]")
else:
console.print(f"\n[yellow]! Backup encountered issues. Check output/backups/[/yellow]")
else:
console.print("[yellow]! Skipping backup — be careful with modifications.[/yellow]")
session["first_run_done"] = True
session["device_serial"] = serial
save_session(session)
console.print()
input(" Press Enter to continue to main menu...")
return session
# Legacy menu system deleted. Use 'watchrom <command>' from the terminal
# for all advanced operations. The guided mode below covers the 4 most
# common workflows: backup, root, bands, flash-rom.
# ══════════════════════════════════════════════════════════════════════════════
# Prompt helpers
# ══════════════════════════════════════════════════════════════════════════════
def prompt_file(label="File path", must_exist=True) -> str:
while True:
val = Prompt.ask(f" [cyan]{label}[/cyan]")
if not val:
return ""
p = Path(val.strip())
if must_exist and not p.exists():
console.print(f" [red]Not found: {p}[/red]")
continue
return str(p)
def prompt_serial() -> str:
devs = adb_devices()
if not devs:
console.print(" [red]No device connected.[/red]")
return ""
if len(devs) == 1:
return devs[0]
for i, d in enumerate(devs, 1):
console.print(f" {i}. {d}")
choice = Prompt.ask(" Select device", default="1")
try:
return devs[int(choice) - 1]
except Exception:
return devs[0]
def prompt_partition() -> str:
COMMON = ["boot","recovery","system","vendor","userdata","cache",
"vbmeta","dtbo","persist","modem","lk","preloader"]
console.print(" Common: " + " ".join(f"[cyan]{p}[/cyan]" for p in COMMON))
return Prompt.ask(" Partition name")
# ══════════════════════════════════════════════════════════════════════════════
# Guided "Caveman" Mode — Linear step-by-step workflow
# ══════════════════════════════════════════════════════════════════════════════
GUIDED_ACTIONS = [
("1", "📱 Back up my device",
"Full backup of all partitions + apps — always do this first"),
("2", "🔓 Root my device (Magisk)",
"Patch boot.img with Magisk and flash — requires backup first"),
("3", "📡 Configure cellular bands",
"Set LTE/5G bands for your carrier (Verizon, T-Mobile, AT&T, etc.)"),
("4", "💾 Flash a custom ROM",
"Flash all partitions from a directory — includes backup + safety checks"),
("5", "🛠 Expert mode — ALL features",
"Full menu: partitions, boot images, APK tools, keys, OTA, etc."),
("0", "❌ Exit WatchROM", ""),
]
def _device_status_line() -> tuple:
"""Return (status_string, adb_serial, fastboot_serial)."""
devs = adb_devices()
fbs = fastboot_devices()
if devs:
info = get_device_info(devs[0])
s = (f"[green]● {info.get('model','?')}[/green] "
f"[dim]{info.get('device','?')} | Android {info.get('android','?')} | "
f"{info.get('platform','?')}[/dim]")
return s, devs[0], None
elif fbs:
return f"[yellow]● Fastboot: {fbs[0]}[/yellow]", None, fbs[0]
return "[red]○ No device connected — plug in via USB and enable USB Debugging[/red]", None, None
def _require_device() -> tuple:
"""Ensure a device is connected; returns (adb_serial, fastboot_serial)."""
devs = adb_devices()
fbs = fastboot_devices()
if devs:
return devs[0], None
if fbs:
return None, fbs[0]
console.print(Panel(
"[yellow]No device detected.[/yellow]\n\n"
"Connect your watch via USB and enable:\n"
" Settings \u2192 About \u2192 Tap Build Number 7\u00d7\n"
" Settings \u2192 Developer Options \u2192 USB Debugging ON\n\n"
"Press [bold]Enter[/bold] to retry, or type [bold]back[/bold] to return.",
title="Device Required", border_style="yellow"))
choice = input().strip().lower()
if choice == "back":
return None, None
return _require_device()
def _confirm_destructive(purpose: str) -> bool:
"""Ask for confirmation before a potentially destructive operation."""
console.print()
console.print(Panel(
f"[bold yellow]\u26a0 {purpose}[/bold yellow]\n\n"
"This will modify your device. A backup is strongly recommended first.\n"
" [dim]Press Enter to continue, or type 'no' to cancel.[/dim]",
border_style="yellow", padding=(1, 2)))
console.print()
return Confirm.ask(" Proceed?", default=False)
def _run_guided_step(pipeline_args: list, step_name: str) -> int:
"""Run a pipeline step with progress feedback. Returns returncode."""
console.print(f"\n [cyan]\u2192 {step_name}...[/cyan]\n")
rc, _ = run_cmd(pipeline_args, interactive=True)
if rc == 0:
console.print(f" [bold green]\u2713 {step_name} completed[/bold green]")
else:
console.print(f" [yellow]! {step_name} had issues (see above)[/yellow]")
return rc
# ── Guided action: Full Backup ─────────────────────────────────────────────
def guided_backup(session: dict) -> dict:
"""Full backup walkthrough."""
console.clear()
console.print(f"\n[bold cyan]{'═'*58}[/bold cyan]")
console.print("[bold white] \U0001f4f1 Full Device Backup[/bold white]")
console.print(f"[bold cyan]{'═'*58}[/bold cyan]\n")
console.print(Panel(
"[bold]What this does:[/bold]\n"
" \u2022 Backs up ALL partitions (boot, system, vendor, etc.)\n"
" \u2022 Backs up your apps and app data\n"
" \u2022 Creates a restore manifest with SHA256 checksums\n\n"
"[bold yellow]This is the single most important safety step.[/bold yellow]\n"
"If anything goes wrong later, you can restore from this backup.\n\n"
"[dim]Estimated time: 5\u201320 minutes depending on device storage.[/dim]",
border_style="cyan", padding=(1, 2)))
adb_serial, fb_serial = _require_device()
if not adb_serial:
return session
info = get_device_info(adb_serial)
console.print(f"\n Device: [green]{adb_serial}[/green]"
f"{' ' + info.get('model','') if info.get('model') != '?' else ''}")
if not Confirm.ask("\n [bold]Start full backup now?[/bold]", default=True):
console.print(" [yellow]Backup cancelled.[/yellow]")
input(" Press Enter...")
return session
_run_guided_step(["pipeline", "full-backup", "--dry-run"], "Preview backup plan")
if not Confirm.ask("\n [bold]Run the full backup?[/bold]", default=True):
console.print(" [yellow]Backup cancelled.[/yellow]")
input(" Press Enter...")
return session
rc = _run_guided_step(["backup", "full", "-s", adb_serial], "Full backup running")
if rc == 0:
session["last_backup"] = time.strftime("%Y-%m-%d %H:%M")
session["device_serial"] = adb_serial
save_session(session)
console.print(f"\n [bold green]\u2713 Backup complete![/bold green]")
else:
console.print(f"\n [yellow]! Backup had issues. Check output/backups/[/yellow]")
input("\n Press Enter to return to menu...")
return session
# ── Guided action: Root Device ─────────────────────────────────────────────
def guided_root(session: dict) -> dict:
"""Root device with Magisk walkthrough."""
console.clear()
console.print(f"\n[bold cyan]{'═'*58}[/bold cyan]")
console.print("[bold white] \U0001f513 Root Device with Magisk[/bold white]")
console.print(f"[bold cyan]{'═'*58}[/bold cyan]\n")
console.print(Panel(
"[bold]What this does:[/bold]\n"
" 1. Checks device connection and battery level\n"
" 2. Backs up your stock boot.img (restore to unroot)\n"
" 3. Patches boot.img with Magisk\n"
" 4. Disables AVB verification (prevents boot loops)\n"
" 5. Flashes patched boot via fastboot\n"
" 6. Verifies root access\n\n"
"[bold yellow]Prerequisites:[/bold yellow]\n"
" \u2022 Bootloader must be UNLOCKED\n"
" \u2022 Magisk APK installed on device\n"
" \u2022 USB Debugging enabled\n\n"
"[dim]Time: ~5 minutes[/dim]",
border_style="cyan", padding=(1, 2)))
adb_serial, fb_serial = _require_device()
if not adb_serial:
return session
console.print(f"\n Device: [green]{adb_serial}[/green]")
last_bk = session.get("last_backup")
if not last_bk:
console.print(Panel(
"[bold yellow]\u26a0 No backup found![/bold yellow]\n\n"
"Rooting modifies your boot partition. Without a backup, you may not\n"
"be able to return to stock if something goes wrong.\n\n"
" [bold]Recommendation:[/bold] Create a backup first (option 1).",
border_style="yellow", padding=(1, 2)))
if not Confirm.ask("\n [bold]Proceed without backup?[/bold]", default=False):
console.print(" [yellow]Root cancelled. Create a backup first.[/yellow]")
input(" Press Enter...")
return session
console.print()
console.print(Panel(
"[bold yellow]\u26a0 Bootloader must be UNLOCKED[/bold yellow]\n\n"
" If your bootloader is locked, rooting will fail.\n"
" To unlock: enable OEM Unlock in Developer Options,\n"
" then: [bold]adb reboot bootloader && fastboot flashing unlock[/bold]\n\n"
" [dim]WARNING: Unlocking wipes all data on most devices.[/dim]",
border_style="yellow", padding=(1, 2)))
if not Confirm.ask("\n [bold]Start root process now?[/bold]", default=False):
console.print(" [yellow]Root cancelled.[/yellow]")
input(" Press Enter...")
return session
_run_guided_step(["pipeline", "root-device", "--dry-run"], "Preview root steps")
if not Confirm.ask(
"\n [bold]Run full root process? Device will reboot during flashing.[/bold]",
default=False):
console.print(" [yellow]Root cancelled.[/yellow]")
input(" Press Enter...")
return session
_run_guided_step(["pipeline", "root-device", "-s", adb_serial], "Rooting device")
console.print(f"\n [bold yellow]If the device does not boot:[/bold yellow]")
console.print(f" \u2022 Reboot to recovery ([bold]adb reboot recovery[/bold])")
console.print(f" \u2022 Flash stock boot.img from output/backups/")
input("\n Press Enter to return to menu...")
return session
# ── Guided action: Configure Bands ─────────────────────────────────────────
def guided_bands(session: dict) -> dict:
"""Configure cellular bands walkthrough."""
console.clear()
console.print(f"\n[bold cyan]{'═'*58}[/bold cyan]")
console.print("[bold white] \U0001f4e1 Configure Cellular Bands[/bold white]")
console.print(f"[bold cyan]{'═'*58}[/bold cyan]\n")
console.print(Panel(
"[bold]What this does:[/bold]\n"
" \u2022 Detects your device chipset (MTK, Unisoc, Qualcomm)\n"
" \u2022 Backs up current band configuration\n"
" \u2022 Applies band profile for your carrier\n"
" \u2022 Reboots the device\n\n"
"[bold yellow]Root is required[/bold yellow] for band configuration changes.\n\n"
"Choose from: Verizon, T-Mobile, AT&T, EU Generic,\n"
"Global Roaming (all bands), and more.",
border_style="cyan", padding=(1, 2)))
adb_serial, fb_serial = _require_device()
if not adb_serial:
return session
console.print(f"\n Device: [green]{adb_serial}[/green]")
console.print("\n [bold]Available carriers:[/bold]")
carriers = [
("1", "Verizon \u2014 Full (LTE + 5G + mmWave)"),
("2", "T-Mobile \u2014 Full bands"),
("3", "AT&T \u2014 Full bands"),
("4", "EU Generic \u2014 Europe multi-carrier"),
("5", "Global \u2014 All bands (restore defaults)"),
("6", "Verizon \u2014 LTE only (disable 5G)"),
("7", "T-Mobile \u2014 5G priority"),
("8", "Other carrier (show full list)"),
]
for num, label in carriers:
console.print(f" [{num}] {label}")
console.print()
choice = Prompt.ask(" Select carrier", default="5")
carrier_map = {
"1": "verizon", "2": "tmobile", "3": "att",
"4": "eu_generic", "5": "global_roaming",
"6": "verizon_lte", "7": "tmobile_5g",
}
carrier = carrier_map.get(choice)
if choice == "8":
_run_guided_step(["bands", "carriers"], "All carrier profiles")
carrier = Prompt.ask(" Enter carrier name from list")
elif not carrier:
carrier = Prompt.ask(" Enter carrier name", default="global_roaming")
if not carrier:
return session
if choice == "6":
cmd_args = ["bands", "verizon", "--tier", "lte-only"]
else:
cmd_args = ["pipeline", "configure-bands", "--carrier", carrier]
console.print(f"\n Selected: [bold]{carrier}[/bold]")
_run_guided_step(cmd_args + ["--dry-run"], "Preview band config")
if not Confirm.ask(f"\n [bold]Apply {carrier} band profile? Device will reboot.[/bold]",
default=False):
console.print(" [yellow]Band configuration cancelled.[/yellow]")
input(" Press Enter...")
return session
_run_guided_step(cmd_args, f"Applying {carrier} band profile")
input("\n Press Enter to return to menu...")
return session
# ── Guided action: Flash ROM ───────────────────────────────────────────────
def guided_flash_rom(session: dict) -> dict:
"""Flash a custom ROM walkthrough."""
console.clear()
console.print(f"\n[bold cyan]{'═'*58}[/bold cyan]")
console.print("[bold white] \U0001f4be Flash Custom ROM[/bold white]")
console.print(f"[bold cyan]{'═'*58}[/bold cyan]\n")
console.print(Panel(
"[bold yellow]\u26a0 WARNING: This modifies your device's firmware.[/bold yellow]\n\n"
"[bold]What this does:[/bold]\n"
" 1. Detects device and sets vendor-specific flash order\n"
" 2. Validates ROM images\n"
" 3. Creates a full backup before flashing\n"
" 4. Disables AVB verification\n"
" 5. Flashes all partitions in safe order\n\n"
"[bold]Prerequisites:[/bold]\n"
" \u2022 Bootloader UNLOCKED\n"
" \u2022 Directory containing .img partition files\n"
" \u2022 Battery at least 50% charged\n"
" \u2022 USB Debugging enabled\n\n"
"[dim]Time: 5\u201315 minutes[/dim]",
border_style="yellow", padding=(1, 2)))
adb_serial, fb_serial = _require_device()
if not adb_serial:
return session
console.print("\n [bold]ROM directory containing .img files:[/bold]")
console.print(" [dim](e.g., /path/to/extracted_ota/ or a WatchROM backup)[/dim]")
parts_dir = prompt_file("ROM directory", must_exist=True)
if not parts_dir:
return session
img_count = len(list(Path(parts_dir).glob("*.img")))
if img_count == 0:
console.print(Panel(f"[red]No .img files found in: {parts_dir}[/red]\n\n"
"The directory should contain partition images like:\n"
" boot.img, system.img, vendor.img, vbmeta.img, etc.",
border_style="red"))
input(" Press Enter...")
return session
console.print(f" [green]Found {img_count} .img files[/green]")
last_bk = session.get("last_backup")
if not last_bk:
console.print(Panel(
"[bold yellow]\u26a0 No backup found![/bold yellow]\n\n"
"Flashing a ROM is a destructive operation. Without a backup,\n"
"you may brick your device if something goes wrong.\n\n"
" [bold]Strongly recommend:[/bold] Create a backup first (option 1).",
border_style="red", padding=(1, 2)))
if not Confirm.ask("\n [bold]Proceed without backup?[/bold]", default=False):
console.print(" [yellow]ROM flash cancelled.[/yellow]")
input(" Press Enter...")
return session
console.print("\n [dim]\u2192 Previewing flash plan...[/dim]")
_run_guided_step(["pipeline", "flash-rom", "--parts-dir", parts_dir, "--dry-run"],
"Preview flash plan")
if not Confirm.ask("\n [bold]I understand the risks. Flash the ROM now?[/bold]",
default=False):
console.print(" [yellow]ROM flash cancelled.[/yellow]")
input(" Press Enter...")
return session
_run_guided_step(["pipeline", "flash-rom", "--parts-dir", parts_dir], "Flashing ROM")
console.print(f"\n [bold yellow]After flashing:[/bold yellow]")
console.print(f" \u2022 Device should reboot automatically")
console.print(f" \u2022 First boot may take 5\u201310 minutes")
console.print(f" \u2022 If stuck at boot logo:\n"
f" [bold]adb reboot recovery[/bold] \u2192 factory reset")
input("\n Press Enter to return to menu...")
return session
# ── Guided main loop ───────────────────────────────────────────────────────
def render_guided_menu(session: dict):
"""Render the guided (caveman) main menu."""
console.clear()
console.print(BANNER)
console.print(f"\n[bold cyan]{'═'*58}[/bold cyan]")
console.print("[bold white] \u231a WatchROM \u2014 Guided Mode[/bold white]")
console.print("[dim] Step-by-step workflow \u2014 no technical knowledge needed[/dim]")
console.print(f"[bold cyan]{'═'*58}[/bold cyan]\n")
status_line, adb_serial, fb_serial = _device_status_line()
console.print(f" Device: {status_line}")
last_bk = session.get("last_backup")
if last_bk:
console.print(f" Backup: [green]\u2713 {last_bk}[/green]")
else:
console.print(f" Backup: [yellow]! No backup yet[/yellow]")
console.print()
console.print(Rule("[bold]What would you like to do?[/bold]"))
console.print()
t = Table(box=box.SIMPLE, show_header=False, padding=(0, 2))
t.add_column("Key", style="bold yellow", width=6)
t.add_column("Action", style="bold white", width=32)
t.add_column("Description", style="dim", width=42)
for key, label, desc in GUIDED_ACTIONS:
t.add_row(f"[{key}]", label, desc)
console.print(t)
console.print()
def guided_main_flow() -> dict:
"""Guided (caveman) main loop."""
session = load_session()
if not session.get("first_run_done"):
console.clear()
console.print(BANNER)
console.print()
console.print(Panel(
"[bold yellow]Welcome to WatchROM![/bold yellow]\n\n"
"This guided mode helps you through the most common tasks.\n"
"All actions include safety checks, previews, and confirmations.\n\n"
"[dim]Type a number and press Enter to select an option.[/dim]",
border_style="yellow", padding=(1, 2)))
console.print()
if Confirm.ask(" Would you like to scan your device and create a backup?",
default=True):
session = first_run_scan(session)
else:
session["first_run_done"] = True
save_session(session)
while True:
render_guided_menu(session)
choice = Prompt.ask(" [bold yellow]Select[/bold yellow]", default="0").strip()
if choice == "0":
console.print(f"\n[bold cyan] Thanks for using WatchROM! \u2605[/bold cyan]\n")
break
elif choice == "1":
session = guided_backup(session)
elif choice == "2":
session = guided_root(session)
elif choice == "3":
session = guided_bands(session)
elif choice == "4":
session = guided_flash_rom(session)
elif choice == "5":
console.print("\n [cyan]\u2192 Switching to Expert Mode...[/cyan]\n")
time.sleep(1)
expert_main_flow(session)
continue
else:
console.print("[red] Invalid choice.[/red]")
time.sleep(0.8)
return session
def expert_main_flow(session: dict):
"""Expert mode: drops to CLI. Use 'watchrom <command>' for full control."""
console.print()
console.print(Panel(
"[bold yellow]Expert Mode[/bold yellow]\n\n"
"Use [bold]watchrom <command>[/bold] from the terminal for full control.\n\n"
"Examples:\n"
" watchrom pipeline root-device Root device\n"
" watchrom pipeline full-backup Full backup\n"
" watchrom device info Device scan\n"
" watchrom bootimg unpack boot.img Unpack boot image\n"
" watchrom --help All commands\n\n"
"Press [bold]Enter[/bold] to return to the guided menu.",
border_style="yellow", padding=(1, 2)))
input()
# ══════════════════════════════════════════════════════════════════════════════
# Entry point
# ══════════════════════════════════════════════════════════════════════════════
def main():
"""Entry point: runs guided mode by default."""
guided_main_flow()
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
console.print("\n\n[bold cyan] Goodbye! ★[/bold cyan]\n")