💾 Archived View for gemini.ctrl-c.club › ~nttp › toys › scrunch › scrunch2.py captured on 2024-02-05 at 10:53:33.
⬅️ Previous capture (2023-01-29)
-=-=-=-=-=-=-
#!/usr/bin/env python3 # coding=utf-8 # # Scrunch Edit: a two-pane outliner for Org Mode and Markdown files # 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 from __future__ import division import webbrowser import os.path import sys if sys.version_info.major >= 3: from tkinter import * from tkinter import ttk from tkinter.simpledialog import askstring from tkinter.messagebox import showinfo, showerror, askyesno from tkinter.filedialog import askopenfilename, asksaveasfilename else: from Tkinter import * import ttk from tkSimpleDialog import askstring from tkMessageBox import showinfo, showerror, askyesno from tkFileDialog import askopenfilename, asksaveasfilename about_text = """ A two-pane outliner Version 2.1g (23 Dec 2022) MIT License """ credits_text = """ Made by No Time To Play based on knowledge gained from TkDocs.com """ class MarkupParser(object): def __init__(self, lines, headChar): self.lines = lines self.cursor = 0 self.headChar = headChar self.heading = None def parseSection(self): if self.cursor >= len(self.lines): return "" body = [] while self.lines[self.cursor][0] != self.headChar: body.append(self.lines[self.cursor].rstrip('\n')) self.cursor += 1 if self.cursor >= len(self.lines): break; return '\n'.join(body) def matchHeading(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.heading = self.lines[self.cursor][i + 1:].strip() self.cursor += 1 return True def parseMarkup(parser, level = 0): subnodes = [] while parser.matchHeading(level + 1): node = { "text": parser.heading, "section": parser.parseSection(), "children": parseMarkup(parser, level + 1) } subnodes.append(node) return subnodes def writeMarkup(f, outline, headChar, level = 1): for i in outline: print(headChar * level, i["text"], file=f) if i["section"] != "" and i["section"] != "\n": print(i["section"], file=f) writeMarkup(f, i["children"], headChar, level + 1) file_types = [("All files", ".*"), ("Org Mode files", ".org"), '"Markdown / Gemini files" {.md .gmi}'] node_text = {"": ""} outline_filename = None modified = False editing = "" # Which node we're currently editing, if any. search = None term = None min_font_size = 6 max_font_size = 16 default_font_size = 11 font_size = default_font_size def add_scrolled_text(parent, w, h): text = Text(parent, width=w, height=h, wrap="word", undo=True, font="Courier " + str(font_size)) scroll = ttk.Scrollbar( parent, orient=VERTICAL, command=text.yview) text.pack(side=LEFT, fill="both", expand=TRUE) text["yscrollcommand"] = scroll.set scroll.pack(side=RIGHT, fill="y") return text def load_text(content, text_box): text_box.delete('1.0', 'end') text_box.insert("end", content) text_box.edit_reset() text_box.edit_modified(False) def text_selection(text_box): if len(text_box.tag_ranges("sel")) > 0: return text_box.get("sel.first", "sel.last") else: return "" interp = Tcl() top = Tk() top.title("Scrunch Edit") top.option_add('*tearOff', FALSE) top["padx"] = 4 #top["pady"] = 4 icon_data = """ iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAADFBMVEUAAAD/AACAgID///8+Fw++ AAAABHRSTlP/AP//07BylAAAAGZJREFUOMvN01EKwCAMA9Ca3v/OcxsD20azjwnLr09sKFoTsRfA F5EAN7AhEoABR08/u0LBmXx5CyBdpgD8idqlzpAm+RY4ULugbDN1YSB0oWCcZAd4usxB3OyfgUOB sM2GRfTvPgAY2wicuMw+DAAAAABJRU5ErkJggg== """ app_icon = PhotoImage(data=icon_data) if sys.version_info.major >= 3: top.iconphoto("", app_icon) toolbar = ttk.Frame(top) workspace = ttk.PanedWindow(top, orient=HORIZONTAL) tree_pane = ttk.Frame(workspace) edit_pane = ttk.Frame(workspace) workspace.add(tree_pane, weight=1) workspace.add(edit_pane, weight=3) tree = ttk.Treeview(tree_pane, selectmode="browse", height=20) tree.heading("#0", text="Sections") tree_scroll = ttk.Scrollbar(tree_pane, orient=VERTICAL, command=tree.yview) tree["yscrollcommand"] = tree_scroll.set editor = add_scrolled_text(edit_pane, 42, 24) status_line = ttk.Frame(top) status = ttk.Label(status_line, text=interp.eval("clock format [clock seconds]"), relief="sunken") grip = ttk.Sizegrip(status_line) def load_outline(data, item = ""): for i in data: added = tree.insert(item, "end", text=i["text"], open=1) node_text[added] = i["section"] load_outline(i["children"], added) def unload_outline(item = ""): outline = [] for i in tree.get_children(item): child = { "text": tree.item(i, "text"), "section": node_text[i], "children": unload_outline(i) } outline.append(child) return outline def parse_file(f, ext): if ext == ".md" or ext == ".gmi": parser = MarkupParser(f.readlines(), '#') elif ext == ".org": parser = MarkupParser(f.readlines(), '*') else: raise RuntimeError("Unknown format") preface = parser.parseSection() children = parseMarkup(parser) return preface, children def load_file(full_path): global modified fn = os.path.basename(full_path) name, ext = os.path.splitext(fn) try: with open(full_path) as f: node_text[""], outline = parse_file(f, ext) tree.delete(*tree.get_children()) load_outline(outline) load_text(node_text[""], editor) modified = False status["text"] = "Opened " + fn top.title(fn + " | Scrunch Edit") return True except RuntimeError as e: showerror("Error parsing file", str(e), parent=top) return False except OSError as e: showerror("Error opening file", str(e), parent=top) return False except IOError as e: # For Python 2.7 showerror("Error opening file", str(e), parent=top) return False def write_file(f, ext, data): if data["preface"] != "" and data["preface"] != "\n": print(data["preface"], file=f) if ext == ".md" or ext == ".gmi": writeMarkup(f, data["outline"], '#') elif ext == ".org": writeMarkup(f, data["outline"], '*') else: raise RuntimeError("Unknown format") def save_file(full_path): if editor.edit_modified(): node_text[editing] = editor.get("1.0", "end").rstrip() + "\n" editor.edit_modified(False) fn = os.path.basename(full_path) name, ext = os.path.splitext(fn) data = { "preface": node_text[""], "outline": unload_outline() } try: with open(full_path, "w") as f: write_file(f, ext, data) f.flush() set_modified(False) status["text"] = "Saved " + fn top.title(fn + " | Scrunch Edit") 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 file_dir(): if outline_filename != None: return os.path.dirname(outline_filename) else: return "." def all_item_ids(item = ""): "Return a flat list of item IDs present in the tree." items = [] for i in tree.get_children(item): items.append(i) items.extend(all_item_ids(i)) return items def start_search(term, items): term = term.lower() if term in node_text[""].lower(): yield "" for i in items: if not tree.exists(i): # It might have been deleted. continue assert i in node_text if term in tree.item(i, "text").lower(): yield i elif term in node_text[i].lower(): yield i def highlight_text(text, start="1.0"): idx = editor.search(text, start, nocase=True) if len(idx) > 0: pos = "{} +{} chars".format(idx, len(text)) editor.tag_remove("sel", "1.0", "end") editor.tag_add("sel", idx, pos) editor.mark_set("insert", pos) editor.see("insert") editor.focus() def select_section(): global editing if editor.edit_modified(): node_text[editing] = editor.get("1.0", "end").rstrip() + "\n" set_modified() editing = tree.focus() load_text(node_text[editing], editor) if term != None: highlight_text(term) def select_preface(): tree.selection_set("") tree.focus("") # Don't call select_section() here, it's implied by selection_set() def set_modified(state=True): global modified modified = state def show_modified(): if modified or editor.edit_modified(): status["text"] = "(modified)" def handle_new(): global outline_filename if modified or editor.edit_modified(): answer = askyesno( title="New outline?", message="Outline is unsaved.\nStart another?", icon="question", parent=top) else: answer = True if answer: tree.delete(*tree.get_children()) top.title("Scrunch Edit") editor.delete('1.0', 'end') outline_filename = None node_text.clear() node_text[""] = "" editor.edit_reset() editor.edit_modified(False) set_modified(False) status["text"] = interp.eval("clock format [clock seconds]") else: status["text"] = "New outline canceled." def handle_open(): global outline_filename if modified or editor.edit_modified(): do_open = askyesno( title="Open another outline?", message="Outline is unsaved.\nOpen another?", icon="question", parent=top) if not do_open: status["text"] = "Opening canceled." return choice = askopenfilename( title="Open existing outline", initialdir=file_dir(), 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...", initialdir=file_dir(), filetypes=file_types, parent=top) if len(choice) == 0: status["text"] = "Save canceled." elif save_file(choice): outline_filename = choice 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 show_stats(): section_count = len(node_text) char_count = sum(len(node_text[i]) for i in node_text) char_average = int(char_count / section_count) stats = "Sections: {}\nCharacters: {}\nAverage: {}" stats = stats.format(section_count, char_count, char_average) showinfo("File statistics", stats, parent=top) def handle_quit(): if modified or editor.edit_modified(): do_quit = askyesno( title="Quit Scrunch Edit?", message="Outline is unsaved. Quit anyway?", icon="question", parent=top) else: do_quit = True if do_quit: top.destroy() else: status["text"] = "Quit canceled." def cut_content(): top.tk.eval("tk_textCut " + str(editor)) def copy_content(): top.tk.eval("tk_textCopy " + str(editor)) def paste_content(): if len(editor.tag_ranges("sel")) > 0: editor.delete("sel.first", "sel.last") top.tk.eval("tk_textPaste " + str(editor)) return "break" def select_all(widget): widget.tag_remove("sel", "1.0", "end") widget.tag_add("sel", "1.0", "end") return "break" def handle_find(): global search, term if search != None: search.close() answer = askstring("Find", "Search pattern:", parent=top, initialvalue=text_selection(editor)) if answer == None: status["text"] = "Search canceled." return search = start_search(answer, all_item_ids()) term = answer do_find() def find_again(): if search == None: handle_find() else: do_find() def do_find(): global search, term next_result = next(search, None) if next_result == None: search.close() search = None term = None status["text"] = "Nothing found." elif next_result == "": select_preface() else: tree.selection_set(next_result) tree.focus(next_result) tree.see(next_result) def set_up_node(node): node_text[node] = "" tree.selection_set(node) tree.focus(node) tree.see(node) set_modified() show_modified() def lower_case(): sel = text_selection(editor) if len(sel) > 0: editor.replace("sel.first", "sel.last", sel.lower()) else: status["text"] = "Nothing selected." return "break" def title_case(): sel = text_selection(editor) if len(sel) > 0: editor.replace("sel.first", "sel.last", sel.title()) else: status["text"] = "Nothing selected." return "break" def upper_case(): sel = text_selection(editor) if len(sel) > 0: editor.replace("sel.first", "sel.last", sel.upper()) else: status["text"] = "Nothing selected." def join_lines(): text = text_selection(editor).replace("\n", " ") if len(text) > 0: editor.replace("sel.first", "sel.last", text) else: status["text"] = "Nothing selected." def open_line(): editor.insert("insert +0 chars", "\n") editor.mark_set("insert", "insert -1 chars") return "break" def add_child(): answer = askstring("Add section", "New section title:", parent=top) if answer == None: status["text"] = "Canceled adding section." else: focus = tree.focus() child = tree.insert(focus, "end", text=answer) set_up_node(child) return "break" def insert_after(): answer = askstring("Add section", "New section title:", parent=top) if answer == None: status["text"] = "Canceled adding section." else: focus = tree.focus() if focus == "": parent = "" position = "end" else: parent = tree.parent(focus) position = tree.index(focus) + 1 child = tree.insert(parent, position, text=answer) set_up_node(child) def delete_section(): focus = tree.focus() if focus == "": status["text"] = "Can't delete preface." answer = False elif node_text[focus] != "" or len(tree.get_children(focus)) > 0: answer = askyesno( title="Delete section?", message="Section isn't empty.\nDelete anyway?", icon="question", parent=top) else: answer = True if answer: tree.selection_set(tree.next(focus)) tree.focus(tree.next(focus)) tree.delete(focus) del node_text[focus] set_modified() show_modified() else: status["text"] = "Deletion canceled." return "break" def rename_section(): focus = tree.focus() if focus == "": status["text"] = "Can't rename preface." else: answer = askstring( "Rename section", "New section title:", parent=top, initialvalue=tree.item(focus, "text")) if answer == None: status["text"] = "Canceled renaming section." else: tree.item(focus, text=answer) set_modified() show_modified() def move_left(): focus = tree.focus() if focus == "": status["text"] = "Can't move preface." return parent = tree.parent(focus) if parent == "": status["text"] = "Section is at top level." return index = tree.index(parent) tree.move(focus, tree.parent(parent), index + 1) set_modified() show_modified() def move_down(): focus = tree.focus() if focus == "": status["text"] = "Can't move preface." return tree.move(focus, tree.parent(focus), tree.index(focus) + 1) set_modified() show_modified() def move_up(): focus = tree.focus() if focus == "": status["text"] = "Can't move preface." return tree.move(focus, tree.parent(focus), tree.index(focus) - 1) set_modified() show_modified() def move_right(): focus = tree.focus() if focus == "": status["text"] = "Can't move preface." return prev = tree.prev(focus) if prev == "": status["text"] = "No previous section." return tree.move(focus, prev, "end") tree.see(focus) set_modified() show_modified() def fold_all(): for i in tree.get_children(): tree.item(i, open=0) def unfold_all(): for i in tree.get_children(): tree.item(i, open=1) def make_font_bigger(): global font_size if font_size > min_font_size: font_size += 1 editor.configure(font="Courier " + str(font_size)) def make_font_smaller(): global font_size if font_size < max_font_size: font_size -= 1 editor.configure(font="Courier " + str(font_size)) def reset_font(): global font_size font_size = default_font_size editor.configure(font="Courier " + str(font_size)) def full_screen(): top.attributes("-fullscreen", not top.attributes("-fullscreen")) def show_about(): showinfo("About Scrunch Edit", about_text, parent=top) def show_credits(): showinfo("Scrunch Edit credits", credits_text, parent=top) def show_site(): try: webbrowser.open_new_tab( "https://ctrl-c.club/~nttp/toys/scrunch/") except webbrowser.Error as e: showerror("Error opening browser", str(e), parent=top) toolbar.pack(side=TOP, pady=4) workspace.pack(side=TOP, fill="both", expand=1) tree.pack(side=LEFT, fill="both", expand=1) tree_scroll.pack(side=RIGHT, fill="y", padx=4, expand=0) status_line.pack(side=BOTTOM, fill="x", pady=4) status.pack(side=LEFT, fill="x", expand=1) grip.pack(side=RIGHT, anchor="s") def toolbutt(txt, under=None, cmd=None): return ttk.Button( toolbar, text=txt, width=8, 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) ttk.Separator(toolbar, orient=VERTICAL).pack( side=LEFT, padx=4, pady=4, fill="y") toolbutt("Insert", 0, insert_after).pack(side=LEFT) toolbutt("Delete", 0, delete_section).pack(side=LEFT) toolbutt("Rename", 4, rename_section).pack(side=LEFT) menubar = Menu(top) file_menu = Menu(menubar) menubar.add_cascade(menu=file_menu, label="File", underline=0) file_menu.add_command( label="New", underline=0, accelerator="Ctrl-N", command=handle_new) file_menu.add_command( label="Open...", underline=0, accelerator="Ctrl-O", command=handle_open) file_menu.add_command( label="Save", underline=0, accelerator="Ctrl-S", command=handle_save) file_menu.add_separator() file_menu.add_command( label="Save as...", underline=5, command=handle_saveas) file_menu.add_command( label="Reload", underline=0, accelerator="Ctrl-R", command=handle_reload) file_menu.add_command( label="Statistics", underline=1, accelerator="Ctrl-T", command=show_stats) file_menu.add_separator() file_menu.add_command( label="Quit", underline=0, accelerator="Ctrl-Q", command=handle_quit) edit_menu = Menu(menubar) menubar.add_cascade(menu=edit_menu, label="Edit", underline=0) edit_menu.add_command( label="Undo", underline=0, accelerator="Ctrl-Z", command=lambda: editor.edit_undo()) edit_menu.add_command( label="Redo", underline=0, accelerator="Ctrl-Y", command=lambda: editor.edit_redo()) edit_menu.add_separator() edit_menu.add_command( label="Cut", underline=0, accelerator="Ctrl-X", command=cut_content) edit_menu.add_command( label="Copy", underline=1, accelerator="Ctrl-C", command=copy_content) edit_menu.add_command( label="Paste", underline=0, accelerator="Ctrl-V", command=paste_content) edit_menu.add_separator() edit_menu.add_command( label="Select all", underline=0, accelerator="Ctrl-A", command=lambda: select_all(editor)) edit_menu.add_separator() edit_menu.add_command( label="Find...", underline=0, accelerator="Ctrl-F", command=handle_find) edit_menu.add_command( label="Again", underline=1, accelerator="Ctrl-G", command=find_again) format_menu = Menu(menubar) menubar.add_cascade(menu=format_menu, label="Format", underline=3) format_menu.add_command(label="Join lines", command=join_lines, underline=0, accelerator="Alt-J") format_menu.add_command(label="Open line", command=open_line, underline=0, accelerator="Alt-O") format_menu.add_separator() format_menu.add_command( label="Lower case", underline=0, accelerator="Alt-L", command=lower_case) format_menu.add_command( label="Title case", underline=0, accelerator="Alt-T", command=title_case) format_menu.add_command( label="Upper case", underline=0, accelerator="Alt-U", command=upper_case) view_menu = Menu(menubar) menubar.add_cascade(menu=view_menu, label="View", underline=0) view_menu.add_command( label="Preface", underline=2, accelerator="Ctrl-E", command=select_preface) view_menu.add_separator() view_menu.add_command( label="Fold all", underline=0, accelerator="Ctrl <", command=fold_all) view_menu.add_command( label="Unfold all", underline=0, accelerator="Ctrl >", command=unfold_all) view_menu.add_separator() view_menu.add_command( label="Bigger font", underline=0, accelerator="Ctrl +", command=make_font_bigger) view_menu.add_command( label="Smaller font", underline=0, accelerator="Ctrl -", command=make_font_smaller) view_menu.add_command( label="Reset font", underline=0, accelerator="Ctrl-0", command=reset_font) view_menu.add_separator() view_menu.add_command( label="Full screen", underline=10, accelerator="F11", command=full_screen) section_menu = Menu(menubar) menubar.add_cascade(menu=section_menu, label="Section", underline=0) section_menu.add_command( label="Add child...", underline=0, accelerator="Ctrl-Ins", command=add_child) section_menu.add_separator() section_menu.add_command( label="Insert after...", underline=0, accelerator="Ctrl-I", command=insert_after) section_menu.add_command( label="Delete", underline=4, accelerator="Ctrl-D", command=delete_section) section_menu.add_command( label="Rename...", underline=4, accelerator="Ctrl-M", command=rename_section) section_menu.add_separator() section_menu.add_command( label="Move up", underline=5, accelerator="Num 8", command=move_up) section_menu.add_command( label="Move right", underline=5, accelerator="Num 6", command=move_right) section_menu.add_command( label="Move left", underline=5, accelerator="Num 4", command=move_left) section_menu.add_command( label="Move down", underline=5, accelerator="Num 2", command=move_down) help_menu = Menu(menubar, name="help") menubar.add_cascade(menu=help_menu, label="Help", underline=0) help_menu.add_command(label="About", underline=0, command=show_about) help_menu.add_command(label="Credits", underline=0, command=show_credits) help_menu.add_command(label="Website", underline=0, command=show_site) top["menu"] = menubar tree.bind("<<TreeviewSelect>>", lambda e: select_section()) editor.bind("<<Modified>>", lambda e: show_modified()) 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("<Control-r>", lambda e: handle_reload()) top.bind("<Control-t>", lambda e: show_stats()) top.bind("<Control-q>", lambda e: handle_quit()) 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("<Command-r>", lambda e: handle_reload()) top.bind("<Command-t>", lambda e: show_stats()) top.bind("<Command-q>", lambda e: handle_quit()) # Undo, cut and copy are already bound to their usual keys by default. editor.bind("<Control-y>", lambda e: editor.edit_redo()) editor.bind("<Control-v>", lambda e: paste_content()) editor.bind("<Control-a>", lambda e: select_all(editor)) top.bind("<Control-f>", lambda e: handle_find()) top.bind("<Control-g>", lambda e: find_again()) editor.bind("<Command-y>", lambda e: editor.edit_redo()) editor.bind("<Command-v>", lambda e: paste_content()) editor.bind("<Command-a>", lambda e: select_all(editor)) top.bind("<Command-f>", lambda e: handle_find()) top.bind("<Command-g>", lambda e: find_again()) editor.tk.call("bind", "Text", "<Control-o>", "") editor.bind("<Alt-j>", lambda e: join_lines()) editor.bind("<Alt-o>", lambda e: open_line()) editor.bind("<Alt-l>", lambda e: lower_case()) editor.bind("<Alt-t>", lambda e: title_case()) editor.bind("<Alt-u>", lambda e: upper_case()) tree.bind("<Insert>", lambda e: add_child()) tree.bind("<Delete>", lambda e: delete_section()) top.bind("<Control-Insert>", lambda e: add_child()) top.bind("<Control-Delete>", lambda e: delete_section()) top.bind("<Command-Insert>", lambda e: add_child()) top.bind("<Command-Delete>", lambda e: delete_section()) top.bind("<Control-i>", lambda e: insert_after()) top.bind("<Control-d>", lambda e: delete_section()) top.bind("<Control-m>", lambda e: rename_section()) top.bind("<F2>", lambda e: rename_section()) top.bind("<Command-i>", lambda e: insert_after()) top.bind("<Command-d>", lambda e: delete_section()) top.bind("<Command-m>", lambda e: rename_section()) tree.bind("<KP_8>", lambda e: move_up()) tree.bind("<KP_6>", lambda e: move_right()) tree.bind("<KP_4>", lambda e: move_left()) tree.bind("<KP_2>", lambda e: move_down()) try: tree.bind("<KP_Up>", lambda e: move_up()) tree.bind("<KP_Right>", lambda e: move_right()) tree.bind("<KP_Left>", lambda e: move_left()) tree.bind("<KP_Down>", lambda e: move_down()) except TclError as e: print("Keypad arrows won't be available.", file=sys.stderr) top.bind("<Control-e>", lambda e: select_preface()) top.bind("<Control-period>", lambda e: fold_all()) top.bind("<Control-comma>", lambda e: unfold_all()) top.bind("<Control-less>", lambda e: fold_all()) top.bind("<Control-greater>", lambda e: unfold_all()) top.bind("<Control-minus>", lambda e: make_font_smaller()) top.bind("<Control-equal>", lambda e: make_font_bigger()) top.bind("<Control-Key-0>", lambda e: reset_font()) top.bind("<Command-e>", lambda e: select_preface()) top.bind("<Command-period>", lambda e: fold_all()) top.bind("<Command-comma>", lambda e: unfold_all()) top.bind("<Command-less>", lambda e: fold_all()) top.bind("<Command-greater>", lambda e: unfold_all()) top.bind("<Command-minus>", lambda e: make_font_smaller()) top.bind("<Command-equal>", lambda e: make_font_bigger()) top.bind("<Command-Key-0>", lambda e: reset_font()) top.bind("<F11>", lambda e: full_screen()) top.protocol("WM_DELETE_WINDOW", handle_quit) if top.tk.call('tk', 'windowingsystem') == 'x11': ttk.Style().theme_use('clam') if len(sys.argv) < 2: pass elif not os.path.exists(sys.argv[1]): outline_filename = os.path.abspath(sys.argv[1]) fn = os.path.basename(outline_filename) top.title(fn + " | Scrunch Edit") elif load_file(sys.argv[1]): outline_filename = os.path.abspath(sys.argv[1]) top.mainloop()