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

#!/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()