From 64189dbdccadd3dcd65a31aff8bddbf6e93dc312 Mon Sep 17 00:00:00 2001 From: Gregory Leeman Date: Wed, 5 Mar 2025 16:19:14 +0000 Subject: [PATCH] lazygit --- workflowy-helper/.gitignore | 5 + workflowy-helper/requirements.txt | 5 + workflowy-helper/setup.py | 18 + workflowy-helper/wf/__init__.py | 0 workflowy-helper/wf/script.py | 951 ++++++++++++++++++++++++++++++ 5 files changed, 979 insertions(+) create mode 100644 workflowy-helper/.gitignore create mode 100644 workflowy-helper/requirements.txt create mode 100644 workflowy-helper/setup.py create mode 100644 workflowy-helper/wf/__init__.py create mode 100755 workflowy-helper/wf/script.py diff --git a/workflowy-helper/.gitignore b/workflowy-helper/.gitignore new file mode 100644 index 0000000..7d8a661 --- /dev/null +++ b/workflowy-helper/.gitignore @@ -0,0 +1,5 @@ +env/ +.env/ +venv/ +__pycache__/ +*.egg-info/ diff --git a/workflowy-helper/requirements.txt b/workflowy-helper/requirements.txt new file mode 100644 index 0000000..8a0b32b --- /dev/null +++ b/workflowy-helper/requirements.txt @@ -0,0 +1,5 @@ +browser-cookie3>=0.19.0 +rich>=13.8.0 +requests>=2.32.0 +uuid>=1.30 +click>=8.0.0 diff --git a/workflowy-helper/setup.py b/workflowy-helper/setup.py new file mode 100644 index 0000000..d0cae94 --- /dev/null +++ b/workflowy-helper/setup.py @@ -0,0 +1,18 @@ +from setuptools import setup + +with open('requirements.txt') as f: + requirements = f.read().splitlines() + +setup( + name='wf', + packages=['wf'], + install_requires=requirements, + description='A command line interface for using WorkFlowy.', + author='Gregory Leeman', + author_email='email@gregoryleeman.com', + entry_points={ + 'console_scripts': [ + 'wf = wf.script:cli' + ], + }, +) diff --git a/workflowy-helper/wf/__init__.py b/workflowy-helper/wf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/workflowy-helper/wf/script.py b/workflowy-helper/wf/script.py new file mode 100755 index 0000000..894c900 --- /dev/null +++ b/workflowy-helper/wf/script.py @@ -0,0 +1,951 @@ +#!/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"