💾 Archived View for gemini.ctrl-c.club › ~nttp › toys › scrunch › scrunch2.py captured on 2024-02-05 at 10:53:33.

View Raw

More Information

⬅️ 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()