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 58% rename from task_manager.py rename to doc_manager.py index 2584fd1..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,29 +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, progress_callback, complete_callback): - def aux(): - if self.docs is None: - last_result = None - # No docs read yet, load them iteratively - for doc in self.load(): - result = self.get_agenda_from_doc(doc) - GObject.idle_add(progress_callback, result) - else: - result = self.get_agenda() - print("Result", result) - GObject.idle_add(progress_callback, result) - GObject.idle_add(complete_callback, True) - - thread = threading.Thread(target=aux) - thread.start() - self.threads.append(thread) - def get_agenda(self) -> Agenda: headline_count = 0 items_in_agenda = [] @@ -151,38 +123,24 @@ class TaskManager: no_hour=other_items, ) - def get_agenda_from_doc(self, doc: OrgDoc) -> Agenda: + def get_notes(self, query) -> List[org_rw.Headline]: headline_count = 0 - items_in_agenda = [] - now = datetime.now() + t0 = datetime.now() + notes = [] + query = [q.lower() for q in query] - for hl in doc.getAllHeadlines(): - headline_count += 1 + for doc in self.docs: + for hl in doc.getAllHeadlines(): + headline_count += 1 - if ( - hl.scheduled - and isinstance(hl.scheduled, OrgTime) - and hl.scheduled.time.active - ): - 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) + data = "\n".join(hl.get_contents("raw")).lower() + if all([q in data for q in query]): + notes.append(hl) - 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( + "Filtered {} to {} items in {:.3f}s".format( + headline_count, len(notes), (datetime.now() - t0).total_seconds() ) - ] + ) - return Agenda( - with_hour=sorted(items_with_hour, key=lambda x: x.scheduled.time), - no_hour=other_items, - ) \ No newline at end of file + return notes diff --git a/emacs_client.py b/emacs_client.py deleted file mode 100644 index d7c685a..0000000 --- a/emacs_client.py +++ /dev/null @@ -1,8 +0,0 @@ -import subprocess - -def navigate_emacs_to_id(item_id): - item_id = item_id.replace('"', '\\"') - return subprocess.check_call([ - 'emacsclient', '-e', '(org-id-goto "{}")'.format(item_id)], - stdout=subprocess.DEVNULL, - ) diff --git a/main.py b/main.py old mode 100644 new mode 100755 index a3ba921..4be07ca --- a/main.py +++ b/main.py @@ -1,184 +1,210 @@ #!/usr/bin/env python3 -import sys -import os import logging -import threading +import os +import sys +import time +import webbrowser -import task_manager -import emacs_client +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 -APP_TITLE = "Org-Convergence" DOCS_PATH = os.environ["ORG_PATH"] -STYLE_FILE_PATH = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "style.css") -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): - __gsignals__ = { - "open-in-emacs": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (object, )), - } + 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) - else: - self.header_bar = None - self.progress_spinner = None + 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) - # self.main_box = Gtk.Box(name='main-box', vexpand=True, hexpand=True) - self.scrollview = Gtk.ScrolledWindow(vexpand=True, hexpand=True) + layout = QVBoxLayout() - self.task_list = Gtk.ListBox(name='task-list') - self.scrollview.set_child(self.task_list) - self.item_rows = [] + # Edit box + self.progressBar = QProgressBar() + self.progressBar.setRange(0, 0) # Make undetermined + layout.addWidget(self.progressBar) - # 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.edit = QLineEdit("", placeholderText="Search for notes") + self.edit.textEdited.connect(self.on_text_edited) + layout.addWidget(self.edit) - self.loading += 1 - self.task_manager.get_task_list( - self.on_task_list_update, - self.on_task_list_ready, + layout.setSpacing(0) + + self.results = QScrollArea(widgetResizable=True) + layout.addWidget(self.results) + QScroller.grabGesture( + self.results.viewport(), + QScroller.LeftMouseButtonGesture, ) - ## Keyboard shortcuts - def open_in_emacs(self, *args): - row = self.task_list.get_selected_row() - if row is None: - return - item = self.item_rows[row.get_index()] - item_id = item.id - if item_id is None: - logging.warning("No ID found for item: {}".format(item)) - return - emacs_client.navigate_emacs_to_id(item_id) + # 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.endLoad() + + 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") - ## Rendering - def build_agenda_task_row(self, task): - row = Gtk.ListBoxRow() - hbox = Gtk.Box() - - 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) - - 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) - - # 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) - - 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_update(self, new_rows): - for item in new_rows.with_hour: - self.task_list.append(self.build_agenda_task_row(item)) - self.item_rows.append(item) - - for item in new_rows.no_hour: - self.task_list.append(self.build_agenda_task_row(item)) - self.item_rows.append(item) - - def on_task_list_ready(self, success): - self.on_ready() - - ## Reactions - def on_status_button_clicked(self, button): - print('Status button clicked: {}'.format(button)) - - def on_clock_button_clicked(self, button): - print('Clock button clicked: {}'.format(button)) - - -class Application(Gtk.Application): - """ Main Aplication class """ - - def __init__(self): - super().__init__(application_id='com.codigoparallevar.org-convergence', - 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, - ) - - # PinePhone screen is 720x1440 (portrait) but, has 2x pixel density - win.set_default_size(360, 720) - - ## Load shortcuts - # Open in emacs - action = Gio.SimpleAction.new("open-in-emacs", None) - action.connect("activate", win.open_in_emacs) - self.add_action(action) - - self.set_accels_for_action('app.open-in-emacs', ["e"]) - - - 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 b2bb664..0000000 --- a/style.css +++ /dev/null @@ -1,13 +0,0 @@ -#task-list { -} - -#task-list .state-button, -#task-list .clock-button { - margin-right: 1ex; -} - -window { -} - -#task-list .task-name { -}