commit 995f34c06c222129bf0e738d4f0fb4c6b65b2900
parent 7e07000abbaed9c1b1ff72fd5f4f2300ffc46b47
Author: lash <dev@holbrook.no>
Date: Mon, 1 Sep 2025 01:22:27 +0100
Replace main window with event listings
Diffstat:
5 files changed, 270 insertions(+), 108 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
@@ -10,6 +10,7 @@ requires-python = ">=3.8"
dependencies = [
"icalendar>=5.0.0",
"confini~=0.6.5",
+ "pyxdg~=0.26",
]
[project.optional-dependencies]
diff --git a/ungana/config.py b/ungana/config.py
@@ -1,9 +1,13 @@
import os
import confini
+from xdg.BaseDirectory import save_data_path
__datadir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data")
def load():
cfg = confini.Config(__datadir)
cfg.process()
+ if not cfg.have('BASE_DATAPATH'):
+ datapath = save_data_path('ungana')
+ cfg.add(datapath, 'BASE_DATAPATH')
return cfg
diff --git a/ungana/data/config.ini b/ungana/data/config.ini
@@ -1,3 +1,4 @@
[base]
domain = nondominium.org
zone = America/El_Salvador
+datapath =
diff --git a/ungana/gui/event.py b/ungana/gui/event.py
@@ -0,0 +1,120 @@
+import gi
+gi.require_version('Gtk', '4.0')
+gi.require_version('Adw', '1')
+from gi.repository import Gtk, Adw, Gio
+
+
+class EventWindow(Gtk.Window):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.cal = None
+ self.box_main = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+ self.set_child(self.box_main)
+
+ title = Gtk.Label(label="UUID")
+ self.box_main.append(title)
+ entry = Gtk.Entry()
+ entry.set_editable(False)
+ entry.set_sensitive(False)
+ self.entry_uuid = entry
+ self.box_main.append(entry)
+
+ title = Gtk.Label(label="Domain")
+ self.box_main.append(title)
+ entry = Gtk.Entry()
+ self.entry_domain = entry
+ self.box_main.append(entry)
+
+ title = Gtk.Label(label="Summary")
+ self.box_main.append(title)
+ entry = Gtk.Entry()
+ self.entry_summary = entry
+ self.box_main.append(entry)
+
+ title = Gtk.Label(label="Description")
+ self.box_main.append(title)
+ entry = Gtk.Entry()
+ self.box_main.append(entry)
+
+ title = Gtk.Label(label="Time zone")
+ self.box_main.append(title)
+ entry = Gtk.DropDown()
+ entry.set_enable_search(True)
+ zones = Gtk.StringList()
+ entry.props.model = zones
+ zonedata = zoneinfo.available_timezones()
+ i = 0
+ for v in sorted(zonedata):
+ zones.append(v)
+ if v == self.get_application().cfg.get('BASE_ZONE'):
+ entry.props.selected = i
+ logg.debug("timezone default: {}".format(v))
+ i += 1
+ self.box_main.append(entry)
+
+ title = Gtk.Label(label="Start")
+ self.box_main.append(title)
+ entry = Gtk.Entry(placeholder_text="YYYY-MM-DD HH:MM")
+ self.box_main.append(entry)
+
+ title = Gtk.Label(label="Duration")
+ self.box_main.append(title)
+ entry = Gtk.Entry(placeholder_text="")
+ self.box_main.append(entry)
+
+ title = Gtk.Label(label="Banner")
+ self.box_main.append(title)
+
+
+ fltr = Gtk.FileFilter()
+ fltr.set_name("Images")
+ fltr.add_mime_type("image/*")
+ fltrs = Gio.ListStore.new(Gtk.FileFilter)
+ fltrs.append(fltr)
+ self.filedialog = Gtk.FileDialog.new()
+ self.filedialog.set_filters(fltrs)
+ self.filedialog.set_default_filter(fltr)
+
+ img = Gtk.Image()
+ self.img = img
+ self.box_main.append(img)
+ self.img.set_size_request(0, 200)
+
+ entry = Gtk.Button(label="Open")
+ entry.connect('clicked', self.show_open_dialog)
+ self.box_main.append(entry)
+
+ self.status = Gtk.Statusbar()
+ self.box_main.append(self.status)
+
+
+ def show_open_dialog(self, button):
+ self.filedialog.open(self, None, self.open_dialog_callback)
+
+
+ def open_dialog_callback(self, dialog, result):
+ try:
+ f = dialog.open_finish(result)
+ if f is not None:
+ self.img.set_from_file(f.get_path())
+ except GLib.Error as e:
+ logg.error("error open: {}".format(e))
+
+ def load(self, fp):
+ self.cal = ICalManager()
+ self.cal.load_ical_file(fp)
+ ev = self.cal.get_first_event(self.cal.calendar)
+
+ v = ev.get('summary')
+ self.entry_summary.set_text(v)
+
+ v = ev.get('uid')
+ (uid, domain) = v.split('@')
+ self.entry_uuid.set_text(uid)
+ self.entry_domain.set_text(domain)
+
+ status_id = self.status.get_context_id("ical")
+ self.status.push(status_id, "{} {:s}: {}@{}".format(_("Loaded event from ical file"), fp, uid, domain))
+
+
diff --git a/ungana/gui/win.py b/ungana/gui/win.py
@@ -1,129 +1,165 @@
import logging
import zoneinfo
import markdown
+import datetime
+import mimetypes
+import os
+import uuid
+from pathlib import Path
+import icalendar
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
-from gi.repository import Gtk, Adw, Gio
+from gi.repository import Gtk, Adw, Gio, GObject
from ungana.i18n import _
from ungana.ical import ICalManager
logg = logging.getLogger("gui.win")
+
+class WindowEvent(GObject.GObject):
+ def __init__(self, ev, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.ev = ev
+ v = self.ev.decoded('uid')
+ r = v.split(b'@')
+ logg.debug(type(r[0]))
+ self.__uuid = uuid.UUID(r[0].decode('utf-8'))
+ self.__domain = r[1].decode('utf-8')
+ #v = self.summary.decode('utf-8')
+ #self.set_text(v)
+ #label = Gtk.Label()
+ #label.set_text(v)
+ #self.set_child(label)
+ #self.set_header(label)
+
+ def summary(self):
+ v = self.ev.decoded('summary')
+ return v.decode('utf-8')
+
+
+ def uuid(self):
+ return self.__uuid
+
+
+ def domain(self):
+ return self.__domain
+
+
+ def when(self):
+ return "fii"
+
+
+def event_item_setup(widget, item):
+ cell = Gtk.Label()
+ item.set_child(cell)
+ logg.debug("setup")
+
+
+def event_head_bind(widget, item):
+ label = item.get_child()
+ o = item.get_item()
+ label.set_text(o.summary())
+ logg.debug("bind")
+
+
+def event_uuid_bind(widget, item):
+ label = item.get_child()
+ o = item.get_item()
+ label.set_text(str(o.uuid()))
+ logg.debug("bind")
+
+
+def event_domain_bind(widget, item):
+ label = item.get_child()
+ o = item.get_item()
+ label.set_text(o.domain())
+ logg.debug("bind")
+
+
+def event_date_bind(widget, item):
+ label = item.get_child()
+ o = item.get_item()
+ label.set_text(o.when())
+ logg.debug("bind")
+
+
class MainWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
-
self.props.show_menubar = True
- self.cal = None
+
self.box_main = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.set_child(self.box_main)
- title = Gtk.Label(label="UUID")
- self.box_main.append(title)
- entry = Gtk.Entry()
- entry.set_editable(False)
- entry.set_sensitive(False)
- self.entry_uuid = entry
- self.box_main.append(entry)
-
- title = Gtk.Label(label="Domain")
- self.box_main.append(title)
- entry = Gtk.Entry()
- self.entry_domain = entry
- self.box_main.append(entry)
-
- title = Gtk.Label(label="Summary")
- self.box_main.append(title)
- entry = Gtk.Entry()
- self.entry_summary = entry
- self.box_main.append(entry)
-
- title = Gtk.Label(label="Description")
- self.box_main.append(title)
- entry = Gtk.Entry()
- self.box_main.append(entry)
-
- title = Gtk.Label(label="Time zone")
- self.box_main.append(title)
- entry = Gtk.DropDown()
- entry.set_enable_search(True)
- zones = Gtk.StringList()
- entry.props.model = zones
- zonedata = zoneinfo.available_timezones()
- i = 0
- for v in sorted(zonedata):
- zones.append(v)
- if v == self.get_application().cfg.get('BASE_ZONE'):
- entry.props.selected = i
- logg.debug("timezone default: {}".format(v))
- i += 1
- self.box_main.append(entry)
-
- title = Gtk.Label(label="Start")
- self.box_main.append(title)
- entry = Gtk.Entry(placeholder_text="YYYY-MM-DD HH:MM")
- self.box_main.append(entry)
-
- title = Gtk.Label(label="Duration")
- self.box_main.append(title)
- entry = Gtk.Entry(placeholder_text="")
- self.box_main.append(entry)
-
- title = Gtk.Label(label="Banner")
- self.box_main.append(title)
-
-
- fltr = Gtk.FileFilter()
- fltr.set_name("Images")
- fltr.add_mime_type("image/*")
- fltrs = Gio.ListStore.new(Gtk.FileFilter)
- fltrs.append(fltr)
- self.filedialog = Gtk.FileDialog.new()
- self.filedialog.set_filters(fltrs)
- self.filedialog.set_default_filter(fltr)
-
- img = Gtk.Image()
- self.img = img
- self.box_main.append(img)
- self.img.set_size_request(0, 200)
-
- entry = Gtk.Button(label="Open")
- entry.connect('clicked', self.show_open_dialog)
- self.box_main.append(entry)
-
- self.status = Gtk.Statusbar()
- self.box_main.append(self.status)
-
-
- def show_open_dialog(self, button):
- self.filedialog.open(self, None, self.open_dialog_callback)
-
-
- def open_dialog_callback(self, dialog, result):
- try:
- f = dialog.open_finish(result)
- if f is not None:
- self.img.set_from_file(f.get_path())
- except GLib.Error as e:
- logg.error("error open: {}".format(e))
-
- def load(self, fp):
- self.cal = ICalManager()
- self.cal.load_ical_file(fp)
- ev = self.cal.get_first_event(self.cal.calendar)
-
- v = ev.get('summary')
- self.entry_summary.set_text(v)
-
- v = ev.get('uid')
- (uid, domain) = v.split('@')
- self.entry_uuid.set_text(uid)
- self.entry_domain.set_text(domain)
-
- status_id = self.status.get_context_id("ical")
- self.status.push(status_id, "{} {:s}: {}@{}".format(_("Loaded event from ical file"), fp, uid, domain))
-
+ #self.store = Gtk.TreeListModel(WindowEvent)
+ #self.store = Gtk.ListStore(str)
+ self.store = Gio.ListStore.new(WindowEvent)
+ self.sel = Gtk.SingleSelection()
+ self.sel.set_model(self.store)
+
+ self.view = Gtk.ColumnView()
+ self.view.set_model(self.sel)
+
+ factory_head = Gtk.SignalListItemFactory()
+ factory_head.connect("setup", event_item_setup)
+ factory_head.connect("bind", event_head_bind)
+
+ factory_uuid = Gtk.SignalListItemFactory()
+ factory_uuid.connect("setup", event_item_setup)
+ factory_uuid.connect("bind", event_uuid_bind)
+
+ factory_domain = Gtk.SignalListItemFactory()
+ factory_domain.connect("setup", event_item_setup)
+ factory_domain.connect("bind", event_domain_bind)
+
+ factory_date = Gtk.SignalListItemFactory()
+ factory_date.connect("setup", event_item_setup)
+ factory_date.connect("bind", event_date_bind)
+
+ rendererTxt = Gtk.CellRendererText()
+ # TODO: is there a row equivalent to set up?
+ col = Gtk.ColumnViewColumn.new(_("Event"), factory_head)
+ #col = Gtk.TreeViewColumn(_("Events"), rendererTxt, text=0)
+ self.view.append_column(col)
+ col = Gtk.ColumnViewColumn.new(_("UUID"), factory_uuid)
+ self.view.append_column(col)
+ col = Gtk.ColumnViewColumn.new(_("Domain"), factory_domain)
+ self.view.append_column(col)
+ col = Gtk.ColumnViewColumn.new(_("Date"), factory_date)
+ self.view.append_column(col)
+ self.box_main.append(self.view)
+
+
+ def add_event(self, ev):
+ logg.info("adding event {}".format(str(ev)))
+ o = WindowEvent(ev)
+ self.store.append(o)
+ #self.store.append([ev.decoded('summary').decode('utf-8')])
+
+
+ def load(self, v, skip_past=True):
+ cfg = self.get_application().cfg
+ now = datetime.datetime.now(datetime.UTC)
+ events = {}
+ logg.debug("processing {}".format(cfg.get('BASE_DATAPATH')))
+ for (r, d, files) in os.walk(cfg.get('BASE_DATAPATH')):
+ for f in files:
+ t = mimetypes.guess_file_type(f)
+ if t[0] != 'text/calendar':
+ continue
+ fp = os.path.join(r, f)
+ logg.debug("processing: {}".format(fp))
+ # TODO: accommodate this load use-case in internal icalendar module
+ cal = icalendar.Calendar.from_ical(Path(fp).read_bytes())
+ for ev in cal.events:
+ for d in ev.decoded('dtstart'):
+ zone = d.tzinfo.tzname(d)
+ if zone == 'UTC':
+ if skip_past and d < now:
+ logg.debug("skip past event: ".format(ev))
+ continue
+ self.add_event(ev)