app
fusion - the Fusion 360 bridge (macOS)
Public Made by Adomby adom
This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Desktop loads the bridge runtime itself from the release zip.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
"""Dismiss Fusion 360 recovery dialogs — macOS.
After a crash or force-kill, Fusion shows two types of recovery dialogs:
1. "Recovered Documents" list dialog (window titled "Fusion360"):
Shows all recovery files with Open/Delete/Close buttons.
Dismissed via the AX close button (mac_ui.close_window_bg).
2. "Open recovery document instead?" prompt:
Appears when opening a file that has a recovery document.
Has Yes/No buttons. We press Tab + Enter to choose No, skipping
recovery and opening the cloud version instead.
Recovery files are NOT deleted — they are moved to ~/.adom/recovery/fusion/
so the user can manually recover them if needed. Each batch is timestamped.
"""
import os
import shutil
import time
from handlers import mac_ui as _mac
def _get_fusion_crash_recovery_dirs() -> list:
"""Find all CrashRecovery directories under Fusion's app-support data.
Fusion stores recovery files at:
~/Library/Application Support/Autodesk/Autodesk Fusion (360)/<USER_ID>/CrashRecovery/
There may be multiple user ID folders. Returns list of existing
CrashRecovery dirs that contain at least one file.
"""
bases = [
os.path.expanduser("~/Library/Application Support/Autodesk/Autodesk Fusion 360"),
os.path.expanduser("~/Library/Application Support/Autodesk/Autodesk Fusion"),
]
dirs = []
for base in bases:
if not os.path.isdir(base):
continue
for entry in os.listdir(base):
cr_dir = os.path.join(base, entry, "CrashRecovery")
if os.path.isdir(cr_dir) and os.listdir(cr_dir):
dirs.append(cr_dir)
return dirs
def relocate_recovery_files() -> dict:
"""Move Fusion crash recovery files to ~/.adom/recovery/fusion/.
Instead of deleting recovery files (which removes a useful safety net),
we relocate them to Adom's own recovery folder. Each batch gets a
timestamped subfolder so the user can find and restore them if needed.
Returns dict with:
- moved: number of files/folders moved
- dest: destination directory (or None if nothing moved)
- sources: list of source CrashRecovery dirs that were processed
"""
cr_dirs = _get_fusion_crash_recovery_dirs()
if not cr_dirs:
return {"moved": 0, "dest": None, "sources": []}
# Create timestamped destination
timestamp = time.strftime("%Y%m%d_%H%M%S")
adom_home = os.path.join(os.path.expanduser("~"), ".adom")
dest_base = os.path.join(adom_home, "recovery", "fusion", timestamp)
os.makedirs(dest_base, exist_ok=True)
moved = 0
sources = []
for cr_dir in cr_dirs:
sources.append(cr_dir)
for item in os.listdir(cr_dir):
src = os.path.join(cr_dir, item)
dst = os.path.join(dest_base, item)
# Handle name collisions by appending a suffix
if os.path.exists(dst):
base_name, ext = os.path.splitext(item)
dst = os.path.join(dest_base, f"{base_name}_{moved}{ext}")
try:
shutil.move(src, dst)
moved += 1
except Exception:
# If move fails (file locked), try copy-then-delete
try:
if os.path.isdir(src):
shutil.copytree(src, dst)
else:
shutil.copy2(src, dst)
shutil.rmtree(src) if os.path.isdir(src) else os.remove(src)
moved += 1
except Exception:
pass # File may be locked by Fusion — skip it
# Clean up empty CrashRecovery dirs
for cr_dir in cr_dirs:
try:
if os.path.isdir(cr_dir) and not os.listdir(cr_dir):
os.rmdir(cr_dir)
except Exception:
pass
# If nothing was actually moved, remove the empty timestamp dir
if moved == 0:
try:
os.rmdir(dest_base)
except Exception:
pass
return {"moved": 0, "dest": None, "sources": sources}
return {"moved": moved, "dest": dest_base, "sources": sources}
def _find_window_by_title(title_substring: str) -> int:
"""CGWindowID of the first visible window whose title contains the substring, or 0."""
found = _mac.find_windows_by_title(title_substring)
return found[0][0] if found else 0
def _find_fusion_dialog_windows() -> list:
"""Fusion-owned non-main windows titled like the recovery list dialog."""
return [d["hwnd"] for d in _mac.find_dialog_windows()
if (d.get("title") or "").strip().lower()
in ("fusion360", "fusion", "autodesk fusion")]
def _send_key(winid: int, key: str, pause: float = 0.05):
"""Send a named key to a window. CGEvent keys land in the FOCUSED app
(there is no post-to-window on mac), so raise the target window first to
avoid typing into whatever the user has focused."""
if winid:
_mac.raise_window(winid)
time.sleep(0.1)
_mac.send_named_key(key)
time.sleep(pause)
def _dismiss_recovery_prompt(winid: int) -> bool:
"""Dismiss an 'Open recovery document instead?' dialog by choosing No.
The dialog has Yes (focused by default) and No buttons. Tab moves focus
to No, Enter clicks it — skipping recovery and opening the cloud version."""
_mac.raise_window(winid)
time.sleep(0.2)
_send_key(winid, "tab")
time.sleep(0.2)
_send_key(winid, "enter")
return True
def dismiss_recovery_dialog(max_wait: float = 5.0) -> bool:
"""Find and dismiss all Fusion 360 recovery dialogs.
Before dismissing, relocates recovery files from Fusion's CrashRecovery
directory to ~/.adom/recovery/fusion/<timestamp>/ so the user can
manually recover them if needed.
Handles the dialog types in priority order:
1. "Open recovery document instead?" — dismissed via Tab+Enter (No)
2. "Fusion360" titled dialog — the Recovered Documents list, closed in background
3. A window explicitly titled "Recovered Documents" — closed in background
4. Fallback: Send Escape to the Fusion main window
Loops to handle multiple recovery prompts (one per recovered file).
Returns True if any dismiss attempt was made.
"""
# Relocate recovery files BEFORE dismissing dialogs
# This preserves them in ~/.adom/recovery/fusion/ for the user
try:
reloc = relocate_recovery_files()
if reloc["moved"] > 0:
import logging
logging.getLogger(__name__).info(
"Relocated %d recovery file(s) to %s",
reloc["moved"], reloc["dest"]
)
except Exception:
pass # Don't let relocation failure block dialog dismissal
deadline = time.monotonic() + max_wait
dismissed_any = False
while time.monotonic() < deadline:
# Priority 1: "Open recovery document instead?" prompt
prompt_id = _find_window_by_title("Open recovery document instead?")
if prompt_id:
_dismiss_recovery_prompt(prompt_id)
dismissed_any = True
time.sleep(0.5)
continue # Check for more prompts
# Priority 2: "Fusion360" titled dialogs (Recovered Documents list)
dialog_ids = _find_fusion_dialog_windows()
if dialog_ids:
for winid in dialog_ids:
_mac.close_window_bg(winid)
dismissed_any = True
time.sleep(0.5)
continue # Check for more
# Priority 3: A window explicitly titled "Recovered Documents"
winid = _find_window_by_title("Recovered Documents")
if winid:
_mac.close_window_bg(winid)
dismissed_any = True
time.sleep(0.5)
continue
# If we already dismissed something, check one more time
if dismissed_any:
time.sleep(0.5)
# One final check
if not _find_window_by_title("Open recovery document instead?") \
and not _find_fusion_dialog_windows() \
and not _find_window_by_title("Recovered Documents"):
break
continue
# Priority 4: Send Escape to the main Fusion window (last resort)
fusion_id = _find_window_by_title("Autodesk Fusion")
if fusion_id:
_send_key(fusion_id, "escape")
dismissed_any = True
time.sleep(1.0)
continue
time.sleep(0.5)
return dismissed_any