diff --git a/.gitignore b/.gitignore index bee8a64..083d732 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ __pycache__ +.idea diff --git a/task_manager.py b/doc_manager.py similarity index 70% rename from task_manager.py rename to doc_manager.py index 4671408..1777b9c 100644 --- a/task_manager.py +++ b/doc_manager.py @@ -1,7 +1,6 @@ import logging import os import sys -import threading from datetime import datetime from typing import List @@ -10,8 +9,6 @@ from org_rw import OrgDoc, OrgTime EXTENSIONS = (".org", ".org.txt") -from gi.repository import GObject - def is_today(ot: OrgTime): now = datetime.now() @@ -42,22 +39,17 @@ class Agenda: for item in self.no_hour: print(item.scheduled.time, item.state, item.title) -class TaskManager: - docs: List[OrgDoc] - threads: List[threading.Thread] - def __init__(self, docs_path: os.PathLike): - self.docs_path = docs_path - self.threads = [] - self.docs = None +class DocumentManager: + docs: list[OrgDoc] + + def __init__(self, base_path: os.PathLike): + self.base_path = base_path def load(self): - t0 = datetime.now() - top = os.path.abspath(self.docs_path) + top = os.path.abspath(self.base_path) docs = [] - if self.docs is None: - self.docs = docs for root, dirs, files in os.walk(top): # Prune dirs @@ -76,9 +68,8 @@ class TaskManager: path = os.path.join(root, name) try: - doc = org_rw.load(open(path), extra_cautious=False) + doc = org_rw.load(open(path), extra_cautious=True) docs.append(doc) - yield doc except Exception as err: import traceback @@ -86,32 +77,10 @@ class TaskManager: print(f"== On {path}") sys.exit(1) - t1 = datetime.now() - logging.info("Loaded {} files in {}s".format(len(docs), t1 - t0)) + logging.info("Loaded {} files".format(len(docs))) self.docs = docs - def get_task_list(self, callback): - def aux(): - if self.docs is None: - last_result = None - for doc in self.load(): - result = self.get_agenda() - if ((last_result is None) - or (len(result.with_hour) != len(last_result.with_hour)) - or (len(result.no_hour) != len(last_result.no_hour))): - print("Loaded:", doc._path) - GObject.idle_add(callback, result) - print("Load completed") - else: - result = self.get_agenda() - print("Result", result) - GObject.idle_add(callback, result) - - thread = threading.Thread(target=aux) - thread.start() - self.threads.append(thread) - def get_agenda(self) -> Agenda: headline_count = 0 items_in_agenda = [] @@ -153,3 +122,25 @@ class TaskManager: 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 8e72b80..d5360fc --- a/main.py +++ b/main.py @@ -1,157 +1,241 @@ #!/usr/bin/env python3 -import sys -import os import logging -import threading +import os +import sys +import time +import webbrowser -import task_manager +from PySide2.QtCore import QObject, QThread, Signal, Slot + +from PySide2.QtGui import QPalette, QColor + +from PySide2.QtWidgets import ( + QApplication, + QDialog, + QFrame, + QGroupBox, + QHBoxLayout, + QLabel, + QLineEdit, + QProgressBar, + QPushButton, + QScrollArea, + QScroller, + QTabBar, + QVBoxLayout, +) + +import doc_manager -APP_TITLE = "Org-mob" DOCS_PATH = os.environ["ORG_PATH"] -STYLE_FILE_PATH = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "style.css") +MAX_SEARCH_NODES = 100 -MIN_TITLE_WIDTH_CHARS = 10 -import gi +class LoadDoneSignal(QObject): + sig = Signal(doc_manager.DocumentManager) -gi.require_version("Gtk", "4.0") -gi.require_version('Polkit', '1.0') -gi.require_version(namespace='Adw', version='1') -from gi.repository import Gtk, Polkit, GObject, Gio, Adw, Gdk +class DocumentLoader(QThread): + def __init__(self, manager): + QThread.__init__(self, None) + self.manager = manager + self.signal = LoadDoneSignal() -class MainWindow(Gtk.Window): + def run(self): + self.manager.load() + self.signal.sig.emit(self.manager) - ## Setup - def __init__(self, *, title, application, task_manager, with_titlebar=True): - super().__init__(title=title, application=application) - self.application = application - self.task_manager = task_manager - self.loading = 0 - if with_titlebar: - self.header_bar = Gtk.HeaderBar() - # self.header_bar.set_show_close_button(True) - # self.header_bar.props.title = APP_TITLE - self.set_titlebar(self.header_bar) +class Dialog(QDialog): + def __init__(self): + super(Dialog, self).__init__() - self.progress_spinner = Gtk.Spinner() - self.progress_spinner.start() - self.header_bar.pack_end(self.progress_spinner) + palette = self.palette() + palette.setColor(QPalette.Window, QColor(255, 255, 255)) + self.setPalette(palette) + self.setAutoFillBackground(True) + + 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) + + layout = QVBoxLayout() + + # Edit box + self.progressBar = QProgressBar() + self.progressBar.setRange(0, 0) # Make undetermined + layout.addWidget(self.progressBar) + + 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, + ) + + # Options + self.tabBar = QTabBar(shape=QTabBar.RoundedSouth) + + self.tabBar.addTab("Agenda") + self.tabBar.addTab("Notes") + self.tabBar.addTab("Tasks") + self.tabBar.addTab("History") + + 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.header_bar = None - self.progress_spinner = None + self.loadNotes() - # self.main_box = Gtk.Box(name='main-box', vexpand=True, hexpand=True) - self.scrollview = Gtk.ScrolledWindow(vexpand=True, hexpand=True) + @Slot() + def update_tab(self): + tabIndex = self.tabBar.currentIndex() + if tabIndex == 0: + self.loadAgenda() + elif tabIndex == 1: + self.loadNotes() + elif tabIndex == 2: + self.loadTasks() + elif tabIndex == 3: + self.loadHistory() - self.task_list = Gtk.ListBox(name='task-list') - self.scrollview.set_child(self.task_list) + def startLoad(self): + self.edit.setDisabled(True) + self.edit.setVisible(False) + self.tabBar.setDisabled(True) + self.progressBar.setVisible(True) - # self.main_box.props.valign = Gtk.Align.CENTER - # self.main_box.props.halign = Gtk.Align.CENTER - # self.main_box.append(self.scrollview) - # self.set_child(self.main_box) - self.set_child(self.scrollview) + self.loader = DocumentLoader(self.manager) + self.loader.signal.sig.connect(self.longoperationcomplete) + self.loading_start_time = time.time() + self.loader.start() - self.loading += 1 - self.task_manager.get_task_list(self.on_task_list_ready) + def endLoad(self): + self.edit.setDisabled(False) + self.edit.setVisible(True) + self.tabBar.setDisabled(False) + self.progressBar.setVisible(False) - ## Rendering - def build_agenda_task_row(self, task): - row = Gtk.ListBoxRow() - hbox = Gtk.Box() + self.update_tab() - state_button = Gtk.Button.new_with_label(task.state or '') - state_button.props.css_classes = ('state-button',) - state_button.connect("clicked", self.on_status_button_clicked) - hbox.append(state_button) + def longoperationcomplete(self, data): + logging.info( + "Loading complete in {:.3f}s".format(time.time() - self.loading_start_time) + ) + self.endLoad() - clock_button = Gtk.Button.new_with_label('C') - clock_button.props.css_classes = ('clock-button',) - clock_button.connect("clicked", self.on_clock_button_clicked) - hbox.append(clock_button) + def loadAgenda(self): + agenda = self.manager.get_agenda() + old = self.results.layout() - # task_name_label = Gtk.Entry(text=task.title, width_chars=max(MIN_TITLE_WIDTH_CHARS, len(task.title))) - task_name_label = Gtk.Label() - task_name_label.set_text(task.title) - task_name_label.props.css_classes = ('task-name',) - hbox.append(task_name_label) + if old: + print("Deleting old") + old.deleteLater() - row.set_child(hbox) - - return row - - def on_ready(self): - self.loading -= 1 - if self.loading < 0: - self.loading = 0 - elif self.loading == 0: - if self.progress_spinner is not None: - self.progress_spinner.stop() - - ## Callbacks - def on_task_list_ready(self, agenda): - # TODO: Avoid reconstructing the whole list every time - i = 0 - child = self.task_list.get_first_child() - while child is not None: - was = child - child = child.get_next_sibling() - i += 1 - self.task_list.remove(was) + layout = QVBoxLayout() for item in agenda.with_hour: - self.task_list.append(self.build_agenda_task_row(item)) + 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: - self.task_list.append(self.build_agenda_task_row(item)) - self.on_ready() + layout.addWidget(self.build_agenda_task_widget(item)) - ## Reactions - def on_status_button_clicked(self, button): - print('Status button clicked: {}'.format(button)) + layout.addStretch() - def on_clock_button_clicked(self, button): - print('Clock button clicked: {}'.format(button)) + frame = QFrame(self.results) + frame.setLayout(layout) + self.results.setWidget(frame) + + def build_agenda_task_widget(self, item): + box = QHBoxLayout() + frame = QFrame() + 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[:MAX_SEARCH_NODES]: + layout.addWidget(self.build_note_task_widget(note)) + + layout.addStretch() + + frame = QFrame(self.results) + frame.setLayout(layout) + self.results.setWidget(frame) + + def loadTasks(self): + logging.warning("loadTasks not yet implemented") + + def loadHistory(self): + logging.warning("loadHistory not yet implemented") -class Application(Gtk.Application): - """ Main Aplication class """ - - def __init__(self): - super().__init__(application_id='com.codigoparallevar.gtk4-organizer', - flags=Gio.ApplicationFlags.FLAGS_NONE) - self.task_manager = task_manager.TaskManager(DOCS_PATH) - - def do_activate(self): - win = self.props.active_window - if not win: - if os.path.exists(STYLE_FILE_PATH): - style_provider = Gtk.CssProvider() - Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) - style_provider.load_from_path(STYLE_FILE_PATH) - - win = MainWindow( - title=APP_TITLE, - application=self, - task_manager=self.task_manager, - ) - win.set_default_size(720, 1024) # PinePhone screen is 720x1440 - - win.present() - - -def main(): - """ Run the main application""" - # GObject.threads_init() +# Create the Qt Application +if __name__ == "__main__": logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s") - app = Application() - return app.run(sys.argv) + app = QApplication(sys.argv) -if __name__ == '__main__': - main() + 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/style.css b/style.css deleted file mode 100644 index e54b961..0000000 --- a/style.css +++ /dev/null @@ -1,20 +0,0 @@ -#task-list { - border: 1px solid #cdc7c2; - padding: 1ex; - margin: 1ex; -} - -#task-list .state-button, -#task-list .clock-button { - margin-right: 1ex; -} - -window { - background-color: #d6d5d4; -} - -#task-list .task-name { - border: none; - border-bottom: 1px solid #ccc; - border-radius: 0; -} \ No newline at end of file