#!/usr/bin/env python3 """ f-around-firefox (faf) - Get information about Firefox's open tabs. Works with Firefox installed via Nix/Home Manager. Usage: faf [method] [rdp_port] Methods: session, s - Read from Firefox session files (default) rdp, r - Use Remote Debugging Protocol both, b - Try both methods """ import json import sys import os import struct import subprocess from pathlib import Path from urllib.request import urlopen from urllib.error import URLError # Try to find Nix Python with lz4 if available def find_nix_python(): """Try to find a Nix Python with lz4 library.""" # Common Nix store paths nix_store = Path("/nix/store") if not nix_store.exists(): return None # Look for Python with lz4 in the store # This is a heuristic - we look for python* directories that might have lz4 try: result = subprocess.run( ["which", "python3"], capture_output=True, text=True ) python_path = result.stdout.strip() if python_path and "/nix/store" in python_path: # Check if this Python has lz4 result = subprocess.run( [python_path, "-c", "import lz4.frame; print('ok')"], capture_output=True, text=True ) if result.returncode == 0: return python_path except: pass return None # Colors for output RED = '\033[0;31m' GREEN = '\033[0;32m' YELLOW = '\033[1;33m' BLUE = '\033[0;34m' NC = '\033[0m' # No Color def find_firefox_profile(): """Find the default Firefox profile directory.""" if sys.platform == "darwin": profile_dir = Path.home() / "Library/Application Support/Firefox" elif sys.platform.startswith("linux"): profile_dir = Path.home() / ".mozilla/firefox" else: print(f"{RED}Unsupported OS: {sys.platform}{NC}", file=sys.stderr) return None profiles_ini = profile_dir / "profiles.ini" if not profiles_ini.exists(): print(f"{RED}Firefox profiles.ini not found at: {profiles_ini}{NC}", file=sys.stderr) print(f"{YELLOW}Make sure Firefox has been run at least once.{NC}", file=sys.stderr) return None # Find the default profile # Parse profiles.ini by sections profile_path = None current_section = None current_path = None is_default = False with open(profiles_ini, 'r') as f: for line in f: line = line.strip() if line.startswith('[') and line.endswith(']'): # Save previous section if it was default if is_default and current_path: profile_path = current_path break # Start new section current_section = line current_path = None is_default = False elif line.startswith("Path="): current_path = line.split("=", 1)[1].strip() elif line == "Default=1": is_default = True # Check if last section was default if is_default and current_path and not profile_path: profile_path = current_path # Fallback: get the first profile with Path= if not profile_path: with open(profiles_ini, 'r') as f: for line in f: if line.startswith("Path="): profile_path = line.split("=", 1)[1].strip() break if not profile_path: return None if Path(profile_path).is_absolute(): return Path(profile_path) else: return profile_dir / profile_path def decompress_lz4(file_path): """Decompress a Mozilla lz4 file and return the JSON content. Firefox uses a custom format: - 8 bytes: "mozLz40\0" header - 4 bytes: uncompressed size (little-endian uint32) - rest: lz4 frame compressed data """ # Try Python lz4 library first (supports Mozilla format) # First try with current Python try: import lz4.block import struct with open(file_path, 'rb') as f: # Skip the 8-byte Mozilla header header = f.read(8) if header[:7] == b'mozLz40': # Read the 4-byte uncompressed size (little-endian uint32) size_bytes = f.read(4) if len(size_bytes) == 4: uncompressed_size = struct.unpack(' 1 else "session" rdp_port = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[1] in ["rdp", "r", "both", "b"] else 6000 if method in ["session", "s"]: print(f"{GREEN}Getting tabs from session files...{NC}") profile_dir = find_firefox_profile() if profile_dir: tabs_info = get_tabs_from_session(profile_dir) if tabs_info: print_tabs(tabs_info, "session") else: sys.exit(1) elif method in ["rdp", "r"]: print(f"{GREEN}Fetching tabs via Remote Debugging Protocol...{NC}") tabs_info = get_tabs_via_rdp(rdp_port) if tabs_info: print_tabs(tabs_info, "rdp") elif method in ["both", "b"]: print(f"{GREEN}=== Session Files Method ==={NC}") profile_dir = find_firefox_profile() if profile_dir: tabs_info = get_tabs_from_session(profile_dir) if tabs_info: print_tabs(tabs_info, "session") print(f"\n{GREEN}=== Remote Debugging Protocol Method ==={NC}") tabs_info = get_tabs_via_rdp(rdp_port) if tabs_info: print_tabs(tabs_info, "rdp") else: print("Usage: faf [method] [rdp_port]") print("") print("Methods:") print(" session, s - Read from Firefox session files (default)") print(" rdp, r - Use Remote Debugging Protocol") print(" both, b - Try both methods") print("") print("Examples:") print(" faf # Use session files (default)") print(" faf session # Use session files") print(" faf rdp # Use RDP on default port 6000") print(" faf rdp 9222 # Use RDP on port 9222") print(" faf both # Try both methods") sys.exit(1) if __name__ == "__main__": main()