diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..083d732 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +.idea diff --git a/doc_manager.py b/doc_manager.py new file mode 100644 index 0000000..32d5a99 --- /dev/null +++ b/doc_manager.py @@ -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 diff --git a/main.py b/main.py old mode 100644 new mode 100755 index 6cdcac7..4be07ca --- a/main.py +++ b/main.py @@ -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_()) diff --git a/org-mode.svg b/org-mode.svg new file mode 100644 index 0000000..917cf3a --- /dev/null +++ b/org-mode.svg @@ -0,0 +1,149 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index c098fc2..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -kivy[base]