You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
932 lines
30 KiB
932 lines
30 KiB
#!/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",
|
|
}
|
|
|
|
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, 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 = []
|
|
logger.debug(f"children: {project_data.get('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'<tspan style="{"text-decoration: underline;" if m.group(1) else ""}'
|
|
f'{"fill: " + color_map[m.group(2)] + ";" if m.group(2) else ""}">{m.group(3)}</tspan>',
|
|
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"""
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Mind Map</title>
|
|
<script src="https://d3js.org/d3.v6.min.js"></script>
|
|
<style>
|
|
body {{
|
|
font-family: Arial, sans-serif;
|
|
background-color: #073642;
|
|
color: #fdf6e3;
|
|
}}
|
|
.node {{
|
|
cursor: pointer;
|
|
}}
|
|
.node circle {{
|
|
fill: #586e75;
|
|
stroke: #586e75;
|
|
stroke-width: 3px;
|
|
}}
|
|
.node text {{
|
|
# color: #fdf6e3 !important;
|
|
fill: #fdf6e3;
|
|
font: 12px sans-serif;
|
|
}}
|
|
.link {{
|
|
fill: none;
|
|
stroke: #586e75;
|
|
stroke-width: 2px;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<svg width="100vw" height="100vh"></svg>
|
|
|
|
<script>
|
|
var data = {json.dumps(transformed_data)};
|
|
|
|
var svg = d3.select("svg")
|
|
.attr("width", "100vw")
|
|
.attr("height", "100vh");
|
|
|
|
var zoom = d3.zoom()
|
|
.scaleExtent([0.5, 5])
|
|
.on("zoom", function(event) {{
|
|
g.attr("transform", "translate(" + event.transform.x + "," + event.transform.y + ") scale(" + event.transform.k + ") rotate(" + currentRotation + ")");
|
|
currentTransform = event.transform;
|
|
}});
|
|
|
|
svg.call(zoom);
|
|
|
|
var g = svg.append("g");
|
|
|
|
// .attr("transform", "translate(" + window.innerWidth / 2 + "," + window.innerHeight / 2 + ")");
|
|
|
|
var currentRotation = 0;
|
|
var currentTransform = d3.zoomIdentity;
|
|
var isRotating = false;
|
|
var lastX = 0;
|
|
|
|
document.addEventListener("keydown", function(event) {{
|
|
if (event.key === "r" || event.key === "R") {{
|
|
isRotating = !isRotating;
|
|
}}
|
|
}});
|
|
|
|
svg.on("mousedown", function(event) {{
|
|
if (isRotating) {{
|
|
lastX = event.clientX;
|
|
}}
|
|
}});
|
|
|
|
svg.on("mousemove", function(event) {{
|
|
if (isRotating) {{
|
|
var dx = event.clientX - lastX;
|
|
currentRotation += dx * 0.1;
|
|
g.attr("transform", "translate(" + currentTransform.x + "," + currentTransform.y + ") scale(" + currentTransform.k + ") rotate(" + currentRotation + ")");
|
|
lastX = event.clientX;
|
|
}}
|
|
}});
|
|
|
|
svg.on("mouseup", function() {{
|
|
isRotating = false;
|
|
}});
|
|
|
|
var tree = d3.tree()
|
|
.size([360, Math.min(window.innerWidth, window.innerHeight) / 2 - 100])
|
|
.separation(function(a, b) {{ return (a.parent === b.parent ? 1 : 2) / a.depth; }});
|
|
|
|
var root = d3.hierarchy(data);
|
|
|
|
tree(root);
|
|
|
|
var link = g.selectAll(".link")
|
|
.data(root.links())
|
|
.enter().append("path")
|
|
.attr("class", "link")
|
|
.attr("d", d3.linkRadial()
|
|
.angle(function(d) {{ return d.x / 180 * Math.PI; }})
|
|
.radius(function(d) {{ return d.y; }}));
|
|
|
|
var node = g.selectAll(".node")
|
|
.data(root.descendants())
|
|
.enter().append("g")
|
|
.attr("class", "node")
|
|
.attr("transform", function(d) {{ return "translate(" + radialPoint(d.x, d.y) + ")"; }});
|
|
|
|
node.append("circle")
|
|
.attr("r", 4.5);
|
|
|
|
node.append("text")
|
|
.attr("dy", ".31em")
|
|
.attr("x", 10)
|
|
.attr("text-anchor", "start")
|
|
.attr("transform", function(d) {{
|
|
return "rotate(" + (d.x - 90) + ") translate(10, 0)";
|
|
}})
|
|
.html(function(d) {{
|
|
return d.data.name;
|
|
}});
|
|
|
|
function radialPoint(x, y) {{
|
|
return [(y = +y) * Math.cos((x - 90) / 180 * Math.PI), y * Math.sin((x - 90) / 180 * Math.PI)];
|
|
}}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
|
|
|
|
# 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"<a href=\".*\">.*</a>")
|
|
project_data = strip(project_data, r"<time .*</time>")
|
|
project_data = strip(project_data, r"<[^>]*>")
|
|
project_data = strip(project_data, r"</[^>]*>")
|
|
project_data = highlight(project_data)
|
|
# project_data = recolorBacklinks(project_data)
|
|
project_data = remove_double_spaces(project_data)
|
|
# console.print(f"\n[base3][bold]{project_data['name']}[/][/]")
|
|
# if project_data.get("description") and show_description:
|
|
# console.print(f"[base01]{project_data['description']}[/]")
|
|
console.print("")
|
|
print_pretty(project_data, color=color, show_description=show_description, show_id=show_id)
|
|
console.print("")
|
|
# generate_d3_mindmap_html(project_data, show_description=show_description, show_id=show_id)
|
|
return True
|
|
|
|
# }}}
|
|
|
|
|
|
def dump(): # {{{
|
|
storage = load_storage()
|
|
print(json.dumps(storage, indent=2))
|
|
|
|
# }}}
|
|
|
|
|
|
@click.group(invoke_without_command=True)
|
|
@click.option("--refresh", is_flag=True, help="Refresh session cookie and Workflowy data")
|
|
@click.option("--debug", is_flag=True, help="Enable debug mode")
|
|
@click.pass_context
|
|
def cli(ctx, refresh, debug):
|
|
"""
|
|
Workdlowy CLI
|
|
"""
|
|
if debug:
|
|
logger.setLevel(logging.DEBUG)
|
|
logger.debug("Debug mode enabled")
|
|
if refresh:
|
|
refresh_cookie()
|
|
refresh_workflowy_data()
|
|
if ctx.invoked_subcommand is None:
|
|
click.echo(ctx.get_help())
|
|
|
|
|
|
@cli.command()
|
|
@click.argument("name", nargs=-1)
|
|
def inbox(name):
|
|
"""Inbox commands"""
|
|
if name:
|
|
name_text = ' '.join(name)
|
|
clip_to_workflowy(name_text, "", INBOX_ID)
|
|
else:
|
|
show(INBOX_ID)
|
|
|
|
|
|
@cli.command()
|
|
@click.option("--hide-comments", is_flag=True, help="Do not show comments")
|
|
@click.option("--hide-headers", is_flag=True, help="Hide headers")
|
|
@click.option("--show-id", is_flag=True, help="Show item id")
|
|
@click.argument("filter", nargs=-1)
|
|
def tasks(filter, hide_comments, hide_headers, show_id):
|
|
"""Tasks commands"""
|
|
if filter:
|
|
show(
|
|
TASKS_ID,
|
|
filters_all=filter,
|
|
flat=True,
|
|
follow_mirrors=True,
|
|
include_headers=True,
|
|
show_description=not hide_comments,
|
|
show_id=show_id,
|
|
)
|
|
else:
|
|
show(
|
|
TASKS_ID,
|
|
follow_mirrors=True,
|
|
show_description=not hide_comments,
|
|
show_id=show_id,
|
|
)
|
|
|
|
|
|
@cli.command()
|
|
@click.argument("filter", nargs=-1)
|
|
def today(filter):
|
|
"""Today commands"""
|
|
t = get_today()
|
|
logger.debug(f"Today: {t}")
|
|
if filter:
|
|
show(
|
|
TASKS_ID,
|
|
filters_all=[t] + list(filter),
|
|
flat=False,
|
|
follow_mirrors=True,
|
|
include_headers=True,
|
|
show_description=True,
|
|
show_id=False,
|
|
)
|
|
else:
|
|
show(
|
|
TASKS_ID,
|
|
filters_all=[t],
|
|
flat=False,
|
|
follow_mirrors=True,
|
|
include_headers=True,
|
|
show_description=True,
|
|
show_id=False,
|
|
)
|
|
|
|
|
|
@cli.command()
|
|
def week():
|
|
"""Today commands"""
|
|
sunday = get_sunday()
|
|
logger.debug(f"Sunday: {sunday}")
|
|
show(
|
|
DIARY_ID,
|
|
filters_all=[sunday],
|
|
flat=False,
|
|
follow_mirrors=True,
|
|
include_headers=True,
|
|
show_description=False,
|
|
show_id=False,
|
|
all_children=True
|
|
)
|
|
|
|
|
|
@cli.command(name="dump")
|
|
def dump_cmd():
|
|
"""Dump storage"""
|
|
dump()
|
|
|
|
|
|
@cli.command(name="refresh")
|
|
def refresh_cmd():
|
|
"""Refresh session cookie and Workflowy data"""
|
|
refresh_cookie()
|
|
refresh_workflowy_data()
|
|
|
|
|
|
def create_planner_command(planner_name, planner_id):
|
|
@click.command(name=planner_name, help=f"{planner_name.capitalize()} commands")
|
|
@click.option("--hide-comments", is_flag=True, help="Do not show comments")
|
|
@click.option("--flat", is_flag=True, help="Show flat list")
|
|
@click.option("--hide-headers", is_flag=True, help="Hide headers")
|
|
@click.option("--show-id", is_flag=True, help="Show item id")
|
|
@click.argument("filter", nargs=-1)
|
|
def planner(filter, hide_comments, flat, hide_headers, show_id):
|
|
if filter:
|
|
show(
|
|
planner_id,
|
|
filters_all=filter,
|
|
show_description=not hide_comments,
|
|
flat=flat,
|
|
include_headers=not hide_headers,
|
|
show_id=show_id,
|
|
)
|
|
else:
|
|
show(
|
|
planner_id,
|
|
show_description=not hide_comments,
|
|
flat=flat,
|
|
include_headers=not hide_headers,
|
|
show_id=show_id,
|
|
)
|
|
return planner
|
|
|
|
|
|
for planner_name, planner_id in PLANNER_IDS.items():
|
|
cli.add_command(create_planner_command(planner_name, planner_id))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
cli()
|
|
|