outnoted2/outnoted2.py

1235 lines
36 KiB
Python

#!/usr/bin/env python3
# coding=utf-8
#
# OutNoted: an outline note-taking editor
# Copyright 2024 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
import shutil
import subprocess
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
import re
import json
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")
if i.hasAttribute("xmlUrl"):
node["feed"] = i.getAttribute("xmlUrl")
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 "feed" in i and i["feed"] != "":
node.setAttribute("type", "rss")
node.setAttribute("xmlUrl", i["feed"])
if "link" in i and i["link"] != "":
node.setAttribute("htmlUrl", i["link"])
elif "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
about_text = """
An outline note-taking editor
Version 2.2a (2 Feb 2024)
MIT License
"""
credits_text = """
Made by No Time To Play
based on knowledge gained
from TkDocs.com
"""
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")
top.option_add('*tearOff', FALSE)
top["padx"] = 4
if top.tk.call('tk', 'windowingsystem') == 'x11':
ttk.Style().theme_use('clam')
bookmark_icon_data = """
R0lGODlhGAAYAIABAAAAAP///yH5BAEKAAEALAAAAAAYABgAAAJKjI+py+0PgZxUwooBlExCyUiQ
xEiQxEiQxEiQxEiQxEiQxEiQdEiHBJEYKAYJRBLABCSQiqECoSAokIliApEwJBCAAwAJi8dkRgEA
Ow==
"""
bookmark_icon = PhotoImage(data=bookmark_icon_data)
app_icon_data = """
R0lGODdhIAAgALEAAAAAAP8AAP+A/////yH5BAEAAAEALAAAAAAgACAAAAKnjI+py+0P4wO0WirV
3SAnKoRiiGWhAY5iyVXIGaSqwLbdActqyfBBDpiRbj3iT4AKCn0aI1BIU86SL2RMusOOqDir7AsY
iMfAanKYoozJXmc7aomuxeVu0nWtzAd1A9Bnsdd3dJcAJ9hmdqUwhMjl57UQMhA2NwjDMEknwPZI
2KA5sumJKck5w5fYBQUp0noJZbW1qirhaljqkWvngeNg1RssPEycUAAAOw==
"""
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 2.2a (2 Feb 2024), 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, link and/or newsfeed.")
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.")
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)
if "feed" in i:
viewport.set(added, "feed", i["feed"])
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
feed = viewport.set(i, "feed")
if feed != "":
child["feed"] = feed
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 file_dir():
if outline_filename != None:
return os.path.dirname(outline_filename)
else:
return "."
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
if (top.tk.call('tk', 'windowingsystem') == 'x11'
and shutil.which("zenity") != None):
choice = open_with_zenity(outline_filename)
else:
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 open_with_zenity(file_name):
if file_name != None:
zenity_cmd = ["zenity", "--file-selection",
"--title", "Open existing outline",
"--filename", file_name,
"--file-filter", "All files | *",
"--file-filter", "Org Mode files | *.org",
"--file-filter", "Markdown / Gemini | *.md *.gmi"]
else:
zenity_cmd = ["zenity", "--file-selection",
"--title", "Open existing outline",
"--file-filter", "All files | *",
"--file-filter", "Org Mode files | *.org",
"--file-filter", "Markdown / Gemini | *.md *.gmi"]
proc = subprocess.run(zenity_cmd, capture_output=True)
return proc.stdout.decode().rstrip()
def handle_save():
if outline_filename == None:
handle_saveas()
else:
save_file(outline_filename)
def handle_saveas():
global outline_filename
if (top.tk.call('tk', 'windowingsystem') == 'x11'
and shutil.which("zenity") != None):
choice = save_with_zenity(outline_filename)
else:
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 save_with_zenity(file_name):
if file_name != None:
zenity_cmd = ["zenity", "--file-selection",
"--title", "Save outline as...",
"--save", "--confirm-overwrite",
"--filename", file_name,
"--file-filter", "All files | *",
"--file-filter", "Org Mode files | *.org",
"--file-filter", "Markdown / Gemini | *.md *.gmi"]
else:
zenity_cmd = ["zenity", "--file-selection",
"--title", "Save outline as...",
"--save", "--confirm-overwrite",
"--file-filter", "All files | *",
"--file-filter", "Org Mode files | *.org",
"--file-filter", "Markdown / Gemini | *.md *.gmi"]
proc = subprocess.run(zenity_cmd, capture_output=True)
return proc.stdout.decode().rstrip()
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_link_icon(note):
v = viewport
if (v.set(note, "link") == "" and v.set(note, "feed") == ""):
v.item(note, image="")
else:
v.item(note, image=bookmark_icon)
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)
set_link_icon(focus)
modified = True
status["text"] = "(modified)"
def set_note_feed():
global modified
focus = viewport.focus()
if focus == "":
status["text"] = "Nothing selected."
return
answer = askstring(
"OutNoted", "Feed URL:", parent=top,
initialvalue=viewport.set(focus, "feed"))
if answer != None:
status["text"] = "Feed set to: " + answer
viewport.set(focus, "feed", answer)
set_link_icon(focus)
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.set(focus, "feed", "")
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 all_item_ids():
viewport.item(i, open=0)
def unfold_all():
for i in all_item_ids():
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 show_stats():
items = all_item_ids()
note_count = len(items)
char_count = 0
link_count = 0
v = viewport
for i in items:
char_count += len(viewport.item(i, "text"))
if v.set(i, "link") != "" or v.set(i, "feed"):
link_count += 1
stats = "Notes: {}\nWith links: {}\nText size: {} chars"
stats = stats.format(note_count, link_count, char_count)
showinfo("Outline statistics", stats, parent=top)
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."
def full_screen():
top.attributes("-fullscreen", not top.attributes("-fullscreen"))
def show_about():
showinfo("About OutNoted", about_text, parent=top)
def show_credits():
showinfo("OutNoted credits", credits_text, parent=top)
def show_site():
try:
webbrowser.open_new_tab(
"https://ctrl-c.club/~nttp/toys/outnoted/")
except webbrowser.Error as e:
showerror("Error opening browser", str(e), parent=top)
toolbar = ttk.Frame(top)
edit_text = StringVar()
edit_line = ttk.Entry(top, textvariable=edit_text)
viewport = ttk.Treeview(top, height=19, selectmode="browse",
columns=('status', 'link', 'feed'), 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)
toolbar.pack(side=TOP, fill="x", padx=2, pady=2)
edit_line.pack(side=TOP, fill="x", padx=2, pady=2, ipadx=4, ipady=4)
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)
icon_data = {}
icon_data["document-new"] = """
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJFjI+pCe2+FBC0ihbR
tBQMkBkb533haJVmhFYPtrRcx84zGNtkrbt8f/n1cBIgLWckMoxBJFCpYUIPsptQNxW9trCQ91AA
ADs="""
icon_data["document-open"] = """
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJMjI+py20AHQOiAplo
tbD3pW3ieGXjuZVIyHlu9IjAQNd2XbLzzQ952uOpQrsg7qApGn0ry/KIFCiNqtg0WA1AnrTs4wX2
YsbksvlcAAA7"""
icon_data["document-save"] = """
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJjjI+pB90CI3QJjDtA
dPMCZGHaJIxhxmDZVnaica7TU6pfEI+S68Fq5gj+brGf8YWzBZc2n2e3OxFFUInUKavWesmndovq
ZqtXsY6MlH7BU8sZ6g6750sHUH7MI3H1fnABGFgAADs="""
icon_data["document-revert"] = """
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJWjI+pBu2+FBC0ihbR
tBQMkDFcN3zhxnklGKHWgy3u2LV0ao8uK+lfxUvMVLCiCFhKKpUslGoJNQV2UWgztSomr1jndHtM
vb6r8O1Cls5oIBijCA/JIwUAOw=="""
icon_data["document-properties"] = """
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJPjI+pyx0Ao4wNiIuz
AHXPuHWaxjHWB5aLNWaqwnrx9SYsMORzjZz5j6IcbpCf0fiabY7HJImJ7NGKUKAUhww6lzqXSEb6
tkImrdCBTqsPBQA7"""
icon_data["edit-clear"] = """
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJJjI+py+0Po3yg2otV
Fbz7DiQbZ5GjEB4nOgxVC4Bq7AHt7crBCuI0aeCRfLXZx4bLAY09F/KXYqJGpSVC+LFeMVzL5AsO
i8eKAgA7"""
icon_data["edit-cut"] = """
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJZjI+pBrAfGhMOIjAq
ENSykU1c9WCgxo0leB5bSrpseKWdzMaurc16DTO1LC/h0NPImH4QTJKpkBgdklLjisUuqlcGV1eN
xMJkUthl1qitXm+3qXV7xNA5ogAAOw=="""
icon_data["edit-copy"] = """
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJWjI+pCO2/EhC0UhCZ
eG3nM1kV9gUTd5amOKLKyW3DPJMaKzuDDW4xQKu9cLnGboiSBXmGEAuoY66SwOUQBzVKUY/gsVTd
RVVhr/Ql7p4lXtqawW2o5gUAOw=="""
icon_data["edit-paste"] = """
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJOjI+pa+DA1HNCTBjB
GKBWzUUBeJEiuKUpxqDl1K7ebMVbV9aLS3tswsv9ELyeTiIT2jjGIzB5cRJlxuEhGLU2Jk0t0JIV
Nbri0ctRTgcKADs="""
icon_data["edit-find"] = """
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJWjI+pi+DvWIIUSPOE
3k9mMYRctDhaiI6MibYaqbCtKMATAM7Da5V4nuKtfsBNrXcjGnnIg2NgWh6bGECoQolZodjO7Qq5
VMFe8TYrxnDL6Uq6EX7LLwUAOw=="""
icon_data["edit-change"] = """
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJHjI+py+0I4ltgVDCh
HRzny1neA3bWVG5jk17oZq5Me5Kw+lZxHrq2jvv1ag6AAOgjCY5BlJHpCUmlhsjTY81mB9VsV6vN
PAoAOw=="""
icon_data["list-add"] = """
R0lGODdhFgAWALEAAAAAAP8AAP////+A/yH5BAEAAAEALAAAAAAWABYAAAJDjI+pG+DvmAKjWiAT
tRXns3He13AdaYQXAz3m0EblS4tpjcN3Tnsqv5oBbcLhqRF7xUYaJap4JP10qCkzY30ur89MAQA7
"""
icon_data["list-remove"] = """
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAItjI+py+0Po5y0GoCz
3vuOD4YiCHjjGZYBgLaDyronLM9mnV7czln+DwwKh4wCADs="""
icon_data["go-previous"] = """
R0lGODdhFgAWALEAAAAAAP8AAP////+A/yH5BAEAAAEALAAAAAAWABYAAAI6jI+py+3PAIxyJoAt
wqNq3mHiKG7AgKbq6oHrm1YuDMsnTbc3zpp87wuRhgqORxMoIQ+ZJdMJjUoDBQA7"""
icon_data["go-down"] = """
R0lGODdhFgAWALEAAAAAAP8AAP////+A/yH5BAEAAAEALAAAAAAWABYAAAJCjI+pG+DvmAKjWiAT
tRXns3He13AdaYQXWprjp55oPLwZbUs4u+uQWYMsHEBRDkTrHBFE11KjivA2UlbjYWU+s4sCADs=
"""
icon_data["go-up"] = """
R0lGODdhFgAWALEAAAAAAP8AAP////+A/yH5BAEAAAEALAAAAAAWABYAAAI6jI+piwAMjXNxuTFo
PRfnt3XeV4kjyZgnmkzqN0XACob0Jt34fNalbuvhAryRTwb8CXdJ5DLkig2HBQA7"""
icon_data["go-next"] = """
R0lGODdhFgAWALEAAAAAAP8AAP////+A/yH5BAEAAAEALAAAAAAWABYAAAI8jI+py+0PA4gMTJrs
xQYMq2ji6H1bN6TqyoJHycaqK8l26sJ3e+k7vvHtaBISCcipESmaZOfEgTqnVEgBADs="""
icon_data["insert-link"] = """
R0lGODdhFgAWALEAAAAAAP8AAP//////ACH5BAEAAAEALAAAAAAWABYAAAJOjI+pm+DOlhMCUBiN
tfXWzIXexzzmCZRjN6aKuIYSSrtNTLE2ArM6X6u9BkQiTDYsHkmNorG1OwCcxlOJOo1KsAEMyGnT
Jgdi0CODTisKADs="""
icons = {}
for i in icon_data:
icons[i] = PhotoImage(data=icon_data[i])
def tool_but(txt, img=None, comm=None):
return ttk.Button(toolbar, text=txt, image=img, command=comm)
tool_but("New", icons["document-new"], handle_new).pack(side=LEFT)
tool_but("Open", icons["document-open"], handle_open).pack(side=LEFT)
tool_but("Save", icons["document-save"], handle_save).pack(side=LEFT)
tool_but("Reload", icons["document-revert"], handle_reload).pack(side=LEFT)
tool_but("Props", icons["document-properties"], edit_props).pack(side=LEFT)
ttk.Separator(toolbar, orient=VERTICAL).pack(
side=LEFT, padx=4, pady=4, fill="y")
tool_but("Clear", icons["edit-clear"], cancel_selection).pack(side=LEFT)
tool_but("Cut", icons["edit-cut"], handle_cut).pack(side=LEFT)
tool_but("Copy", icons["edit-copy"], handle_copy).pack(side=LEFT)
tool_but("Paste", icons["edit-paste"], handle_paste).pack(side=LEFT)
ttk.Separator(toolbar, orient=VERTICAL).pack(
side=LEFT, padx=4, pady=4, fill="y")
tool_but("Find", icons["edit-find"], handle_find).pack(side=LEFT)
tool_but("Edit", icons["edit-change"], start_editing).pack(side=LEFT)
ttk.Separator(toolbar, orient=VERTICAL).pack(
side=LEFT, padx=4, pady=4, fill="y")
tool_but("Insert", icons["list-add"], handle_insert).pack(side=LEFT)
tool_but("Delete", icons["list-remove"], handle_delete).pack(side=LEFT)
ttk.Separator(toolbar, orient=VERTICAL).pack(
side=LEFT, padx=4, pady=4, fill="y")
tool_but("Move left", icons["go-previous"], move_left).pack(side=LEFT)
tool_but("Move down", icons["go-down"], move_down).pack(side=LEFT)
tool_but("Move up", icons["go-up"], move_up).pack(side=LEFT)
tool_but("Move right", icons["go-next"], move_right).pack(side=LEFT)
ttk.Separator(toolbar, orient=VERTICAL).pack(
side=LEFT, padx=4, pady=4, fill="y")
tool_but("Link", icons["insert-link"], set_note_link).pack(side=LEFT)
def menu_item(menu, lab, under, accel=None, comm=None):
menu.add_command(
label=lab, underline=under, accelerator=accel, command=comm)
menubar = Menu(top)
m = Menu(menubar)
menu_item(m, "New", 0, "Ctrl-N", handle_new)
menu_item(m, "Open", 0, "Ctrl-O", handle_open)
menu_item(m, "Save", 0, "Ctrl-S", handle_save)
m.add_separator()
menu_item(m, "Save as...", 5, comm=handle_saveas)
menu_item(m, "Reload", 0, "Ctrl-R", handle_reload)
m.add_separator()
menu_item(m, "Properties", 6, "Ctrl-T", edit_props)
menu_item(m, "Statistics", 4, None, show_stats)
m.add_separator()
menu_item(m, "Quit", 0, "Ctrl-Q", handle_quit)
menubar.add_cascade(menu=m, label="File", underline=0)
m = Menu(menubar)
menu_item(m, "Clear", 2, "Escape", cancel_selection)
m.add_separator()
menu_item(m, "Cut", 0, "Ctrl-X", handle_cut)
menu_item(m, "Copy", 1, "Ctrl-C", handle_copy)
menu_item(m, "Paste", 0, "Ctrl-V", handle_paste)
m.add_separator()
menu_item(m, "Find...", 0, "Ctrl-F", handle_find)
menu_item(m, "Next", 0, "Ctrl-G", find_again)
menubar.add_cascade(menu=m, label="Edit", underline=0)
m = Menu(menubar)
menu_item(m, "Insert", 0, "Insert", handle_insert)
menu_item(m, "Delete", 0, "Delete", handle_delete)
m.add_separator()
menu_item(m, "Insert after...", 7, "Ctrl-I", insert_after)
menu_item(m, "Edit", 0, "Ctrl-E", start_editing)
m.add_separator()
menu_item(m, "Move up", 5, "Num 8", move_up)
menu_item(m, "Move right", 5, "Num 6", move_right)
menu_item(m, "Move left", 5, "Num 4", move_left)
menu_item(m, "Move down", 5, "Num 2", move_down)
menubar.add_cascade(menu=m, label="Note", underline=0)
m = Menu(menubar)
menu_item(m, "ToDo", 0, "Ctrl-1", lambda: set_note_status("TODO"))
menu_item(m, "Next", 0, "Ctrl-2", lambda: set_note_status("NEXT"))
menu_item(m, "Done", 0, "Ctrl-3", lambda: set_note_status("DONE"))
m.add_separator()
menu_item(m, "Reset", 0, "Ctrl-0", lambda: set_note_status(""))
m.add_separator()
menu_item(m, "Link...", 0, "Ctrl-L", set_note_link)
menu_item(m, "Feed URL...", 0, "Ctrl-K", set_note_feed)
menu_item(m, "Go to site", 0, "Ctrl-H", go_to_site)
menubar.add_cascade(menu=m, label="Stat", underline=0)
m = Menu(menubar)
menu_item(m, "Fold all", 0, "Ctrl -", fold_all)
menu_item(m, "Unfold all", 0, "Ctrl +", unfold_all)
m.add_separator()
menu_item(m, "Full screen", 10, "F11", full_screen)
menubar.add_cascade(menu=m, label="View", underline=0)
m = Menu(menubar, name="help")
menu_item(m, "About", 0, "F1", show_about)
menu_item(m, "Credits", 0, comm=show_credits)
menu_item(m, "Website", 0, comm=show_site)
menubar.add_cascade(menu=m, label="Help", underline=0)
top["menu"] = menubar
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("<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-k>", lambda e: set_note_feed())
top.bind("<Control-h>", lambda e: go_to_site())
top.bind("<Command-l>", lambda e: set_note_link())
top.bind("<Command-k>", lambda e: set_note_feed())
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.bind("<F1>", lambda e: show_about())
top.bind("<F11>", lambda e: full_screen())
top.protocol("WM_DELETE_WINDOW", handle_quit)
if len(sys.argv) < 2:
load_help()
elif load_file(sys.argv[1]):
outline_filename = sys.argv[1]
else:
load_help()
top.mainloop()