From 0e1e54efc359431dccb470ffab71efccfbf0c825 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Fri, 30 Jan 2026 18:17:49 +0100 Subject: [PATCH] Add firefox-tabs.py script --- scripts/firefox-tabs.py | 476 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 476 insertions(+) create mode 100755 scripts/firefox-tabs.py diff --git a/scripts/firefox-tabs.py b/scripts/firefox-tabs.py new file mode 100755 index 0000000..2bf229a --- /dev/null +++ b/scripts/firefox-tabs.py @@ -0,0 +1,476 @@ +#!/usr/bin/env python3 +""" +Script to get information about Firefox's open tabs. +Works with Firefox installed via Nix/Home Manager. + +Usage: + python3 firefox-tabs.py [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: python3 firefox-tabs.py [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(" python3 firefox-tabs.py # Use session files (default)") + print(" python3 firefox-tabs.py session # Use session files") + print(" python3 firefox-tabs.py rdp # Use RDP on default port 6000") + print(" python3 firefox-tabs.py rdp 9222 # Use RDP on port 9222") + print(" python3 firefox-tabs.py both # Try both methods") + sys.exit(1) + + +if __name__ == "__main__": + main() +