#!/Users/gl6/env/bin/python import os import json import requests import uuid import browser_cookie3 import time from datetime import datetime, timedelta import re import click import logging from rich.console import Console from rich.theme import Theme from rich.logging import RichHandler STORAGE_FILE = os.path.join(os.path.expanduser('~'), '.wf.json') INBOX_ID = "13859f14-79ab-b758-7224-8dad0236f1e2" TASKS_ID = "042d7e11-f613-856c-fb97-c0e6ee7ece05" DIARY_ID = "01205285-f5b1-8026-0cfa-942f514e297e" PLANNER_IDS = { "life": "f6f993a3-c696-c070-296e-6e5055fc834f", "admin": "d3911123-138e-8eb3-f2ba-2495d8169660", "art": "c2e9f4b2-59ce-d127-4da6-949e54ec1442", "health": "4fc929b8-f04b-0dde-9a1a-9d889a13316d", "mind": "c6db70a1-72e3-dbcc-f4b9-bfb10a1b4280", "social": "60e3d667-eb40-3d26-cc3f-6b151cc5efa4", "church": "5c68fad5-ad2b-8018-6535-b1462aed1277", "work": "4c4970fc-9023-f861-1392-dbf88dd89187", "affirm": "a1412d52-c72c-0612-f5b1-48874ef03943", } solarized_theme = Theme({ "base03": "bright_black", "base02": "black", "base01": "bright_green", "base00": "bright_yellow", "base0": "bright_blue", "base1": "bright_cyan", "base2": "white", "base3": "bright_white", "orange": "bright_red", "violet": "bright_magenta", "red": "red", "bold red": "bold red", "underline base03": "underline bright_black", "underline base02": "underline black", "underline base01": "underline bright_green", "underline base00": "underline bright_yellow", "underline base0": "underline bright_blue", "underline base1": "underline bright_cyan", "underline base2": "underline white", "underline base3": "underline bright_white", "underline orange": "bright_red underline", "underline violet": "bright_magenta underline", "bold base03": "bold bright_black", "bold base02": "bold black", "bold base01": "bold bright_green", "bold base00": "bold bright_yellow", "bold base0": "bold bright_blue", "bold base1": "bold bright_cyan", "bold base2": "bold white", "bold base3": "bold bright_white", "bold orange": "bright_red bold", "bold violet": "bright_magenta bold", }) console = Console(highlight=False, theme=solarized_theme) logging.basicConfig(handlers=[RichHandler(level="NOTSET", console=console)]) logger = logging.getLogger('rich') logger.setLevel(logging.INFO) # helpers {{{ def get_ordinal(n): if 10 <= n % 100 <= 20: suffix = 'th' else: suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th') return str(n) + suffix def get_today(): now = datetime.now() return now.strftime("%a, %b %d, %Y") # return now.strftime(f"%a {get_ordinal(now.day)} %b") def get_sunday(): now = datetime.now() sunday = now - timedelta(days=now.weekday()) - timedelta(days=1) return sunday.strftime("%a, %b %d, %Y") # return now.strftime(f"%a {get_ordinal(now.day)} %b") def generate_uuid(): return str(uuid.uuid4()) def load_storage(): if os.path.exists(STORAGE_FILE): logger.debug(f"Loading storage from {STORAGE_FILE}") with open(STORAGE_FILE, "r") as f: return json.load(f) return {} def save_storage(data): with open(STORAGE_FILE, "w") as f: json.dump(data, f) def save_to_storage(key, value): storage = load_storage() storage[key] = value save_storage(storage) def load_from_storage(key): storage = load_storage() if key in storage: return storage[key] return {} def clear_storage(): if os.path.exists(STORAGE_FILE): os.remove(STORAGE_FILE) # }}} def refresh_cookie(): # {{{ logger.debug("Refreshing session cookie") cookies = browser_cookie3.chrome() session_cookie = None for cookie in cookies: if cookie.name == "sessionid" and "workflowy.com" in cookie.domain: session_cookie = cookie.value break if session_cookie: logger.debug(f"Found session cookie: {session_cookie}") save_to_storage("session_cookie", session_cookie) return True else: logger.error("Session cookie not found. Are you logged into Workflowy?") return False # }}} def check_cookie(): # {{{ session_cookie = load_from_storage("session_cookie") if session_cookie: logger.debug(f"Session cookie found: {session_cookie}") else: logger.error("Session cookie not found. Run refresh_cookie() first.") return False url = "https://workflowy.com/get_initialization_data?client_version=15" headers = {"Cookie": f"sessionid={session_cookie}"} response = requests.get(url, headers=headers) if response.status_code == 200: logger.debug("Session cookie is valid.") return True else: logger.error(f"Session cookie is invalid. Status code {response.status_code}") return False # }}} def refresh_workflowy_data(): # {{{ session_cookie = load_from_storage("session_cookie") if not session_cookie: console.log("Session cookie not found. Run refresh_cookie() first.") return url = "https://workflowy.com/get_initialization_data?client_version=15" headers = {"Cookie": f"sessionid={session_cookie}"} response = requests.get(url, headers=headers) if response.status_code == 200: try: data = response.json() globals_data = {item[0]: item[1] for item in data["globals"]} storage = load_storage() storage["userid"] = globals_data["USER_ID"] storage["joined"] = data["projectTreeData"]["mainProjectTreeInfo"][ "dateJoinedTimestampInSeconds" ] storage["transid"] = data["projectTreeData"]["mainProjectTreeInfo"][ "initialMostRecentOperationTransactionId" ] storage["pollid"] = generate_uuid() # Simulate g() + g() storage["root"] = {"nm": "root", "ch": data["projectTreeData"]["mainProjectTreeInfo"]["rootProjectChildren"]} save_storage(storage) console.log("Successfully refreshed and saved Workflowy data.") return True except Exception as e: console.log(f"Error parsing response: {e}") return False else: console.log(f"Error fetching Workflowy data: Status code {response.status_code}") return False # }}} def check_workflowy_data(): # {{{ storage = load_storage() if not storage: console.log("Workflowy data is not initialized. Run the initialization first.") return False if not storage.get("userid") or not storage.get("transid") or not storage.get( "pollid" ): console.log("Workflowy data is incomplete. Run the initialization again.") return False return True # }}} def clip_to_workflowy(name, description, parent_id): # {{{ storage = load_storage() if not storage: console.log("Workflowy data is not initialized. Run the initialization first.") return new_uuid = generate_uuid() timestamp = int(time.time()) - storage.get("joined", 0) request = [ { "most_recent_operation_transaction_id": storage.get("transid"), "operations": [ { "type": "create", "data": { "projectid": new_uuid, "parentid": parent_id, "priority": 9999, }, "client_timestamp": timestamp, "undo_data": {}, }, { "type": "edit", "data": { "projectid": new_uuid, "name": name, "description": description, }, "client_timestamp": timestamp, "undo_data": { "previous_last_modified": timestamp, "previous_name": "", "previous_description": "", }, }, ], } ] data = { "client_id": "2015-11-17 19:25:15.397732", "client_version": 15, "push_poll_id": storage.get("pollid"), "push_poll_data": json.dumps(request), "crosscheck_user_id": storage.get("userid"), } headers = {"Cookie": f"sessionid={storage.get('session_cookie')}"} response = requests.post("https://workflowy.com/push_and_poll", data=data, headers=headers) if response.status_code == 200: resp_obj = response.json() if resp_obj.get("logged_out"): console.log("Error: Logged out of Workflowy!") elif not resp_obj.get("results"): console.log("Error: Unknown error!") else: storage["transid"] = resp_obj["results"][0][ "new_most_recent_operation_transaction_id" ] save_storage(storage) console.print("[green]Successfully clipped to Workflowy![/green]") else: console.log(f"Error: Failed with status code {response.status_code}") # }}} def simplify_project(project_data, full_data, follow_mirrors=False): # {{{ if project_data.get("metadata", {}).get("mirror", {}).get("originalId"): if follow_mirrors: originalId = project_data["metadata"]["mirror"]["originalId"] project_data, full_data = find_project_by_id(full_data, full_data, originalId) else: return None, full_data if project_data.get("metadata", {}).get("isReferencesRoot"): if project_data.get("ch"): if len(project_data["ch"]) > 0: project_data["nm"] = "backlinks" project_data["metadata"]["layoutMode"] = "h1" else: return None, full_data else: return None, full_data if project_data: simplified_project = { "name": project_data.get("nm", ""), "id": project_data.get("id", ""), "children": [], "description": project_data.get("no", "").rstrip().replace("\n$", ""), "format": project_data.get("metadata", {}).get("layoutMode", None) } children = project_data.get("ch", []) for child in children: simplified_child, full_data = simplify_project(child, full_data, follow_mirrors=follow_mirrors) if simplified_child: simplified_project["children"].append(simplified_child) return simplified_project, full_data return None, full_data # }}} def flatten(children): # {{{ flattened = [] for child in children: try: grand_children = child.get("children", []) except Exception as e: print(f"child: {child} {e}") grand_children = [] child["children"] = [] flattened.append(child) flattened.extend(flatten(grand_children)) return flattened # }}} def flatten_project(project_data): # {{{ project_data["children"] = flatten(project_data.get("children", [])) return project_data # }}} # def filter_project_any(project_data, filters): # {{{ # filtered_project = { # "name": project_data["name"], # "id": project_data.get("id", ""), # # "description": project_data["description"], # "children": [], # "format": project_data["format"] # } # children = project_data.get("children", []) # for child in children: # include = False # for filter_text in filters: # if filter_text.lower() in child["name"].lower(): # include = True # break # if include: # filtered_project["children"].append(filter_project_any(child, filters)) # return filtered_project def filter_project_any(project_data, filters, include_headers=False, all_children=False): # {{{ include = False if include_headers: if project_data["format"] == "h1" or project_data["format"] == "h2": include = True for filter_text in filters: if filter_text in project_data["name"]: include = True break children = [] for child in project_data.get("children", []): if all_children and include: logger.debug(f"Not filtering children of {project_data['name']}") pass else: child = filter_project_any(child, filters, include_headers=include_headers, all_children=all_children) if child: children.append(child) project_data["children"] = children if include or children: return project_data else: return None # }}} def filter_project_all(project_data, filters, include_headers=False, all_children=False): # {{{ include = True if include_headers and (project_data["format"] == "h1" or project_data["format"] == "h2"): pass else: for filter_text in filters: if filter_text not in project_data["name"]: if filter_text not in project_data.get("description", ""): include = False break if include: logger.debug(f"Including {project_data['name']}") logger.debug(f"all_children: {all_children}") children = [] for child in project_data.get("children", []): if all_children and include: logger.debug(f"Not filtering children of {project_data['name']}") pass else: child = filter_project_all(child, filters, include_headers=include_headers, all_children=all_children) if child: children.append(child) project_data["children"] = children if include or children: return project_data else: return None # }}} def replace(project_data, regex, replacement): # {{{ project_data["name"] = re.sub(regex, replacement, project_data["name"]) children = project_data.get("children", []) for child in children: replace(child, regex, replacement) return project_data # }}} def strip(project_data, regex): # {{{ project_data = replace(project_data, regex, "") return project_data # }}} def remove_double_spaces(project_data): # {{{ # project_data["name"] = re.sub(r"\s+", " ", project_data["name"]) project_data["name"] = project_data["name"].replace(" ", " ") children = project_data.get("children", []) for child in children: remove_double_spaces(child) return project_data # }}} highlights = { "@done": "green", "@missed": "red", "@na": "blue", "#WORK": "red", } def highlight(project_data): # {{{ for key, value in highlights.items(): regex = f"{key}\\b" project_data["name"] = re.sub(regex, f"[{value}]{key}[/]", project_data["name"]) children = project_data.get("children", []) for child in children: highlight(child) return project_data # }}} colors1 = [ [["xTASK", "#READY"], "blue", True], [["xTASK", "#MAYBE"], "violet", True], [["xTASK", "#WAITING"], "cyan", True], [["xTASK", "#DAILY"], "green", True], [["xTASK", "#WEEKLY"], "green", True], [["xTASK", "#IRREGULAR"], "green", True], [["xPROJECT", "#ACTIVE"], "magenta", True], [["xPROJECT", "#STALLED"], "cyan", True], [["xPROJECT", "#PLANT"], "orange", True], [["xSOMEDAY"], "violet", True], [["xHABIT"], "orange", True], [["xSTORY"], "cyan", True], [["xGOAL"], "yellow", True], [["xVIS"], "red", True], [["xRESPONSIBILITY"], "red", True], [["Sunday"], "underline violet", False], [["Monday"], "underline red", False], [["Tuesday"], "underline cyan", False], [["Wednesday"], "underline magenta", False], [["Thursday"], "underline green", False], [["Friday"], "underline yellow", False], [["Saturday"], "underline blue", False], [["#r"], "black on blue", False], [["#g"], "black on yellow", False], [["#w"], "red", False], [["#p"], "green", False], ] def recolor(project_data, colors): # {{{ for rule in colors: keywords = rule[0] color = rule[1] hide = rule[2] match = True for keyword in keywords: if keyword not in project_data["name"]: match = False break if match: project_data["name"] = f"[{color}]{project_data['name']}[/]" if hide: for keyword in keywords: project_data["name"] = project_data["name"].replace(keyword, "") project_data["name"] = project_data["name"].rstrip() + f" [base01]{" ".join(keywords)}[/]" children = project_data.get("children", []) for child in children: recolor(child, colors) return project_data # }}} def print_pretty(data, indent=0, color="grey", show_description=True, show_id=False): # {{{ try: for item in data["children"]: if item["name"] == "backlinks": # console.print(" " * indent + "[base01]• backlinks[/]") continue console.print(" " * indent + f"[base3]•[/] [{color}]{item['name']}[/][base01]{' ' + item['id'].split('-')[4] if show_id else ''}[/]") if item["description"] and show_description: console.print(" " * (indent + 1) + f"[base01]{item['description'].replace('\n', '\n' + ' ' * (indent + 1))}[/]") if item["children"]: print_pretty(item, indent + 1, color, show_description=show_description, show_id=show_id) except Exception as e: console.log(f"Error: {e}") # }}} def generate_d3_mindmap_html(data, show_description=True, show_id=False): # {{{ import json import re # Map of Rich tag colors to hex values or CSS color names color_map = { "base3": "#073642", "base2": "#002b36", "base1": "#586e75", "base0": "#657b83", "base00": "#839496", "base01": "#93a1a1", "base02": "#eee8d5", "base03": "#fdf6e3", "yellow": "#b58900", "orange": "#cb4b16", "red": "#dc322f", "magenta": "#d33682", "violet": "#6c71c4", "blue": "#268bd2", "cyan": "#2aa198", "green": "#859900", } def parse_rich_tags(text): text = re.sub(r"\[(underline)? ?([a-zA-Z0-9]+)?\](.*?)\[/\]", lambda m: f'{m.group(3)}', text) return text def remove_rich_tags(text): text = re.sub(r"\[(underline)? ?([a-zA-Z0-9]+)?\](.*?)\[/\]", r"\3", text) return text # Recursively transform data to add the parsed Rich-style text def transform_data(item): node = { "name": remove_rich_tags(parse_rich_tags(item["name"])), # Parse Rich tags in name "id": item["id"].split("-")[4] if show_id else "", "description": parse_rich_tags(item["description"]) if show_description else "" } if item["children"]: node["children"] = [transform_data(child) for child in item["children"]] return node transformed_data = transform_data(data) # Create the HTML content with D3.js code html_content = f""" Mind Map """ # Write the HTML content to a file with open("d3_mindmap.html", "w") as f: f.write(html_content) print("D3.js mind map HTML generated as 'd3_mindmap.html'.") # }}} def find_project_by_id(project_data, full_data, target_id): # {{{ if project_data.get("id"): if target_id in project_data.get("id"): return project_data, full_data for child in project_data.get("ch", []): result, full_data = find_project_by_id(child, full_data, target_id) if result: return result, full_data return None, full_data # }}} def show(parent_id, flat=False, filters_all=None, filters_any=None, color="grey", follow_mirrors=False, include_headers=False, show_description=True, show_id=False, all_children=False): # {{{ root_data = load_from_storage("root") project_data, root_data = find_project_by_id(root_data, root_data, parent_id) project_data, root_data = simplify_project(project_data, root_data, follow_mirrors=follow_mirrors) if flat: project_data = flatten_project(project_data) if filters_all is not None: project_data = filter_project_all(project_data, filters_all, include_headers=include_headers, all_children=all_children) if filters_any is not None: project_data = filter_project_any(project_data, filters_any, include_headers=include_headers, all_children=all_children) project_data = replace(project_data, r" *<", "<") project_data = replace(project_data, r" *$", "") project_data = recolor(project_data, colors1) project_data = strip(project_data, r".*") project_data = strip(project_data, r"