Compare commits

...

No commits in common. "9c79d017ffb9ce501ba0ae3218f2cfa612e88fae" and "a8e667dbce3926a010bce73dbacbea7e0619306b" have entirely different histories.

5 changed files with 490 additions and 48 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
__pycache__
.idea

142
doc_manager.py Normal file
View File

@ -0,0 +1,142 @@
import logging
import os
import sys
from datetime import datetime
from typing import List
import org_rw
from org_rw import OrgDoc, OrgTime
EXTENSIONS = (".org", ".org.txt")
def is_today(ot: OrgTime):
now = datetime.now()
return (
(ot.time.year == now.year)
and (ot.time.month == now.month)
and (ot.time.day == now.day)
)
class Agenda:
def __init__(
self,
/,
with_hour: List[org_rw.Headline],
no_hour: List[org_rw.Headline],
):
self.with_hour = with_hour
self.no_hour = no_hour
def print(self):
for item in self.with_hour:
print(item.scheduled.time, item.state, item.title)
if len(self.with_hour) > 0:
print("--------")
for item in self.no_hour:
print(item.scheduled.time, item.state, item.title)
class DocumentManager:
docs: list[OrgDoc]
def __init__(self, base_path: os.PathLike):
self.base_path = base_path
def load(self):
top = os.path.abspath(self.base_path)
docs = []
for root, dirs, files in os.walk(top):
# Prune dirs
i = 0
while i < len(dirs):
if dirs[i].startswith(".git"):
del dirs[i]
else:
i += 1
# Process files
for name in files:
if all(map(lambda ext: not name.endswith(ext), EXTENSIONS)):
continue
path = os.path.join(root, name)
try:
doc = org_rw.load(open(path), extra_cautious=True)
docs.append(doc)
except Exception as err:
import traceback
traceback.print_exc()
print(f"== On {path}")
sys.exit(1)
logging.info("Loaded {} files".format(len(docs)))
self.docs = docs
def get_agenda(self) -> Agenda:
headline_count = 0
items_in_agenda = []
now = datetime.now()
for doc in self.docs:
for hl in doc.getAllHeadlines():
headline_count += 1
if hl.scheduled and isinstance(hl.scheduled, OrgTime):
if is_today(hl.scheduled):
items_in_agenda.append(hl)
elif (hl.scheduled.time.to_datetime() < now) and hl.is_todo:
items_in_agenda.append(hl)
logging.info("Read {} items".format(headline_count))
logging.info("{} items in agenda today".format(len(items_in_agenda)))
items_with_hour = [
item
for item in items_in_agenda
if item.scheduled and is_today(item.scheduled) and item.scheduled.time.hour
]
other_items = [
item
for item in items_in_agenda
if not (
item.scheduled and is_today(item.scheduled) and item.scheduled.time.hour
)
]
logging.info("{} items today for a specific hour".format(len(items_with_hour)))
return Agenda(
with_hour=sorted(items_with_hour, key=lambda x: x.scheduled.time),
no_hour=other_items,
)
def get_notes(self, query) -> List[org_rw.Headline]:
headline_count = 0
t0 = datetime.now()
notes = []
query = [q.lower() for q in query]
for doc in self.docs:
for hl in doc.getAllHeadlines():
headline_count += 1
data = "\n".join(hl.get_contents("raw")).lower()
if all([q in data for q in query]):
notes.append(hl)
logging.info(
"Filtered {} to {} items in {:.3f}s".format(
headline_count, len(notes), (datetime.now() - t0).total_seconds()
)
)
return notes

244
main.py Normal file → Executable file
View File

@ -1,60 +1,210 @@
from kivy.app import App
from kivy.core.window import Window
from kivy.modules import inspector
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.scrollview import ScrollView
from kivy.uix.textinput import TextInput
#!/usr/bin/env python3
import logging
import os
import sys
import time
import webbrowser
from PySide2.QtCore import QObject, QThread, Signal, Slot
from PySide2.QtWidgets import (QApplication, QDialog, QFrame, QGroupBox,
QHBoxLayout, QLabel, QLineEdit, QProgressBar,
QPushButton, QScrollArea, QScroller, QTabBar,
QVBoxLayout)
import doc_manager
DOCS_PATH = os.environ["ORG_PATH"]
class TestApp(App):
class LoadDoneSignal(QObject):
sig = Signal(doc_manager.DocumentManager)
class DocumentLoader(QThread):
def __init__(self, manager):
QThread.__init__(self, None)
self.manager = manager
self.signal = LoadDoneSignal()
def run(self):
self.manager.load()
self.signal.sig.emit(self.manager)
class Dialog(QDialog):
def __init__(self):
App.__init__(self)
self.results = []
super(Dialog, self).__init__()
def on_omnibox_change(self, _instance, value):
print("", value)
while self.results:
btn = self.results.pop()
self.layout.remove_widget(btn)
self.setWindowTitle("OrgEditor")
scrSize = self.screen().size()
self.resize(scrSize.width() / 1.5, scrSize.height() / 1.5)
self.loader = None
self.manager = doc_manager.DocumentManager(DOCS_PATH)
for i in range(100):
btn = Button(text=value + str(i), height=40, size_hint_y=None)
self.layout.add_widget(btn)
self.results.append(btn)
layout = QVBoxLayout()
def build(self):
self.root = BoxLayout(orientation="vertical", spacing=5)
# Make sure the height is such that there is something to scroll.
self.root.bind(minimum_height=self.root.setter("height"))
# Edit box
self.progressBar = QProgressBar()
self.progressBar.setRange(0, 0) # Make undetermined
layout.addWidget(self.progressBar)
self.omnibox = TextInput(
text="",
hint_text="Search here files and headlines",
multiline=False,
write_tab=False,
size_hint_y=None,
height=40,
focus=False,
self.edit = QLineEdit("", placeholderText="Search for notes")
self.edit.textEdited.connect(self.on_text_edited)
layout.addWidget(self.edit)
layout.setSpacing(0)
self.results = QScrollArea(widgetResizable=True)
layout.addWidget(self.results)
QScroller.grabGesture(
self.results.viewport(),
QScroller.LeftMouseButtonGesture,
)
self.omnibox.bind(text=self.on_omnibox_change)
self.root.add_widget(self.omnibox)
self.viewer = ScrollView(
size_hint=(1, 1), size=(Window.width / 2, Window.height / 2)
# Options
self.tabBar = QTabBar(shape=QTabBar.RoundedSouth)
self.tabBar.addTab("Agenda")
self.tabBar.addTab("Notes")
self.tabBar.addTab("Tasks")
self.tabBar.currentChanged.connect(self.update_tab)
layout.addWidget(self.tabBar)
self.setLayout(layout)
self.startLoad()
@Slot()
def on_text_edited(self):
if self.tabBar.currentIndex() != 1:
self.tabBar.setCurrentIndex(1)
else:
self.loadNotes()
@Slot()
def update_tab(self):
tabIndex = self.tabBar.currentIndex()
if tabIndex == 0:
self.loadAgenda()
elif tabIndex == 1:
self.loadNotes()
elif tabIndex == 2:
self.loadTasks()
def startLoad(self):
self.edit.setDisabled(True)
self.edit.setVisible(False)
self.tabBar.setDisabled(True)
self.progressBar.setVisible(True)
self.loader = DocumentLoader(self.manager)
self.loader.signal.sig.connect(self.longoperationcomplete)
self.loading_start_time = time.time()
self.loader.start()
def endLoad(self):
self.edit.setDisabled(False)
self.edit.setVisible(True)
self.tabBar.setDisabled(False)
self.progressBar.setVisible(False)
self.update_tab()
def longoperationcomplete(self, data):
logging.info(
"Loading complete in {:.3f}s".format(time.time() - self.loading_start_time)
)
self.layout = BoxLayout(orientation="vertical", spacing=2, size_hint_y=None)
self.layout.bind(minimum_height=self.layout.setter("height"))
for i in range(100):
btn = Button(text=str(i), height=40, size_hint_y=None)
self.layout.add_widget(btn)
self.results.append(btn)
self.endLoad()
self.viewer.add_widget(self.layout)
self.root.add_widget(self.viewer)
inspector.create_inspector(Window, self.root)
return self.root
def loadAgenda(self):
agenda = self.manager.get_agenda()
old = self.results.layout()
if old:
print("Deleting old")
old.deleteLater()
layout = QVBoxLayout()
for item in agenda.with_hour:
layout.addWidget(self.build_agenda_task_widget(item))
# if len(agenda.with_hour) > 0 and len(agenda.no_hour) > 0:
# layout.addWidget(QSplitter())
for item in agenda.no_hour:
layout.addWidget(self.build_agenda_task_widget(item))
frame = QFrame(self.results)
frame.setLayout(layout)
self.results.setWidget(frame)
def build_agenda_task_widget(self, item):
box = QHBoxLayout()
frame = QGroupBox()
frame.setLayout(box)
state_button = QPushButton(text=f"{item.state or '-'}", maximumWidth=60)
if item.is_done:
state_button.setFlat(True)
box.addWidget(state_button)
box.addWidget(QLabel(text=f"{item.scheduled.time}", maximumWidth=200))
box.addWidget(QLabel(text=f"{item.title}"))
def on_clicked():
state_button.setText("DONE")
# state_button.setFlat(True)
# item.state = 'DONE'
if not item.is_done:
state_button.clicked.connect(on_clicked)
return frame
def build_note_task_widget(self, item):
box = QHBoxLayout()
frame = QGroupBox()
frame.setLayout(box)
titleButton = QPushButton(text=f"{item.title}")
box.addWidget(titleButton)
def on_clicked():
webbrowser.open("org-protocol://org-id?id=" + item.id)
titleButton.clicked.connect(on_clicked)
return frame
def loadNotes(self):
query = self.edit.text()
notes = self.manager.get_notes(query.split())
old = self.results.layout()
if old:
print("Deleting old")
old.deleteLater()
layout = QVBoxLayout()
for note in notes:
layout.addWidget(self.build_note_task_widget(note))
frame = QFrame(self.results)
frame.setLayout(layout)
self.results.setWidget(frame)
def loadTasks(self):
logging.warning("loadTasks not yet implemented")
TestApp().run()
# Create the Qt Application
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s")
app = QApplication(sys.argv)
dialog = Dialog()
sys.exit(dialog.exec_())

149
org-mode.svg Normal file
View File

@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="144.98"
height="160"
viewBox="-7.65 -13.389 144.98 160"
version="1.1"
id="svg28"
sodipodi:docname="org-mode.svg"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
<metadata
id="metadata34">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs32" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1364"
inkscape:window-height="743"
id="namedview30"
showgrid="false"
inkscape:zoom="1.066"
inkscape:cx="88.23"
inkscape:cy="32.52"
inkscape:window-x="0"
inkscape:window-y="23"
inkscape:window-maximized="1"
inkscape:current-layer="layer2"
inkscape:document-rotation="0" />
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Backdrop"
style="display:inline">
<circle
style="opacity:1;fill:#1a1a1a;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.0067;stroke-linecap:square;stroke-linejoin:bevel;stroke-miterlimit:1;paint-order:fill markers stroke"
id="path878"
cx="66.35"
cy="72.64"
r="68.86" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Base"
style="display:inline">
<path
fill="#77aa99"
stroke="#000000"
stroke-width="0.5"
d="m 6.448,104.3 c 0,0 10.022,36.1 46.552,24.6 25.19,-7.2 43.25,3 46.11,4.1 l 4.39,-2.5 2.9,-2.1 C 105.8,126.9 102.8,114.6 76.98,99.64 61.72,91.32 71.3,76.99 72.39,71.93 c 0,0 12.23,8.55 21.09,-4.52 8.02,2.06 13.32,-1.47 20.22,1.03 4.2,1.83 21.8,0.72 15.3,-9.11 4.1,-2.68 4.5,-1.81 6.6,-5.9 1.1,-4.95 -1.4,-6.01 -2.2,-7.36 -0.2,-3.15 -2.8,-4.37 -6,-2.13 -7.2,-1.29 -14.3,-0.68 -17.8,-0.98 -7.7,-1.07 -14.03,-5.13 -14.03,-5.13 0.93,-1.24 0.48,-3.91 -5.5,-4.1 -3.29,-1.08 -6.75,-3.13 -9.29,-3.16 -2.57,0 -2.91,-2.54 -2.91,-2.54 -1.61,-0.87 -3.93,-4.25 -3.91,-9.44 0,-3.27 -6.09,0.32 -6.09,6.79 0,6.46 -5.82,7.76 -5.82,7.76 0,0 -2.55,-2.28 -2.86,-5.96 -0.58,-3.79 -4.93,-9.56 -7.05,-2.43 -3.23,7.64 -3.49,9.42 -4.12,13.15 -1.31,7.71 -0.34,8.01 -0.34,8.01 L 14.85,69.34 Z"
id="path4"
transform="translate(0,0.001)"
sodipodi:nodetypes="ccccccccscccccccscsscccsccc" />
<path
fill="#314b49"
stroke="#314b49"
stroke-width="0.75"
stroke-linecap="round"
stroke-linejoin="round"
d="m 84.11,42.83 c 1.55,-0.56 0.9,-0.41 1.15,-0.58 -2.96,0.58 -9.63,0.62 -14.31,-1.13 0.39,0.23 2.56,0.96 2.84,1.13 0.22,0.71 0.1,1.43 2.93,2.71 2.56,0.79 5.85,0.46 6.84,-0.53 0.11,-1.69 0.12,-1.07 0.55,-1.6 z"
id="path6"
transform="translate(0,0.001)" />
<path
fill="#314b49"
stroke="#314b49"
stroke-width="0.5"
d="m 116.5,61.98 c -2.9,-2.09 -4.9,-0.27 -6.5,-0.41 1,-0.65 3.8,-2.07 8.3,-2.07 2.5,0 3.8,2.2 5.5,2.21 1.2,0 4.5,-1.71 5.2,-2.38 -1,0.84 0.4,1.76 -0.5,3.01 -0.3,0.36 -0.9,1.51 -2.7,2.05 -2,0.97 -5.4,1.76 -9.3,-2.41 z"
id="path8"
transform="translate(0,0.001)" />
<path
fill="#314b49"
d="m 54.93,24.03 c 0,0 -3.35,8 0.31,15.33 3.67,7.34 0.53,-6.83 4.69,-4.16 3.4,0.39 -2.38,-3.21 -2.03,-7.82 -0.18,-2.89 -1.77,-5.19 -2.97,-3.35 z m 64.37,26.39 c 0,1.12 -1.3,2.03 -3,2.03 3.6,-1.12 -0.2,-4.66 -3.1,-2.03 0,-1.12 1.4,-2.03 3.1,-2.03 1.7,0 3,0.91 3,2.03 z"
id="path10"
transform="translate(0,0.001)" />
<path
fill="#314b49"
d="m 114.2,47.83 c 3.7,-0.23 6.3,0.33 5.5,3.14 0.6,-1.12 1.3,-2.83 -1,-3.51 -1.8,-0.38 -3.3,-0.37 -4.5,0.37 z"
id="path12"
transform="translate(0,0.001)" />
<path
fill="#796958"
stroke="#000000"
stroke-width="0.5"
d="m 76.27,30.18 c 0,0 -0.25,7.47 6.72,2.6 3.61,0.1 2.01,-3.86 2.01,-3.86 2.44,-0.68 2.81,-3.85 1.85,-5.71 3.11,0.1 2.61,-4.72 2.18,-6.38 2.44,-0.93 2.77,-3.83 1.77,-6.13 2.93,-0.67 3.02,-4.116 2.77,-6.55 3.02,-0.168 2.6,-5.457 2.6,-6.549 2.6,-1.679 2.02,-3.946 2.42,-6.573 1.61,-3.248 -0.57,-4.178 -2.11,-0.71 -1.65,3.001 -3.77,4.311 -3.75,6.528 0.75,1.259 -5.63,3.106 -3.61,7.052 -1.43,1.763 -4.79,4.03 -3.59,6.732 -0.61,1.33 -4.89,4.43 -3.04,7.37 -4.03,2.69 -3.79,3.34 -2.94,5.79 -2.16,1.38 -4.41,4.14 -3.28,6.39 z"
id="path16"
transform="translate(0,0.001)" />
<path
fill="#ffffff"
d="m 94.09,-5.087 c 0,0 -0.73,1.324 -0.73,2.133 0,0.809 2.18,0.568 2.93,-0.227 -1.63,0.02 -2.97,0.289 -2.2,-1.906 z m -4.26,6.27 c 0,0 -0.81,1.068 -0.18,2.316 0.39,0.98 2.81,0.962 3.55,0.167 C 91.57,3.69 89.06,3.379 89.83,1.183 Z M 86.7,7.638 c 0,0 -1,1.346 -0.49,2.602 0,0.81 2.83,0.96 3.58,0.16 -1.63,0 -3.65,-0.488 -3.09,-2.762 z M 83.62,14.8 c 0,0 -1.4,1.54 -0.15,2.95 1.44,0.8 3.75,0 4.49,-0.75 -1.63,0 -4.9,0.1 -4.34,-2.2 z m -3,5.72 c 0,0 -1.58,1.42 0,3.31 1.43,0.81 4.57,0.2 5.31,-0.59 -1.63,0 -5.99,-1.35 -5.29,-2.72 z m -3.15,6.74 c 0,0 -1.4,1.54 -0.15,2.95 1.44,0.81 6.04,-0.19 6.78,-0.98 -1.63,0 -7.19,0.31 -6.63,-1.97 z"
id="path18"
transform="translate(0,0.001)" />
<path
opacity="0.26"
d="m 60.33,57.94 c 0,0 6.76,13.59 17.59,13.99 10.84,0.4 10.84,-2.73 10.84,-2.73 0,0 -15.53,3.04 -28.43,-11.26 z m 3.16,7.69 c 2.27,3.1 4.85,5.22 7.72,6.38 0,0 -7.37,11.11 -3.61,20.02 3.75,8.87 13.12,11.07 23.32,21.27 7.94,7.9 12.98,16.6 12.98,16.6 l -4.52,2.4 c 0,0 -9.14,-8.2 -20.73,-7.5 0,0 -2.68,-9.7 -10.58,-17.6 C 56.83,95.95 51.9,81.32 63.49,65.63 Z"
id="path22"
transform="translate(0,0.001)"
sodipodi:nodetypes="csccccsscccsc" />
<path
opacity="0.27"
fill="#ffffff"
d="m 71.28,19.39 c 0.99,-0.96 1.14,-0.65 0.85,0.28 -0.98,2.36 0.35,4.65 0.8,6.21 l -3.87,0.11 c 0.11,-2.27 0.25,-4.79 2.22,-6.6 z"
id="path26"
transform="translate(0,0.001)" />
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Simp"
style="display:inline">
<path
fill="#a04d32"
stroke="#000000"
stroke-width="0.5"
d="m 52.84,54.65 c -1.64,-2.19 -2.62,-4.33 -3.21,-6.27 -1.4,-3.3 -3.43,-8.02 -0.61,-17.24 0,0 0.62,-0.38 0,0 -2.62,1.63 -3.57,6.69 -13.08,9.89 -3.21,1.07 -9.87,7.03 -15.43,7.48 C 10.94,49.28 6.393,61.65 -2.091,77.2 l 0.693,6.03 1.4808,7.02 C 7.319,109.3 3.994,101 9.172,110.4 c 2.138,5.4 2.988,4.2 2.988,4.2 -5.641,-23.37 5.43,5.7 5.78,-16.85 1.74,3.65 36.48,-40.52 34.9,-43.1 z"
id="path14-3"
style="display:inline"
sodipodi:nodetypes="cccsssccccccc" />
<path
fill="#a04d32"
stroke="#000000"
stroke-width="0.5"
d="m 76.29,30.29 c -0.4,-0.45 -0.5,-0.9 -0.4,-1.72 0,0 0.4,-4.11 -16.29,-0.16 -1.07,0.74 2.4,4.73 2.4,4.73 0,0 0.42,0.21 1.05,-0.42 5.7,4.24 17.09,1.64 13.24,-2.43 z"
id="path20-6"
style="display:inline"
sodipodi:nodetypes="cccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -1 +0,0 @@
kivy[base]