From 97a63380c6457535293f554727d256d04e35dae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sat, 3 Apr 2021 00:16:06 +0200 Subject: [PATCH 01/21] Initial commit. --- .gitignore | 1 + main.py | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 .gitignore create mode 100755 main.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/main.py b/main.py new file mode 100755 index 0000000..4f12d2c --- /dev/null +++ b/main.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 + +import logging +import os +import sys +import time + +from PySide2.QtCore import QObject, QThread, Signal, Slot +from PySide2.QtWidgets import (QApplication, QDialog, QGroupBox, QHBoxLayout, + QLabel, QLineEdit, QProgressBar, QPushButton, + QScrollArea, QTabBar, QVBoxLayout) + +ORG_PATH = os.environ['ORG_PATH'] + + +class LoadDoneSignal(QObject): + sig = Signal(str) + + +class DocumentLoader(QThread): + def __init__(self, parent = None): + QThread.__init__(self, parent) + self.exiting = False + self.signal = LoadDoneSignal() + + def run(self): + end = time.time() + 3 + while self.exiting==False: + sys.stdout.write('*') + sys.stdout.flush() + time.sleep(1) + now = time.time() + if now >= end: + self.exiting = True + self.signal.sig.emit('OK') + + +class Dialog(QDialog): + def __init__(self): + super(Dialog, self).__init__() + + self.loader = None + + 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') + layout.addWidget(self.edit) + + layout.setSpacing(0) + + self.results = QScrollArea() + layout.addWidget(self.results) + + # 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 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.tabBar.setDisabled(True) + self.progressBar.setVisible(True) + + self.loader = DocumentLoader() + self.loader.signal.sig.connect(self.longoperationcomplete) + self.loader.start() + + def endLoad(self): + self.edit.setDisabled(False) + self.tabBar.setDisabled(False) + self.progressBar.setVisible(False) + + self.update_tab() + + def longoperationcomplete(self, data): + print("Complete with", data) + self.endLoad() + + def loadAgenda(self): + logging.warning("loadAgenda not yet implemented") + + def loadNotes(self): + logging.warning("loadNotes not yet implemented") + + def loadTasks(self): + logging.warning("loadTasks not yet implemented") + +# Create the Qt Application +app = QApplication(sys.argv) + +dialog = Dialog() +sys.exit(dialog.exec_()) From 6a71f342f09ea3b7a4b4a978348e7badcd8a7069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sat, 3 Apr 2021 01:13:41 +0200 Subject: [PATCH 02/21] Add base functionality: Show agenda. --- .gitignore | 1 + doc_manager.py | 106 +++++++++++++++++++++++++++++++++++++++++++++++++ main.py | 79 +++++++++++++++++++++++++----------- 3 files changed, 163 insertions(+), 23 deletions(-) create mode 100644 doc_manager.py diff --git a/.gitignore b/.gitignore index bee8a64..083d732 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ __pycache__ +.idea diff --git a/doc_manager.py b/doc_manager.py new file mode 100644 index 0000000..fe1229d --- /dev/null +++ b/doc_manager.py @@ -0,0 +1,106 @@ +import logging +import os +import sys +from datetime import datetime +from typing import List + +import org_rw +from org_rw import OrgTime + + +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: + def __init__(self, basepath): + self.basepath = basepath + + def load(self): + top = os.path.abspath(self.basepath) + + docs = [] + + for root, dirs, files in os.walk(top): + for name in files: + if ".org" not in name: + 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 + ) diff --git a/main.py b/main.py index 4f12d2c..de08f09 100755 --- a/main.py +++ b/main.py @@ -8,31 +8,25 @@ import time from PySide2.QtCore import QObject, QThread, Signal, Slot from PySide2.QtWidgets import (QApplication, QDialog, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QProgressBar, QPushButton, - QScrollArea, QTabBar, QVBoxLayout) + QScrollArea, QTabBar, QVBoxLayout, QSplitter, QFrame) -ORG_PATH = os.environ['ORG_PATH'] +import doc_manager + +DOCS_PATH = os.environ['ORG_PATH'] class LoadDoneSignal(QObject): - sig = Signal(str) - + sig = Signal(doc_manager.DocumentManager) class DocumentLoader(QThread): - def __init__(self, parent = None): - QThread.__init__(self, parent) - self.exiting = False + def __init__(self, manager): + QThread.__init__(self, None) + self.manager = manager self.signal = LoadDoneSignal() def run(self): - end = time.time() + 3 - while self.exiting==False: - sys.stdout.write('*') - sys.stdout.flush() - time.sleep(1) - now = time.time() - if now >= end: - self.exiting = True - self.signal.sig.emit('OK') + self.manager.load() + self.signal.sig.emit(self.manager) class Dialog(QDialog): @@ -40,6 +34,7 @@ class Dialog(QDialog): super(Dialog, self).__init__() self.loader = None + self.manager = doc_manager.DocumentManager(DOCS_PATH) layout = QVBoxLayout() @@ -53,7 +48,7 @@ class Dialog(QDialog): layout.setSpacing(0) - self.results = QScrollArea() + self.results = QScrollArea(widgetResizable=True) layout.addWidget(self.results) # Options @@ -81,26 +76,61 @@ class Dialog(QDialog): def startLoad(self): self.edit.setDisabled(True) + self.edit.setVisible(False) self.tabBar.setDisabled(True) self.progressBar.setVisible(True) - self.loader = DocumentLoader() + 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): - print("Complete with", data) + logging.info("Loading complete in {:.3f}s".format(time.time() - self.loading_start_time)) self.endLoad() def loadAgenda(self): - logging.warning("loadAgenda not yet implemented") + agenda = self.manager.get_agenda() + old = self.results.layout() + + if old: + print("Deleting old") + old.deleteLater() + + layout = QVBoxLayout() + + for item in agenda.with_hour: + text = "{} {} {}".format( + item.scheduled.time, + item.state, + item.title, + ) + label = QLabel(text=text) + layout.addWidget(label) + + # if len(agenda.with_hour) > 0 and len(agenda.no_hour) > 0: + # layout.addWidget(QSplitter()) + + for item in agenda.no_hour: + text = "{} {} {}".format( + item.scheduled.time, + item.state, + item.title, + ) + label = QLabel(text=text) + layout.addWidget(label) + + frame = QFrame(self.results) + frame.setLayout(layout) + self.results.setWidget(frame) def loadNotes(self): logging.warning("loadNotes not yet implemented") @@ -109,7 +139,10 @@ class Dialog(QDialog): logging.warning("loadTasks not yet implemented") # Create the Qt Application -app = QApplication(sys.argv) +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s") -dialog = Dialog() -sys.exit(dialog.exec_()) + app = QApplication(sys.argv) + + dialog = Dialog() + sys.exit(dialog.exec_()) From 74cb24ae973a484e1d93a7b520da01e0db449adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sat, 3 Apr 2021 01:29:17 +0200 Subject: [PATCH 03/21] Add kinetic scrolling. --- main.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index de08f09..089edd9 100755 --- a/main.py +++ b/main.py @@ -6,9 +6,8 @@ import sys import time from PySide2.QtCore import QObject, QThread, Signal, Slot -from PySide2.QtWidgets import (QApplication, QDialog, QGroupBox, QHBoxLayout, - QLabel, QLineEdit, QProgressBar, QPushButton, - QScrollArea, QTabBar, QVBoxLayout, QSplitter, QFrame) +from PySide2.QtWidgets import (QApplication, QDialog, QLabel, QLineEdit, QProgressBar, QScrollArea, QTabBar, + QVBoxLayout, QFrame, QScroller) import doc_manager @@ -50,6 +49,9 @@ class Dialog(QDialog): self.results = QScrollArea(widgetResizable=True) layout.addWidget(self.results) + QScroller.grabGesture( + self.results.viewport(), QScroller.LeftMouseButtonGesture, + ) # Options self.tabBar = QTabBar(shape=QTabBar.RoundedSouth) From b126e178b119f999bd2e0d286d4c8e0b37e1709d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sat, 3 Apr 2021 01:35:34 +0200 Subject: [PATCH 04/21] Add window title. --- main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/main.py b/main.py index 089edd9..418546c 100755 --- a/main.py +++ b/main.py @@ -32,6 +32,7 @@ class Dialog(QDialog): def __init__(self): super(Dialog, self).__init__() + self.setWindowTitle("OrgEditor") self.loader = None self.manager = doc_manager.DocumentManager(DOCS_PATH) From f3e1573677d8786426d5cebb116a5040f69d17bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sat, 3 Apr 2021 01:47:33 +0200 Subject: [PATCH 05/21] Minor cleanup in document loader. --- doc_manager.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/doc_manager.py b/doc_manager.py index fe1229d..8e53025 100644 --- a/doc_manager.py +++ b/doc_manager.py @@ -5,8 +5,9 @@ from datetime import datetime from typing import List import org_rw -from org_rw import OrgTime +from org_rw import OrgTime, OrgDoc +EXTENSIONS = ( ".org", ".org.txt" ) def is_today(ot: OrgTime): now = datetime.now() @@ -37,17 +38,19 @@ class Agenda: class DocumentManager: - def __init__(self, basepath): - self.basepath = basepath + docs: list[OrgDoc] + + def __init__(self, base_path: os.PathLike): + self.base_path = base_path def load(self): - top = os.path.abspath(self.basepath) + top = os.path.abspath(self.base_path) docs = [] for root, dirs, files in os.walk(top): for name in files: - if ".org" not in name: + if all(map(lambda ext: not name.endswith(ext), EXTENSIONS)): continue path = os.path.join(root, name) From 2196c13b147536ea546fb10c92f0dc63fae320e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sat, 3 Apr 2021 01:47:58 +0200 Subject: [PATCH 06/21] Try to find a reasonable size when starting application. --- main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/main.py b/main.py index 418546c..7128930 100755 --- a/main.py +++ b/main.py @@ -33,6 +33,9 @@ class Dialog(QDialog): super(Dialog, self).__init__() 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) From df5931aeb7e252a41067459f1007bce7da3391a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Tue, 14 Sep 2021 23:02:40 +0200 Subject: [PATCH 07/21] Save changes. --- doc_manager.py | 11 +++- main.py | 51 ++++++++++++----- org-mode.svg | 149 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 195 insertions(+), 16 deletions(-) create mode 100644 org-mode.svg diff --git a/doc_manager.py b/doc_manager.py index 8e53025..edd4e05 100644 --- a/doc_manager.py +++ b/doc_manager.py @@ -5,7 +5,7 @@ from datetime import datetime from typing import List import org_rw -from org_rw import OrgTime, OrgDoc +from org_rw import OrgDoc, OrgTime EXTENSIONS = ( ".org", ".org.txt" ) @@ -49,6 +49,15 @@ class DocumentManager: 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 diff --git a/main.py b/main.py index 7128930..12eaf34 100755 --- a/main.py +++ b/main.py @@ -7,7 +7,7 @@ import time from PySide2.QtCore import QObject, QThread, Signal, Slot from PySide2.QtWidgets import (QApplication, QDialog, QLabel, QLineEdit, QProgressBar, QScrollArea, QTabBar, - QVBoxLayout, QFrame, QScroller) + QVBoxLayout, QFrame, QScroller, QHBoxLayout, QPushButton, QGroupBox) import doc_manager @@ -47,6 +47,7 @@ class Dialog(QDialog): 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) @@ -70,6 +71,10 @@ class Dialog(QDialog): self.setLayout(layout) self.startLoad() + @Slot() + def on_text_edited(self): + self.tabBar.setCurrentIndex(1) + @Slot() def update_tab(self): tabIndex = self.tabBar.currentIndex() @@ -114,30 +119,46 @@ class Dialog(QDialog): layout = QVBoxLayout() for item in agenda.with_hour: - text = "{} {} {}".format( - item.scheduled.time, - item.state, - item.title, - ) - label = QLabel(text=text) - layout.addWidget(label) + 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: - text = "{} {} {}".format( - item.scheduled.time, - item.state, - item.title, - ) - label = QLabel(text=text) - layout.addWidget(label) + 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 loadNotes(self): logging.warning("loadNotes not yet implemented") 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 + + + + + + + + + + + + + + + + + + + + + + + + + From a8e667dbce3926a010bce73dbacbea7e0619306b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Wed, 15 Sep 2021 00:04:39 +0200 Subject: [PATCH 08/21] Add base note searching functionality. --- doc_manager.py | 40 ++++++++++++++++++++++------ main.py | 71 +++++++++++++++++++++++++++++++++++++------------- 2 files changed, 85 insertions(+), 26 deletions(-) diff --git a/doc_manager.py b/doc_manager.py index edd4e05..32d5a99 100644 --- a/doc_manager.py +++ b/doc_manager.py @@ -7,7 +7,8 @@ from typing import List import org_rw from org_rw import OrgDoc, OrgTime -EXTENSIONS = ( ".org", ".org.txt" ) +EXTENSIONS = (".org", ".org.txt") + def is_today(ot: OrgTime): now = datetime.now() @@ -19,10 +20,12 @@ def is_today(ot: OrgTime): class Agenda: - def __init__(self, /, - with_hour: List[org_rw.Headline], - no_hour: List[org_rw.Headline], - ): + 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 @@ -52,7 +55,7 @@ class DocumentManager: # Prune dirs i = 0 while i < len(dirs): - if dirs[i].startswith('.git'): + if dirs[i].startswith(".git"): del dirs[i] else: i += 1 @@ -79,7 +82,6 @@ class DocumentManager: self.docs = docs def get_agenda(self) -> Agenda: - headline_count = 0 items_in_agenda = [] now = datetime.now() @@ -114,5 +116,27 @@ class DocumentManager: return Agenda( with_hour=sorted(items_with_hour, key=lambda x: x.scheduled.time), - no_hour=other_items + 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 index 12eaf34..4be07ca 100755 --- a/main.py +++ b/main.py @@ -4,19 +4,23 @@ import logging import os import sys import time +import webbrowser from PySide2.QtCore import QObject, QThread, Signal, Slot -from PySide2.QtWidgets import (QApplication, QDialog, QLabel, QLineEdit, QProgressBar, QScrollArea, QTabBar, - QVBoxLayout, QFrame, QScroller, QHBoxLayout, QPushButton, QGroupBox) +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'] +DOCS_PATH = os.environ["ORG_PATH"] class LoadDoneSignal(QObject): sig = Signal(doc_manager.DocumentManager) + class DocumentLoader(QThread): def __init__(self, manager): QThread.__init__(self, None) @@ -34,8 +38,7 @@ class Dialog(QDialog): self.setWindowTitle("OrgEditor") scrSize = self.screen().size() - self.resize(scrSize.width() / 1.5, - scrSize.height() / 1.5) + self.resize(scrSize.width() / 1.5, scrSize.height() / 1.5) self.loader = None self.manager = doc_manager.DocumentManager(DOCS_PATH) @@ -46,7 +49,7 @@ class Dialog(QDialog): self.progressBar.setRange(0, 0) # Make undetermined layout.addWidget(self.progressBar) - self.edit = QLineEdit("", placeholderText='Search for notes') + self.edit = QLineEdit("", placeholderText="Search for notes") self.edit.textEdited.connect(self.on_text_edited) layout.addWidget(self.edit) @@ -55,7 +58,8 @@ class Dialog(QDialog): self.results = QScrollArea(widgetResizable=True) layout.addWidget(self.results) QScroller.grabGesture( - self.results.viewport(), QScroller.LeftMouseButtonGesture, + self.results.viewport(), + QScroller.LeftMouseButtonGesture, ) # Options @@ -73,7 +77,10 @@ class Dialog(QDialog): @Slot() def on_text_edited(self): - self.tabBar.setCurrentIndex(1) + if self.tabBar.currentIndex() != 1: + self.tabBar.setCurrentIndex(1) + else: + self.loadNotes() @Slot() def update_tab(self): @@ -105,7 +112,9 @@ class Dialog(QDialog): self.update_tab() def longoperationcomplete(self, data): - logging.info("Loading complete in {:.3f}s".format(time.time() - self.loading_start_time)) + logging.info( + "Loading complete in {:.3f}s".format(time.time() - self.loading_start_time) + ) self.endLoad() def loadAgenda(self): @@ -141,32 +150,58 @@ class Dialog(QDialog): 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}") - ) + 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.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): - logging.warning("loadNotes not yet implemented") + 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") + # Create the Qt Application -if __name__ == '__main__': +if __name__ == "__main__": logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s") app = QApplication(sys.argv) From 37ef32a964a02fe971d64e1893531456b6c99008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sun, 24 Oct 2021 23:24:00 +0200 Subject: [PATCH 09/21] Ignore agenda items with non-active timestamps. --- doc_manager.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc_manager.py b/doc_manager.py index 32d5a99..1777b9c 100644 --- a/doc_manager.py +++ b/doc_manager.py @@ -90,7 +90,11 @@ class DocumentManager: for hl in doc.getAllHeadlines(): headline_count += 1 - if hl.scheduled and isinstance(hl.scheduled, OrgTime): + 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: From 0ed83a8306b91d1f8c5225859c26c2f3bc8e7cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Wed, 22 Dec 2021 22:44:34 +0100 Subject: [PATCH 10/21] Initial version. --- main.py | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 main.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..d8d7a8e --- /dev/null +++ b/main.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 + +import sys +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version('Polkit', '1.0') + +from gi.repository import Gtk, Polkit, GObject, Gio + +class MainWindow(Gtk.Window): + def __init__(self, title, width, height, application=None): + super().__init__(title=title, application=application) + + task_list = Gtk.ListBox() + self.set_child(task_list) + + row = Gtk.ListBoxRow() + hbox = Gtk.Box(spacing=6) + button = Gtk.Button.new_with_label("Click Me") + button.connect("clicked", self.on_click_me_clicked) + hbox.append(button) + row.set_child(hbox) + task_list.append(row) + + row = Gtk.ListBoxRow() + hbox = Gtk.Box(spacing=6) + button = Gtk.Button.new_with_mnemonic("_Open") + button.connect("clicked", self.on_open_clicked) + hbox.append(button) + row.set_child(hbox) + task_list.append(row) + + row = Gtk.ListBoxRow() + hbox = Gtk.Box(spacing=6) + button = Gtk.Button.new_with_mnemonic("_Close") + button.connect("clicked", self.on_close_clicked) + hbox.append(button) + row.set_child(hbox) + task_list.append(row) + + def on_click_me_clicked(self, button): + print('"Click me" button was clicked') + + def on_open_clicked(self, button): + print('"Open" button was clicked') + + def on_close_clicked(self, button): + print("Closing application") + self.close() + + +class Application(Gtk.Application): + """ Main Aplication class """ + + def __init__(self): + super().__init__(application_id='com.codigoparallevar.gtk4-organizer', + flags=Gio.ApplicationFlags.FLAGS_NONE) + + def do_activate(self): + win = self.props.active_window + if not win: + win = MainWindow("My Gtk4 Application", 800, 800, application=self) + win.present() + + +def main(): + """ Run the main application""" + app = Application() + return app.run(sys.argv) + + +if __name__ == '__main__': + main() From d7c350866a691f6d341dc11702ff3a1615125deb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sun, 26 Dec 2021 20:57:59 +0100 Subject: [PATCH 11/21] Add base async document loading. --- .gitignore | 1 + main.py | 79 +++++++++++++++----------- task_manager.py | 143 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+), 33 deletions(-) create mode 100644 .gitignore create mode 100644 task_manager.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/main.py b/main.py index d8d7a8e..8a7fdb1 100644 --- a/main.py +++ b/main.py @@ -1,53 +1,59 @@ #!/usr/bin/env python3 import sys +import os +import logging +import threading + +import task_manager + +APP_TITLE = "Org-mob" +DOCS_PATH = os.environ["ORG_PATH"] + import gi 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 -from gi.repository import Gtk, Polkit, GObject, Gio class MainWindow(Gtk.Window): - def __init__(self, title, width, height, application=None): + def __init__(self, *, title, application, task_manager): super().__init__(title=title, application=application) + self.application = application + self.task_manager = task_manager + self.task_manager.get_task_list(self.on_task_list_ready) - task_list = Gtk.ListBox() - self.set_child(task_list) + self.task_list = Gtk.ListBox() + self.set_child(self.task_list) + + def on_button_clicked(self, button): + print('{} was clicked'.format(button)) + + def build_agenda_task_row(self, task): row = Gtk.ListBoxRow() hbox = Gtk.Box(spacing=6) - button = Gtk.Button.new_with_label("Click Me") - button.connect("clicked", self.on_click_me_clicked) - hbox.append(button) + + state_button = Gtk.Button.new_with_label(task.state or '') + state_button.connect("clicked", self.on_button_clicked) + hbox.append(state_button) + + task_name_label = Gtk.Entry(text=task.title) + hbox.append(task_name_label) + + row.set_child(hbox) - task_list.append(row) + return row - row = Gtk.ListBoxRow() - hbox = Gtk.Box(spacing=6) - button = Gtk.Button.new_with_mnemonic("_Open") - button.connect("clicked", self.on_open_clicked) - hbox.append(button) - row.set_child(hbox) - task_list.append(row) + def on_task_list_ready(self, agenda): + for item in agenda.with_hour: + self.task_list.append(self.build_agenda_task_row(item)) - row = Gtk.ListBoxRow() - hbox = Gtk.Box(spacing=6) - button = Gtk.Button.new_with_mnemonic("_Close") - button.connect("clicked", self.on_close_clicked) - hbox.append(button) - row.set_child(hbox) - task_list.append(row) - - def on_click_me_clicked(self, button): - print('"Click me" button was clicked') - - def on_open_clicked(self, button): - print('"Open" button was clicked') - - def on_close_clicked(self, button): - print("Closing application") - self.close() + for item in agenda.no_hour: + self.task_list.append(self.build_agenda_task_row(item)) class Application(Gtk.Application): @@ -56,16 +62,23 @@ class Application(Gtk.Application): 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: - win = MainWindow("My Gtk4 Application", 800, 800, application=self) + win = MainWindow( + title=APP_TITLE, + application=self, + task_manager=self.task_manager, + ) win.present() def main(): """ Run the main application""" + GObject.threads_init() + logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s") app = Application() return app.run(sys.argv) diff --git a/task_manager.py b/task_manager.py new file mode 100644 index 0000000..97f3501 --- /dev/null +++ b/task_manager.py @@ -0,0 +1,143 @@ +import logging +import os +import sys +import threading +from datetime import datetime +from typing import List + +import org_rw +from org_rw import OrgDoc, OrgTime + +EXTENSIONS = (".org", ".org.txt") + +from gi.repository import GObject + + +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 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 + + def load(self): + t0 = datetime.now() + top = os.path.abspath(self.docs_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) + + t1 = datetime.now() + logging.info("Loaded {} files in {}s".format(len(docs), t1 - t0)) + + self.docs = docs + + def get_task_list(self, callback): + def aux(): + if self.docs is None: + self.load() + 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 = [] + now = datetime.now() + + 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) + + 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, + ) From b4c086c04989e8c6f55a4421dfab4b3fa335a494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sun, 26 Dec 2021 21:09:20 +0100 Subject: [PATCH 12/21] Add sample CSS styling. --- main.py | 11 ++++++++++- style.css | 10 ++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 style.css diff --git a/main.py b/main.py index 8a7fdb1..f908d2d 100644 --- a/main.py +++ b/main.py @@ -9,6 +9,9 @@ import task_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") import gi @@ -16,7 +19,7 @@ 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 +from gi.repository import Gtk, Polkit, GObject, Gio, Adw, Gdk class MainWindow(Gtk.Window): @@ -67,11 +70,17 @@ class Application(Gtk.Application): 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.present() diff --git a/style.css b/style.css new file mode 100644 index 0000000..c1f7a5d --- /dev/null +++ b/style.css @@ -0,0 +1,10 @@ +label { + padding-top: 20px; + background-color: red; + /* font: Vera 20px; */ +} + +entry { + color: #0f0; +} + From 259dfd12297dde0688321624b53dd616bb105bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Mon, 27 Dec 2021 20:28:24 +0100 Subject: [PATCH 13/21] Explore basic styling. --- main.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++--------- style.css | 22 +++++++++++++----- 2 files changed, 73 insertions(+), 17 deletions(-) diff --git a/main.py b/main.py index f908d2d..e6cca21 100644 --- a/main.py +++ b/main.py @@ -13,6 +13,8 @@ STYLE_FILE_PATH = os.path.join( os.path.dirname(os.path.abspath(__file__)), "style.css") +MIN_TITLE_WIDTH_CHARS = 10 + import gi gi.require_version("Gtk", "4.0") @@ -23,40 +25,84 @@ from gi.repository import Gtk, Polkit, GObject, Gio, Adw, Gdk class MainWindow(Gtk.Window): - def __init__(self, *, title, application, task_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) + + 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.main_box = Gtk.Box(name='main-box') + self.task_list = Gtk.ListBox(name='task-list') + + self.main_box.props.valign = Gtk.Align.CENTER + self.main_box.props.halign = Gtk.Align.CENTER + self.main_box.append(self.task_list) + self.set_child(self.main_box) + + self.loading += 1 self.task_manager.get_task_list(self.on_task_list_ready) - self.task_list = Gtk.ListBox() - self.set_child(self.task_list) - - - def on_button_clicked(self, button): - print('{} was clicked'.format(button)) - + ## Rendering def build_agenda_task_row(self, task): row = Gtk.ListBoxRow() - hbox = Gtk.Box(spacing=6) + hbox = Gtk.Box() state_button = Gtk.Button.new_with_label(task.state or '') - state_button.connect("clicked", self.on_button_clicked) + state_button.props.css_classes = ('state-button',) + state_button.connect("clicked", self.on_status_button_clicked) hbox.append(state_button) - task_name_label = Gtk.Entry(text=task.title) + 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.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_ready(self, agenda): for item in agenda.with_hour: self.task_list.append(self.build_agenda_task_row(item)) for item in agenda.no_hour: self.task_list.append(self.build_agenda_task_row(item)) + 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): diff --git a/style.css b/style.css index c1f7a5d..e54b961 100644 --- a/style.css +++ b/style.css @@ -1,10 +1,20 @@ -label { - padding-top: 20px; - background-color: red; - /* font: Vera 20px; */ +#task-list { + border: 1px solid #cdc7c2; + padding: 1ex; + margin: 1ex; } -entry { - color: #0f0; +#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 From 68a2cd9b11d703417409cbcb41db82f2b97c6684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Fri, 25 Mar 2022 01:47:20 +0100 Subject: [PATCH 14/21] WIP: Improve rendering on touch/mobile devices. --- main.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/main.py b/main.py index e6cca21..fbd05d6 100644 --- a/main.py +++ b/main.py @@ -23,7 +23,6 @@ gi.require_version(namespace='Adw', version='1') from gi.repository import Gtk, Polkit, GObject, Gio, Adw, Gdk - class MainWindow(Gtk.Window): ## Setup @@ -46,13 +45,17 @@ class MainWindow(Gtk.Window): self.header_bar = None self.progress_spinner = None - self.main_box = Gtk.Box(name='main-box') - self.task_list = Gtk.ListBox(name='task-list') + # self.main_box = Gtk.Box(name='main-box', vexpand=True, hexpand=True) + self.scrollview = Gtk.ScrolledWindow(vexpand=True, hexpand=True) - self.main_box.props.valign = Gtk.Align.CENTER - self.main_box.props.halign = Gtk.Align.CENTER - self.main_box.append(self.task_list) - self.set_child(self.main_box) + self.task_list = Gtk.ListBox(name='task-list') + self.scrollview.set_child(self.task_list) + + # 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.loading += 1 self.task_manager.get_task_list(self.on_task_list_ready) @@ -72,13 +75,15 @@ class MainWindow(Gtk.Window): 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.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 + + return row def on_ready(self): self.loading -= 1 @@ -132,7 +137,7 @@ class Application(Gtk.Application): def main(): """ Run the main application""" - GObject.threads_init() + # GObject.threads_init() logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s") app = Application() return app.run(sys.argv) From 35c46ba894e68310a6912b29af20ee3689994d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Fri, 25 Mar 2022 19:26:53 +0100 Subject: [PATCH 15/21] Draft naive incremental load. --- main.py | 9 +++++++++ task_manager.py | 22 +++++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index fbd05d6..d183cc7 100644 --- a/main.py +++ b/main.py @@ -95,6 +95,15 @@ class MainWindow(Gtk.Window): ## 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) + for item in agenda.with_hour: self.task_list.append(self.build_agenda_task_row(item)) diff --git a/task_manager.py b/task_manager.py index 97f3501..4671408 100644 --- a/task_manager.py +++ b/task_manager.py @@ -56,6 +56,8 @@ class TaskManager: top = os.path.abspath(self.docs_path) docs = [] + if self.docs is None: + self.docs = docs for root, dirs, files in os.walk(top): # Prune dirs @@ -74,8 +76,9 @@ class TaskManager: path = os.path.join(root, name) try: - doc = org_rw.load(open(path), extra_cautious=True) + doc = org_rw.load(open(path), extra_cautious=False) docs.append(doc) + yield doc except Exception as err: import traceback @@ -91,10 +94,19 @@ class TaskManager: def get_task_list(self, callback): def aux(): if self.docs is None: - self.load() - result = self.get_agenda() - print("Result", result) - GObject.idle_add(callback, result) + 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() From dc7353151ee15da884a7179995f052c2b65b890a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Fri, 25 Mar 2022 19:28:41 +0100 Subject: [PATCH 16/21] Add simple default window size. --- main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/main.py b/main.py index d183cc7..cd6974d 100644 --- a/main.py +++ b/main.py @@ -140,6 +140,7 @@ class Application(Gtk.Application): application=self, task_manager=self.task_manager, ) + win.set_default_size(600, 400) win.present() From d9772908195d1b8f00cb4f61c451cbef8ee19562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sun, 27 Mar 2022 23:34:01 +0200 Subject: [PATCH 17/21] Make default size closer to PinePhone form factor. --- main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index cd6974d..422f055 100644 --- a/main.py +++ b/main.py @@ -140,7 +140,9 @@ class Application(Gtk.Application): application=self, task_manager=self.task_manager, ) - win.set_default_size(600, 400) + + # PinePhone screen is 720x1440 (portrait) but, has 2x pixel density + win.set_default_size(360, 720) win.present() From 5846868f9f72b76b2f7323e337a94aac4df4e03d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sun, 27 Mar 2022 23:44:58 +0200 Subject: [PATCH 18/21] Fix iterative load to not clear and re-draw items. --- main.py | 24 ++++++++++------------- task_manager.py | 51 ++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/main.py b/main.py index 422f055..213f683 100644 --- a/main.py +++ b/main.py @@ -58,7 +58,10 @@ class MainWindow(Gtk.Window): self.set_child(self.scrollview) self.loading += 1 - self.task_manager.get_task_list(self.on_task_list_ready) + self.task_manager.get_task_list( + self.on_task_list_update, + self.on_task_list_ready, + ) ## Rendering def build_agenda_task_row(self, task): @@ -83,7 +86,7 @@ class MainWindow(Gtk.Window): row.set_child(hbox) - return row + return row def on_ready(self): self.loading -= 1 @@ -94,21 +97,14 @@ class MainWindow(Gtk.Window): 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) - - for item in agenda.with_hour: + 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)) - for item in agenda.no_hour: + for item in new_rows.no_hour: self.task_list.append(self.build_agenda_task_row(item)) + + def on_task_list_ready(self, success): self.on_ready() ## Reactions diff --git a/task_manager.py b/task_manager.py index 4671408..2584fd1 100644 --- a/task_manager.py +++ b/task_manager.py @@ -91,22 +91,19 @@ class TaskManager: self.docs = docs - def get_task_list(self, callback): + 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() - 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") + result = self.get_agenda_from_doc(doc) + GObject.idle_add(progress_callback, result) else: result = self.get_agenda() print("Result", result) - GObject.idle_add(callback, result) + GObject.idle_add(progress_callback, result) + GObject.idle_add(complete_callback, True) thread = threading.Thread(target=aux) thread.start() @@ -153,3 +150,39 @@ class TaskManager: with_hour=sorted(items_with_hour, key=lambda x: x.scheduled.time), no_hour=other_items, ) + + def get_agenda_from_doc(self, doc: OrgDoc) -> Agenda: + headline_count = 0 + items_in_agenda = [] + now = datetime.now() + + 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) + + 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 + ) + ] + + return Agenda( + with_hour=sorted(items_with_hour, key=lambda x: x.scheduled.time), + no_hour=other_items, + ) \ No newline at end of file From 247cf7cf8a368fe55ab5f14b8fddf09fccc1aabd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sun, 27 Mar 2022 23:46:17 +0200 Subject: [PATCH 19/21] Rename to org-convergence. --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 213f683..6a9596f 100644 --- a/main.py +++ b/main.py @@ -7,7 +7,7 @@ import threading import task_manager -APP_TITLE = "Org-mob" +APP_TITLE = "Org-Convergence" DOCS_PATH = os.environ["ORG_PATH"] STYLE_FILE_PATH = os.path.join( os.path.dirname(os.path.abspath(__file__)), @@ -119,7 +119,7 @@ class Application(Gtk.Application): """ Main Aplication class """ def __init__(self): - super().__init__(application_id='com.codigoparallevar.gtk4-organizer', + super().__init__(application_id='com.codigoparallevar.org-convergence', flags=Gio.ApplicationFlags.FLAGS_NONE) self.task_manager = task_manager.TaskManager(DOCS_PATH) From 6c22d19c6c13e7f4d37158d10fd9a46cbecaf05c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sun, 27 Mar 2022 23:57:01 +0200 Subject: [PATCH 20/21] Experiment with a cleaner stylesheet. --- style.css | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/style.css b/style.css index e54b961..b2bb664 100644 --- a/style.css +++ b/style.css @@ -1,7 +1,4 @@ #task-list { - border: 1px solid #cdc7c2; - padding: 1ex; - margin: 1ex; } #task-list .state-button, @@ -10,11 +7,7 @@ } window { - background-color: #d6d5d4; } #task-list .task-name { - border: none; - border-bottom: 1px solid #ccc; - border-radius: 0; -} \ No newline at end of file +} From c83b424248dcc27d89dfb02bf4eb323f33c8fc34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Mon, 28 Mar 2022 00:44:33 +0200 Subject: [PATCH 21/21] Add e to open-in-emacs using emacsclient. --- emacs_client.py | 8 ++++++++ main.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 emacs_client.py diff --git a/emacs_client.py b/emacs_client.py new file mode 100644 index 0000000..d7c685a --- /dev/null +++ b/emacs_client.py @@ -0,0 +1,8 @@ +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 index 6a9596f..a3ba921 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,7 @@ import logging import threading import task_manager +import emacs_client APP_TITLE = "Org-Convergence" DOCS_PATH = os.environ["ORG_PATH"] @@ -24,6 +25,9 @@ gi.require_version(namespace='Adw', version='1') from gi.repository import Gtk, Polkit, GObject, Gio, Adw, Gdk class MainWindow(Gtk.Window): + __gsignals__ = { + "open-in-emacs": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (object, )), + } ## Setup def __init__(self, *, title, application, task_manager, with_titlebar=True): @@ -50,6 +54,7 @@ class MainWindow(Gtk.Window): self.task_list = Gtk.ListBox(name='task-list') self.scrollview.set_child(self.task_list) + self.item_rows = [] # self.main_box.props.valign = Gtk.Align.CENTER # self.main_box.props.halign = Gtk.Align.CENTER @@ -63,6 +68,19 @@ class MainWindow(Gtk.Window): self.on_task_list_ready, ) + ## 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) + + ## Rendering def build_agenda_task_row(self, task): row = Gtk.ListBoxRow() @@ -100,9 +118,11 @@ class MainWindow(Gtk.Window): 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() @@ -140,6 +160,15 @@ class Application(Gtk.Application): # 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()