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 8e72b80..a3ba921 100644 --- a/main.py +++ b/main.py @@ -6,8 +6,9 @@ import logging import threading import task_manager +import emacs_client -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__)), @@ -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 @@ -58,7 +63,23 @@ 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, + ) + + ## 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): @@ -83,7 +104,7 @@ class MainWindow(Gtk.Window): row.set_child(hbox) - return row + return row def on_ready(self): self.loading -= 1 @@ -94,21 +115,16 @@ 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)) + self.item_rows.append(item) - for item in agenda.no_hour: + 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 @@ -123,7 +139,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) @@ -140,7 +156,18 @@ class Application(Gtk.Application): application=self, task_manager=self.task_manager, ) - win.set_default_size(720, 1024) # PinePhone screen is 720x1440 + + # 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() 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 +} 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