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.
847 lines
28 KiB
847 lines
28 KiB
#!/Users/gl6/env/bin/python
|
|
|
|
import os
|
|
import json
|
|
import requests
|
|
import uuid
|
|
import browser_cookie3
|
|
import time
|
|
from datetime import datetime
|
|
import re
|
|
from rich.console import Console
|
|
from rich.theme import Theme
|
|
|
|
STORAGE_FILE = os.path.join(os.path.expanduser('~'), '.workflowy_data.json')
|
|
|
|
INBOX_ID = "f5cf6b0b-2931-4a59-9301-0a52813e51e3"
|
|
TASKS_ID = "4f5fca00-8141-3f33-ac7a-b732f37515f4"
|
|
PLANNER_IDS = {
|
|
"life": "d0162605-db2d-7f97-f451-4408da2bb5df",
|
|
"admin": "3af78fcf-71d1-c020-26f1-a230b2403683",
|
|
"art": "d33c5cd2-6183-776f-33e8-c0714c8c5d63",
|
|
"health": "ab0ed672-7868-f38d-06c1-86c6464cd85b",
|
|
"mind": "09e9a2c7-954d-9178-7bca-de54d6f6680d",
|
|
"social": "2be5c4e5-ebfe-a462-3389-5b83aa86d016",
|
|
"church": "c69aa1a1-26a3-2ab7-a125-fe6b61d108bd",
|
|
"work": "f6306ec9-9243-a249-3af7-aa7d64963e2b",
|
|
"affirm": "a1412d52-c72c-0612-f5b1-48874ef03943",
|
|
"tabs": "63aa4d3a-4db3-9b8c-cbae-a49fca975c1e",
|
|
}
|
|
|
|
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",
|
|
"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",
|
|
})
|
|
|
|
console = Console(highlight=False, theme=solarized_theme)
|
|
|
|
# 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 generate_uuid():
|
|
return str(uuid.uuid4())
|
|
|
|
|
|
def load_storage():
|
|
if os.path.exists(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 None
|
|
|
|
|
|
def clear_storage():
|
|
if os.path.exists(STORAGE_FILE):
|
|
os.remove(STORAGE_FILE)
|
|
|
|
|
|
# }}}
|
|
|
|
|
|
def refresh_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:
|
|
console.log(f"Found session cookie: {session_cookie}")
|
|
save_to_storage("session_cookie", session_cookie)
|
|
return True
|
|
else:
|
|
console.log("Session cookie not found. Are you logged into Workflowy?")
|
|
return False
|
|
|
|
# }}}
|
|
|
|
|
|
def check_cookie(): # {{{
|
|
session_cookie = load_from_storage("session_cookie")
|
|
if session_cookie:
|
|
console.log(f"Session cookie found: {session_cookie}")
|
|
else:
|
|
console.log("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:
|
|
console.log("Session cookie is valid.")
|
|
return True
|
|
else:
|
|
console.log(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):
|
|
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", []):
|
|
child = filter_project_any(child, filters, include_headers=include_headers)
|
|
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): # {{{
|
|
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"]:
|
|
include = False
|
|
break
|
|
children = []
|
|
for child in project_data.get("children", []):
|
|
child = filter_project_all(child, filters, include_headers=include_headers)
|
|
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"])
|
|
children = project_data.get("children", [])
|
|
for child in children:
|
|
remove_double_spaces(child)
|
|
return project_data
|
|
# }}}
|
|
|
|
|
|
highlights = {
|
|
"@a": "red",
|
|
"@b": "blue",
|
|
"@c": "green",
|
|
"@d": "violet",
|
|
"@done": "green",
|
|
"@missed": "red",
|
|
"@na": "blue",
|
|
"@one": "base01",
|
|
"@two": "base01",
|
|
"@three": "base01",
|
|
"@five": "base01",
|
|
"@eight": "base01",
|
|
"@thirteen": "base01",
|
|
}
|
|
|
|
|
|
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 = {
|
|
"xnext": "blue",
|
|
"xtimed": "orange",
|
|
"xwhenever": "orange",
|
|
"xwaiting": "cyan",
|
|
"xmaybe": "violet",
|
|
"xrepeat": "green",
|
|
"xscheduled": "red",
|
|
"xblocked": "red",
|
|
"xvisualisation": "red",
|
|
"xactive": "underline magenta",
|
|
"xhold": "underline orange",
|
|
"xsprint": "violet",
|
|
"xstory": "cyan",
|
|
"xfuture": "yellow",
|
|
"xarchive": "underline base01",
|
|
"xsomeday": "underline violet"
|
|
}
|
|
|
|
colors2 = {
|
|
"@done": "strike",
|
|
"@missed": "strike",
|
|
"@na": "strike",
|
|
}
|
|
|
|
|
|
def recolor(project_data, colors): # {{{
|
|
for key, value in colors.items():
|
|
if key in project_data["name"]:
|
|
if key.startswith("x"):
|
|
project_data["name"] = f"[{value}]{project_data['name'].strip(" ")}[/] [base01]{key}[/]"
|
|
else:
|
|
project_data["name"] = f"[{value}]{project_data['name'].strip()}[/]"
|
|
break
|
|
# if project_data["format"] == "h1":
|
|
# project_data["name"] = f"[base3][underline]{project_data['name']}[/]"
|
|
# if project_data["format"] == "h2":
|
|
# project_data["name"] = f"[base1][underline]{project_data['name']}[/]"
|
|
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"]:
|
|
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): # {{{
|
|
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)
|
|
if filters_any is not None:
|
|
project_data = filter_project_any(project_data, filters_any, include_headers=include_headers)
|
|
project_data = replace(project_data, r" *<", "<")
|
|
project_data = replace(project_data, r" *$", "")
|
|
project_data = recolor(project_data, colors2)
|
|
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 = 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))
|
|
|
|
# }}}
|
|
|
|
|
|
def main(): # {{{
|
|
import argparse
|
|
parser = argparse.ArgumentParser(description="Workdlowy CLI")
|
|
parser.add_argument("--refresh", action="store_true", help="Refresh session cookie and Workflowy data")
|
|
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
|
|
inbox_parser = subparsers.add_parser("inbox", help="Inbox commands")
|
|
inbox_parser.add_argument("name", help="Item text", nargs="*")
|
|
|
|
tasks_parser = subparsers.add_parser("tasks", help="Tasks commands")
|
|
tasks_parser.add_argument("--hide-comments", action="store_true", help="Do not show comments")
|
|
tasks_parser.add_argument("--hide-headers", action="store_true", help="Hide headers")
|
|
tasks_parser.add_argument("--show-id", action="store_true", help="Show item id")
|
|
|
|
today_parser = subparsers.add_parser("today", help="Today commands")
|
|
today_parser.add_argument("filter", help="Filter text", nargs="*", default=None)
|
|
today_parser.add_argument("--hide-comments", action="store_true", help="Do not show comments")
|
|
today_parser.add_argument("--flat", action="store_true", help="Show flat list")
|
|
today_parser.add_argument("--hide-headers", action="store_true", help="Hide headers")
|
|
today_parser.add_argument("--show-id", action="store_true", help="Show item id")
|
|
|
|
subparsers.add_parser("dump", help="Dump storage")
|
|
subparsers.add_parser("refresh", help="Refresh session cookie and Workflowy data")
|
|
|
|
for planner_name, planner_id in PLANNER_IDS.items():
|
|
planner_parser = subparsers.add_parser(planner_name, help=f"{planner_name.capitalize()} commands")
|
|
planner_parser.add_argument("filter", help="Filter text", nargs="*", default=None)
|
|
planner_parser.add_argument("--hide-comments", action="store_true", help="Do not show comments")
|
|
planner_parser.add_argument("--flat", action="store_true", help="Show flat list")
|
|
planner_parser.add_argument("--hide-headers", action="store_true", help="Hide headers")
|
|
planner_parser.add_argument("--show-id", action="store_true", help="Show item id")
|
|
|
|
focus_parser = subparsers.add_parser("focus", help="Focus commands")
|
|
focus_parser.add_argument("id", help="Item id", nargs=1)
|
|
focus_parser.add_argument("--hide-comments", action="store_true", help="Do not show comments")
|
|
focus_parser.add_argument("--flat", action="store_true", help="Show flat list")
|
|
focus_parser.add_argument("--hide-headers", action="store_true", help="Hide headers")
|
|
focus_parser.add_argument("--show-id", action="store_true", help="Show item id")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.command == "refresh" or args.refresh:
|
|
refresh_cookie()
|
|
refresh_workflowy_data()
|
|
|
|
if args.command == "dump":
|
|
dump()
|
|
|
|
if args.command == "inbox":
|
|
if args.name:
|
|
name_text = ' '.join(args.name)
|
|
clip_to_workflowy(name_text, "", INBOX_ID)
|
|
else:
|
|
show(INBOX_ID)
|
|
|
|
if args.command == "tasks":
|
|
if args.filter:
|
|
if args.filter == ["today"]:
|
|
t = get_today()
|
|
show(TASKS_ID, filters_all=[t], flat=True, follow_mirrors=True, include_headers=True, show_description=not args.hide_comments, show_id=args.show_id)
|
|
else:
|
|
show(TASKS_ID, filters_all=args.filter, flat=True, follow_mirrors=True, include_headers=True, show_description=not args.hide_comments, show_id=args.show_id)
|
|
else:
|
|
show(TASKS_ID, follow_mirrors=True, show_description=not args.hide_comments, show_id=args.show_id)
|
|
|
|
if args.command == "today":
|
|
t = get_today()
|
|
|
|
|
|
|
|
for planner_name, PLANNER_ID in PLANNER_IDS.items():
|
|
if args.command == planner_name:
|
|
if args.filter:
|
|
show(PLANNER_ID, filters_all=args.filter, show_description=not args.hide_comments, flat=args.flat, include_headers=not args.hide_headers, show_id=args.show_id)
|
|
else:
|
|
show(PLANNER_ID, show_description=not args.hide_comments, flat=args.flat, include_headers=not args.hide_headers, show_id=args.show_id)
|
|
|
|
if args.command == "focus":
|
|
show(args.id[0], follow_mirrors=True, show_description=not args.hide_comments, flat=args.flat, include_headers=not args.hide_headers, show_id=args.show_id)
|
|
|
|
|
|
# }}}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|