💾 Archived View for gemini.ctrl-c.club › ~nttp › toys › outnoted › outnoted.py captured on 2023-11-04 at 14:28:20.
⬅️ Previous capture (2023-03-20)
-=-=-=-=-=-=-
#!/usr/bin/env python3 # coding=utf-8 # # OutNoted: an outline note-taking editor # Copyright 2021, 2022 Felix Pleșoianu <https://felix.plesoianu.ro/> # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. from __future__ import print_function import webbrowser import os.path import json import sys if sys.version_info.major >= 3: from tkinter import * from tkinter import ttk from tkinter.simpledialog import askstring from tkinter.messagebox import showerror, askyesno from tkinter.filedialog import askopenfilename, asksaveasfilename else: from Tkinter import * import ttk from tkSimpleDialog import askstring from tkMessageBox import showerror, askyesno from tkFileDialog import askopenfilename, asksaveasfilename import re import xml.dom.minidom from xml.parsers.expat import ExpatError class MarkupParser(object): def __init__(self, lines, headChar): self.lines = lines self.metadata = {"title": ""} self.cursor = 0 self.headChar = headChar self.headline = None def parseMeta(self): return self.metadata # Most formats lack inherent metadata. def skipSection(self): if self.cursor >= len(self.lines): return None # body = [] while self.lines[self.cursor][0] != self.headChar: # body.push(this.lines[this.cursor]); self.cursor += 1 if self.cursor >= len(self.lines): break; # return body def matchHeadline(self, level = 0): if self.cursor >= len(self.lines): return False for i in range(level): if self.lines[self.cursor][i] != self.headChar: return False self.headline = self.lines[self.cursor][i + 1:].strip() self.cursor += 1 return True class OrgParser(MarkupParser): re_meta = re.compile("^\s*#\+([A-Z]+):(.*)$", re.IGNORECASE) re_status = re.compile(r"^(TODO|NEXT|DONE)\b(.*)$") def __init__(self, lines): super(OrgParser, self).__init__(lines, '*') def parseMeta(self): while self.cursor < len(self.lines): ln = self.lines[self.cursor] m = self.re_meta.match(ln) if m != None: key = m.group(1).strip() value = m.group(2).strip() self.metadata[key] = value else: break self.cursor += 1 return self.metadata def parseMarkup(parser, level = 0): parser.skipSection() subnodes = [] while parser.matchHeadline(level + 1): node = { "text": parser.headline, "children": parseMarkup(parser, level + 1) } subnodes.append(node) return subnodes def parseStatus(outline): for i in outline: m = OrgParser.re_status.match(i["text"]) if m != None: i["status"] = m.group(1) i["text"] = m.group(2).strip() parseStatus(i["children"]) return outline class OPMLoader: def __init__(self, source): data = xml.dom.minidom.parse(source) self.head = data.getElementsByTagName("head")[0] self.body = data.getElementsByTagName("body")[0] self.metadata = {"title": ""} def parseMeta(self): for i in self.head.childNodes: if i.nodeType == i.ELEMENT_NODE: text = i.firstChild.nodeValue self.metadata[i.nodeName] = text return self.metadata def parseOPML(node): subnodes = [] for i in node.childNodes: if i.nodeType == i.ELEMENT_NODE: node = { "text": i.getAttribute("text"), "children": parseOPML(i) } if i.hasAttribute("url"): node["link"] = i.getAttribute("url") elif i.hasAttribute("htmlUrl"): node["link"] = i.getAttribute("htmlUrl") subnodes.append(node) return subnodes def printMarkup(f, outline, headChar, level = 1): for i in outline: print(headChar * level, i["text"], file=f) printMarkup(f, i["children"], headChar, level + 1) def printOrgMarkup(f, outline, level = 1): for i in outline: if "status" in i and i["status"] != "": print('*' * level, i["status"], i["text"], file=f) else: print('*' * level, i["text"], file=f) printOrgMarkup(f, i["children"], level + 1) def buildOutline(document, parent, outline): for i in outline: node = document.createElement("outline"); node.setAttribute("text", i["text"]) if "link" in i and i["link"] != "": node.setAttribute("type", "link") node.setAttribute("url", i["link"]) buildOutline(document, node, i["children"]) parent.appendChild(node) def buildOPML(metadata, outline): document = xml.dom.minidom.parseString( "<opml version='2.0'><head></head><body></body></opml>") head = document.getElementsByTagName("head")[0] if "title" in metadata: title = document.createElement("title") title.appendChild( document.createTextNode( metadata["title"])) head.appendChild(title) body = document.getElementsByTagName("body")[0] buildOutline(document, body, outline) return document file_types = [("All files", ".*"), '"OutNoted files" {.out}', ("Org Mode files", ".org"), ("OPML 2.0 files", ".opml"), '"Markdown / Gemini" {.md .gmi}'] metadata = {"title": "OutNoted introduction"} outline_filename = None modified = False editing = "" # Which node we're currently editing, if any. clipboard = None search = None interp = Tcl() top = Tk() top.title("OutNoted") toolbar = ttk.Frame(top) edit_text = StringVar() edit_line = ttk.Entry(top, textvariable=edit_text) viewport = ttk.Treeview(top, height=20, selectmode="browse", columns=('status', 'link'), displaycolumns=('status',)) scrollbar = ttk.Scrollbar(top, orient=VERTICAL, command=viewport.yview) viewport["yscrollcommand"] = scrollbar.set viewport.column("status", width=64, stretch=0, anchor="center") viewport.heading("#0", text="Notes") viewport.heading("status", text="Status") status_line = ttk.Frame(top) status = ttk.Label(status_line, text=interp.eval("clock format [clock seconds]"), relief="sunken") grip = ttk.Sizegrip(status_line) bookmark_icon_data = """ R0lGODlhGAAYAIABAAAAAP///yH5BAEKAAEALAAAAAAYABgAAAJKjI+py+0PgZxUwooBlExCyUiQ xEiQxEiQxEiQxEiQxEiQxEiQdEiHBJEYKAYJRBLABCSQiqECoSAokIliApEwJBCAAwAJi8dkRgEA Ow== """ bookmark_icon = PhotoImage(data=bookmark_icon_data) app_icon_data = """ R0lGODdhIAAgALEAAAAAAP8AAICAgP///yH5BAEAAAEALAAAAAAgACAAAAKmjI+py+0P4wO0WirV 3SAnOoRiiGWCYICjWHIVcqbAynaBWxrxPdNDvgDuVDSgxqZD8Xw/ZNA57BUBp2pVBlMSVxSrFXuI 1saraFZGqqRp5rB2tmmy32fey74NtZMyoGUOxmeXIKBWRucW6JY3sBcwtHDyA8hTxyCpJyDiuHOp 2ViVqdgZqelliph0uuOlyrnKeuXmCCGbpeQhqEDqgRuZCxwsPLxQAAA7 """ app_icon = PhotoImage(data=app_icon_data) if sys.version_info.major >= 3: top.iconphoto("", app_icon) def load_help(): viewport.insert("", "end", "about", text="About OutNoted", open=1) viewport.insert("about", "end", text="An outline note-taking editor") viewport.insert("about", "end", text="Version 1.3.2 (26 February 2023), by No Time To Play") viewport.insert("about", "end", text="Open source under the MIT License") viewport.insert("", "end", "features", text="Features", open=1) viewport.insert("features", "end", text="Create and edit outlines made of one-line notes.") viewport.insert("features", "end", text="Open and save outline formats like Org Mode and OPML.") viewport.insert("features", "end", text="Treat any note as a task and / or link.") viewport.insert("", "end", "usage", text="How to use", open=1) tmp = viewport.insert("usage", "end", open=1, text="Press Ctrl-Insert to add a note, Enter to save.") viewport.insert(tmp, "end", text="Or just click in the edit line and start typing.") viewport.insert("usage", "end", text="Keep typing to add more notes in the same place.") viewport.insert("usage", "end", text="Use Ctrl-Escape to clear the selection first.") viewport.insert("usage", "end", text="Press Tab from the edit line to focus the tree.") viewport.insert("usage", "end", text="Ctrl-E starts editing the selected note.") viewport.insert("usage", "end", text="Escape on the edit line cancels editing the note.") viewport.insert("usage", "end", text="Insert adds a child note to the one selected.") viewport.insert("usage", "end", text="Delete removes the selected note and its children.") viewport.insert("usage", "end", text="Use the numeric keypad to move notes around.") viewport.insert("usage", "end", text="Right-click in the main view for a context menu.") def load_outline(data, item = ""): for i in data: added = viewport.insert(item, "end", text=i["text"], open=1) if "status" in i: viewport.set(added, "status", i["status"]) if "link" in i: viewport.set(added, "link", i["link"]) viewport.item(added, image=bookmark_icon) load_outline(i["children"], added) def unload_outline(item = ""): outline = [] for i in viewport.get_children(item): child = { "text": viewport.item(i, "text"), "children": unload_outline(i) } status = viewport.set(i, "status") if status != "": child["status"] = status link = viewport.set(i, "link") if link != "": child["link"] = link outline.append(child) return outline def parse_file(f, ext): if ext == ".out": data = json.load(f) return data["metadata"], data["outline"] elif ext == ".md" or ext == ".gmi": parser = MarkupParser(f.readlines(), '#') return parser.parseMeta(), parseMarkup(parser) elif ext == ".org": parser = OrgParser(f.readlines()) meta = parser.parseMeta() # Parse metadata first! markup = parseMarkup(parser) return meta, parseStatus(markup) elif ext == ".opml": parser = OPMLoader(f) return parser.parseMeta(), parseOPML(parser.body) else: raise RuntimeError("Unknown format") def load_file(full_path): global modified, metadata fn = os.path.basename(full_path) name, ext = os.path.splitext(fn) try: with open(full_path) as f: metadata, outline = parse_file(f, ext) viewport.delete(*viewport.get_children()) load_outline(outline) modified = False status["text"] = "Opened " + fn top.title(fn + " | OutNoted") return True except RuntimeError as e: showerror("Error reading file", str(e), parent=top) except TypeError as e: showerror("Error reading file", str(e), parent=top) except KeyError as e: showerror("Error reading file", str(e), parent=top) except OSError as e: showerror("Error opening file", str(e), parent=top) except IOError as e: # For Python 2.7 showerror("Error opening file", str(e), parent=top) except AttributeError as e: showerror("Error reading file", "File missing head or body: " + str(e), parent=top) except ExpatError as e: showerror("Error reading file", "Bad XML in input file: " + str(e), parent=top) return False def write_file(f, ext, data): if ext == ".out" or ext == ".json": json.dump(data, f) elif ext == ".md" or ext == ".gmi": printMarkup(f, data["outline"], '#') elif ext == ".org": for i in data["metadata"]: if data["metadata"][i] != "": tmp = "#+{}: {}".format( i, data["metadata"][i]) print(tmp, file=f) print(file=f) printOrgMarkup(f, data["outline"]) elif ext == ".opml": markup = buildOPML(data["metadata"], data["outline"]) markup.writexml(f, "", " ", "\n", encoding="UTF-8") else: raise RuntimeError("Unknown format") def save_file(full_path): global modified fn = os.path.basename(full_path) name, ext = os.path.splitext(fn) data = { "metadata": metadata, "outline": unload_outline() } try: with open(full_path, "w") as f: write_file(f, ext, data) f.flush() modified = False status["text"] = "Saved " + fn top.title(fn + " | OutNoted") return True except RuntimeError as e: showerror("Error writing file", str(e), parent=top) return False except OSError as e: showerror("Error saving file", str(e), parent=top) return False except IOError as e: # For Python 2.7 showerror("Error saving file", str(e), parent=top) return False def all_item_ids(item = ""): "Return a flat list of item IDs present in the viewport." items = [] for i in viewport.get_children(item): items.append(i) items.extend(all_item_ids(i)) return items def start_search(term, items): term = term.lower() for i in items: if viewport.exists(i): # It might have been deleted. text = viewport.item(i, "text") if term in text.lower(): yield i def handle_new(): global modified, outline_filename if modified: answer = askyesno( title="New outline?", message="Outline is unsaved. Start another?", icon="question", parent=top) else: answer = True if answer: viewport.delete(*viewport.get_children()) top.title("OutNoted") outline_filename = None metadata["title"] = "" modified = False else: status["text"] = "New outline canceled." def handle_open(): global outline_filename if modified: do_open = askyesno( title="Open another outline?", message="Outline is unsaved. Open another?", icon="question", parent=top) if not do_open: status["text"] = "Opening canceled." return choice = askopenfilename( title="Open existing outline", filetypes=file_types, parent=top) if len(choice) == 0: status["text"] = "Opening canceled." elif not os.path.isfile(choice): showerror( "Error opening file", "File not found: " + choice, parent=top) elif load_file(choice): outline_filename = choice def handle_save(): if outline_filename == None: handle_saveas() else: save_file(outline_filename) def handle_saveas(): global outline_filename choice = asksaveasfilename( title="Save outline as...", filetypes=file_types, parent=top) if len(choice) == 0: status["text"] = "Save canceled." elif save_file(choice): outline_filename = choice def handle_find(): global search if search != None: search.close() answer = askstring("Search", "Find text:", parent=top) if answer == None: status["text"] = "Search canceled." return search = start_search(answer, all_item_ids()) do_find() def find_again(): if search == None: handle_find() else: do_find() def do_find(): global search next_result = next(search, "") if next_result == "": search.close() search = None status["text"] = "Nothing found." else: viewport.selection_set(next_result) viewport.focus(next_result) viewport.see(next_result) def handle_insert(): global editing editing = "" edit_line.focus() def handle_delete(): global modified focus = viewport.focus() if focus == "": status["text"] = "Nothing selected." answer = False elif len(viewport.get_children(focus)) > 0: answer = askyesno( title="Delete note?", message="Note has children. Delete anyway?", icon="question", parent=top) else: answer = True if answer: viewport.selection_set(viewport.next(focus)) viewport.focus(viewport.next(focus)) viewport.delete(focus) modified = True status["text"] = "(modified)" else: status["text"] = "Deletion canceled." def insert_after(): global modified answer = askstring("Add note", "New note text:", parent=top) if answer == None: status["text"] = "Canceled adding note." else: focus = viewport.focus() if focus == "": parent = "" position = "end" else: parent = viewport.parent(focus) position = viewport.index(focus) + 1 child = viewport.insert(parent, position, text=answer) viewport.see(child) viewport.selection_set(child) viewport.focus(child) modified = True status["text"] = "(modified)" def move_left(): global modified focus = viewport.focus() if focus == "": status["text"] = "Nothing selected." return parent = viewport.parent(focus) if parent == "": status["text"] = "Note is at top level." return index = viewport.index(parent) viewport.move(focus, viewport.parent(parent), index + 1) modified = True status["text"] = "(modified)" def move_down(): global modified focus = viewport.focus() if focus == "": status["text"] = "Nothing selected." return viewport.move(focus, viewport.parent(focus), viewport.index(focus) + 1) modified = True status["text"] = "(modified)" def move_up(): global modified focus = viewport.focus() if focus == "": status["text"] = "Nothing selected." return viewport.move(focus, viewport.parent(focus), viewport.index(focus) - 1) modified = True status["text"] = "(modified)" def move_right(): global modified focus = viewport.focus() if focus == "": status["text"] = "Nothing selected." return prev = viewport.prev(focus) if prev == "": status["text"] = "No previous note." return viewport.move(focus, prev, "end") viewport.see(focus) modified = True status["text"] = "(modified)" def handle_edit_line(): global editing, modified if editing == "": viewport.see( viewport.insert( viewport.focus(), "end", text=edit_text.get())) edit_text.set("") else: viewport.item(editing, text=edit_text.get()) editing = "" edit_text.set("") modified = True status["text"] = "(modified)" def cancel_edit_line(): global editing editing = "" edit_text.set("") def start_editing(): global editing editing = viewport.focus() edit_text.set(viewport.item(editing, "text")) edit_line.focus() def cancel_selection(): viewport.selection_set("") viewport.focus("") def set_note_status(text): global modified focus = viewport.focus() if focus == "": status["text"] = "Nothing selected." elif viewport.set(focus, "status") == text: viewport.set(focus, "status", "") modified = True status["text"] = "(modified)" else: viewport.set(focus, "status", text) modified = True status["text"] = "(modified)" def set_note_link(): global modified focus = viewport.focus() if focus == "": status["text"] = "Nothing selected." return answer = askstring( "OutNoted", "Note link:", parent=top, initialvalue=viewport.set(focus, "link")) if answer != None: status["text"] = "Link set to: " + answer viewport.set(focus, "link", answer) if answer == "": viewport.item(focus, image="") else: viewport.item(focus, image=bookmark_icon) modified = True status["text"] = "(modified)" def go_to_site(): focus = viewport.focus() if focus == "": status["text"] = "Nothing selected." return link = viewport.set(focus, "link") if link == "": status["text"] = "Note isn't linked." else: try: webbrowser.open_new_tab(link) except webbrowser.Error as e: showerror("Error opening browser", str(e), parent=top) def reset_note(): global modified focus = viewport.focus() if focus == "": status["text"] = "Nothing selected." return else: viewport.set(focus, "status", "") viewport.set(focus, "link", "") viewport.item(focus, image="") modified = True status["text"] = "(modified)" def handle_cut(): global clipboard, modified focus = viewport.focus() if focus == "": status["text"] = "Nothing selected." else: clipboard = [{ "text": viewport.item(focus, "text"), "children": unload_outline(focus) }] top.clipboard_clear() top.clipboard_append(viewport.item(focus, "text")) viewport.delete(focus) modified = True status["text"] = "(modified)" def handle_copy(): global clipboard focus = viewport.focus() if focus == "": status["text"] = "Nothing selected." else: clipboard = [{ "text": viewport.item(focus, "text"), "children": unload_outline(focus) }] top.clipboard_clear() top.clipboard_append(viewport.item(focus, "text")) def handle_paste(): global modified if clipboard == None: status["text"] = "Clipboard is empty." else: load_outline(clipboard, viewport.focus()) modified = True status["text"] = "(modified)" def fold_all(): for i in viewport.get_children(): viewport.item(i, open=0) def unfold_all(): for i in viewport.get_children(): viewport.item(i, open=1) def edit_props(): global modified answer = askstring( "Properties", "Outline title:", parent=top, initialvalue=metadata["title"]) if answer != None: metadata["title"] = answer modified = True status["text"] = "(modified)" def handle_reload(): if outline_filename == None: showinfo("Oops!", "The outline was never saved.", parent=top) else: do_open = askyesno( title="Reload outline?", message="Reload outline from last save?", icon="question", parent=top) if not do_open: status["text"] = "Reloading canceled." else: load_file(outline_filename) def handle_quit(): if modified: do_quit = askyesno( title="Quit OutNoted?", message="Outline is unsaved. Quit anyway?", icon="question", parent=top) else: do_quit = True if do_quit: top.destroy() else: status["text"] = "Quit canceled." toolbar.pack(side=TOP, fill="x", padx=2, pady=2) edit_line.pack(side=TOP, fill="x", padx=2, pady=2, ipadx=2, ipady=2) status_line.pack(side=BOTTOM, fill="x", padx=2, pady=2) status.pack(side=LEFT, fill="x", ipadx=2, ipady=2, expand=1) grip.pack(side=RIGHT, anchor="s") viewport.pack(side=LEFT, fill="both", padx=2, pady=2, expand=1) scrollbar.pack(side=RIGHT, fill="y", padx=2, pady=2, expand=0) def toolbutt(txt, under=None, cmd=None): return ttk.Button( toolbar, text=txt, width=6, underline=under, command=cmd) toolbutt("New", 0, handle_new).pack(side=LEFT) toolbutt("Open", 0, handle_open).pack(side=LEFT) toolbutt("Save", 0, handle_save).pack(side=LEFT) ttk.Separator(toolbar, orient=VERTICAL).pack( side=LEFT, padx=4, pady=4, fill="y") toolbutt("Find", 0, handle_find).pack(side=LEFT) toolbutt("Again", 1, find_again).pack(side=LEFT) toolbutt("Edit", 0, start_editing).pack(side=LEFT) ttk.Separator(toolbar, orient=VERTICAL).pack( side=LEFT, padx=4, pady=4, fill="y") toolbutt("1 ToDo", 0, lambda: set_note_status("TODO")).pack(side=LEFT) toolbutt("2 Next", 0, lambda: set_note_status("NEXT")).pack(side=LEFT) toolbutt("3 Done", 0, lambda: set_note_status("DONE")).pack(side=LEFT) ttk.Separator(toolbar, orient=VERTICAL).pack( side=LEFT, padx=4, pady=4, fill="y") menu_button = ttk.Menubutton(toolbar, text="Menu", underline=0, width=5) menu_button.pack(side=LEFT) main_menu = Menu(menu_button, tearoff=0) main_menu.add_command( label="Reset note", underline=7, accelerator="Ctrl-0", command=lambda: set_note_status("")) #main_menu.add_separator() main_menu.add_command( label="Link...", underline=0, accelerator="Ctrl-L", command=set_note_link) main_menu.add_command( label="Go to site", underline=0, accelerator="Ctrl-H", command=go_to_site) main_menu.add_separator() main_menu.add_command( label="Cut", underline=0, accelerator="Ctrl-X", command=handle_cut) main_menu.add_command( label="Copy", underline=1, accelerator="Ctrl-C", command=handle_copy) main_menu.add_command( label="Paste", underline=0, accelerator="Ctrl-V", command=handle_paste) main_menu.add_separator() main_menu.add_command( label="Fold all", underline=0, accelerator="Ctrl -", command=fold_all) main_menu.add_command( label="Unfold all", underline=0, accelerator="Ctrl +", command=unfold_all) main_menu.add_separator() main_menu.add_command(label="Properties...", underline=6, command=edit_props) main_menu.add_command(label="Save as...", underline=0, command=handle_saveas) main_menu.add_command(label="Reload...", underline=0, command=handle_reload) main_menu.add_separator() main_menu.add_command( label="Quit", underline=0, accelerator="Ctrl-Q", command=handle_quit) menu_button.configure(menu=main_menu) context_menu = Menu(top, tearoff=0) context_menu.add_command( label="Insert", underline=2, accelerator="Ins", command=handle_insert) context_menu.add_command( label="Delete", underline=4, accelerator="Del", command=handle_delete) context_menu.add_command( label="Clear", underline=3, accelerator="Esc", command=cancel_selection) context_menu.add_separator() context_menu.add_command( label="Insert after...", underline=7, accelerator="Ctrl-I", command=insert_after) context_menu.add_separator() context_menu.add_command( label="Move up", underline=5, accelerator="Num 8", command=move_up) context_menu.add_command( label="Move right", underline=5, accelerator="Num 6", command=move_right) context_menu.add_command( label="Move left", underline=5, accelerator="Num 4", command=move_left) context_menu.add_command( label="Move down", underline=5, accelerator="Num 2", command=move_left) if top.tk.call('tk', 'windowingsystem') == 'aqua': top.bind('<2>', lambda e: context_menu.post(e.x_root, e.y_root)) top.bind('<Control-1>', lambda e: context_menu.post(e.x_root, e.y_root)) else: top.bind('<3>', lambda e: context_menu.post(e.x_root, e.y_root)) edit_line.bind("<Return>", lambda e: handle_edit_line()) edit_line.bind("<Escape>", lambda e: cancel_edit_line()) viewport.bind("<Control-e>", lambda e: start_editing()) viewport.bind("<Escape>", lambda e: cancel_selection()) viewport.bind("<Insert>", lambda e: handle_insert()) viewport.bind("<Delete>", lambda e: handle_delete()) # viewport.bind("<Double-1>", lambda e: handle_insert()) viewport.bind("<KP_8>", lambda e: move_up()) viewport.bind("<KP_6>", lambda e: move_right()) viewport.bind("<KP_4>", lambda e: move_left()) viewport.bind("<KP_2>", lambda e: move_down()) try: top.bind("<KP_Up>", lambda e: move_up()) top.bind("<KP_Right>", lambda e: move_right()) top.bind("<KP_Left>", lambda e: move_left()) top.bind("<KP_Down>", lambda e: move_down()) except TclError as e: print("Keypad arrows won't be available", file=sys.stderr) top.bind("<Control-n>", lambda e: handle_new()) top.bind("<Control-o>", lambda e: handle_open()) top.bind("<Control-s>", lambda e: handle_save()) top.bind("<Command-n>", lambda e: handle_new()) top.bind("<Command-o>", lambda e: handle_open()) top.bind("<Command-s>", lambda e: handle_save()) top.bind("<Control-f>", lambda e: handle_find()) top.bind("<Control-g>", lambda e: find_again()) top.bind("<Command-f>", lambda e: handle_find()) top.bind("<Command-g>", lambda e: find_again()) top.bind("<Control-Escape>", lambda e: cancel_selection()) top.bind("<Control-Insert>", lambda e: handle_insert()) top.bind("<Control-Delete>", lambda e: handle_delete()) top.bind("<Command-Escape>", lambda e: cancel_selection()) top.bind("<Command-Insert>", lambda e: handle_insert()) top.bind("<Command-Delete>", lambda e: handle_delete()) top.bind("<Control-i>", lambda e: insert_after()) top.bind("<Command-i>", lambda e: insert_after()) top.bind("<Control-Key-1>", lambda e: set_note_status("TODO")) top.bind("<Control-Key-2>", lambda e: set_note_status("NEXT")) top.bind("<Control-Key-3>", lambda e: set_note_status("DONE")) top.bind("<Control-Key-0>", lambda e: set_note_status("")) top.bind("<Command-Key-1>", lambda e: set_note_status("TODO")) top.bind("<Command-Key-2>", lambda e: set_note_status("NEXT")) top.bind("<Command-Key-3>", lambda e: set_note_status("DONE")) top.bind("<Command-Key-0>", lambda e: set_note_status("")) top.bind("<Control-l>", lambda e: set_note_link()) top.bind("<Control-h>", lambda e: go_to_site()) top.bind("<Command-l>", lambda e: set_note_link()) top.bind("<Command-h>", lambda e: go_to_site()) viewport.bind("<Control-x>", lambda e: handle_cut()) viewport.bind("<Control-c>", lambda e: handle_copy()) viewport.bind("<Control-v>", lambda e: handle_paste()) viewport.bind("<Command-x>", lambda e: handle_cut()) viewport.bind("<Command-c>", lambda e: handle_copy()) viewport.bind("<Command-v>", lambda e: handle_paste()) top.bind("<Control-minus>", lambda e: fold_all()) top.bind("<Control-plus>", lambda e: unfold_all()) top.bind("<Control-equal>", lambda e: unfold_all()) top.bind("<Command-minus>", lambda e: fold_all()) top.bind("<Command-plus>", lambda e: unfold_all()) top.bind("<Command-equal>", lambda e: unfold_all()) top.bind("<Control-q>", lambda e: handle_quit()) top.bind("<Command-q>", lambda e: handle_quit()) top.protocol("WM_DELETE_WINDOW", handle_quit) if top.tk.call('tk', 'windowingsystem') == 'x11': ttk.Style().theme_use('clam') if len(sys.argv) < 2: load_help() elif load_file(sys.argv[1]): outline_filename = sys.argv[1] else: load_help() top.mainloop()