From 8b26bc33792fad1b4db247ed9978dc03eb2b0197 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Wed, 10 Jun 2026 01:47:47 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20wakeword=20results=20table=20=E2=80=94?= =?UTF-8?q?=20sortable=20columns=20+=20CSV=20export=20(v2.03.21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - All columns sortable by clicking header; numeric cols sort numerically - "Copy as CSV" → clipboard; "Save CSV…" → file chooser Co-Authored-By: Claude Sonnet 4.6 --- linux/CHANGELOG.md | 9 ++++ linux/blitztext/__init__.py | 2 +- linux/blitztext/gtksettings.py | 85 +++++++++++++++++++++++++++++++--- 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/linux/CHANGELOG.md b/linux/CHANGELOG.md index a84da1c..99bd221 100644 --- a/linux/CHANGELOG.md +++ b/linux/CHANGELOG.md @@ -9,6 +9,15 @@ The version is defined in [`blitztext/__init__.py`](blitztext/__init__.py). ## [Unreleased] +## [2.03.21] - 2026-06-10 + +### Added +- **Wakeword results table: sortable columns.** Click any column header to sort + ascending/descending. Numeric columns (Detected, Total, Recall %, False fires, + Time) sort numerically. +- **Wakeword results table: CSV export.** "Copy as CSV" copies the table to the + clipboard; "Save CSV…" opens a file chooser to write a `.csv` file. + ## [2.03.20] - 2026-06-10 ### Added diff --git a/linux/blitztext/__init__.py b/linux/blitztext/__init__.py index aba6989..9da2e7e 100644 --- a/linux/blitztext/__init__.py +++ b/linux/blitztext/__init__.py @@ -6,4 +6,4 @@ counterpart to the macOS Blitztext menu bar app: it runs natively on the host (not in a container) so it can type into any application via xdotool. """ -__version__ = "2.03.20" +__version__ = "2.03.21" diff --git a/linux/blitztext/gtksettings.py b/linux/blitztext/gtksettings.py index 125dd78..259af3f 100644 --- a/linux/blitztext/gtksettings.py +++ b/linux/blitztext/gtksettings.py @@ -2427,9 +2427,21 @@ notebook.bt-nb tab:checked label { # Results table: Engine | Wakeword | Voice | Detected | Total | Recall% | False fires | Time # Col indices: 0 1 2 3 4 5 6 7 + # col 8 = foreground colour (not displayed) self.wwb_store = Gtk.ListStore(str, str, str, str, str, str, str, str, str) - ww_tree = Gtk.TreeView(model=self.wwb_store) - ww_tree.set_grid_lines(Gtk.TreeViewGridLines.HORIZONTAL) + wwb_sort = Gtk.TreeModelSort(model=self.wwb_store) + + def _num_cmp_wwb(model, a, b, col): + def _f(v): + try: + return float(str(v).rstrip("%")) + except (ValueError, AttributeError): + return float("inf") + va, vb = _f(model.get_value(a, col)), _f(model.get_value(b, col)) + return (va > vb) - (va < vb) + + self.wwb_tree = Gtk.TreeView(model=wwb_sort) + self.wwb_tree.set_grid_lines(Gtk.TreeViewGridLines.HORIZONTAL) for title, i, expand in [ ("Engine", 0, False), ("Wakeword", 1, False), @@ -2443,12 +2455,29 @@ notebook.bt-nb tab:checked label { r.set_property("ellipsize", Pango.EllipsizeMode.END) col = Gtk.TreeViewColumn(title, r, text=i, foreground=8) col.set_resizable(True); col.set_expand(expand) - ww_tree.append_column(col) + col.set_sort_column_id(i) + self.wwb_tree.append_column(col) + if i in (3, 4, 5, 6, 7): + wwb_sort.set_sort_func(i, _num_cmp_wwb, i) + + # Toolbar: Copy CSV + Save CSV + tb = Gtk.Box(spacing=6) + tb.set_margin_top(6) + _copy_btn = Gtk.Button(label="Copy as CSV") + _copy_btn.set_tooltip_text("Copy results to clipboard as comma-separated values") + _copy_btn.connect("clicked", self._wwbench_copy_csv) + _save_btn = Gtk.Button(label="Save CSV…") + _save_btn.set_tooltip_text("Save results to a .csv file") + _save_btn.connect("clicked", self._wwbench_save_csv) + tb.pack_start(_copy_btn, False, False, 0) + tb.pack_start(_save_btn, False, False, 0) + page.pack_start(tb, False, False, 0) + ww_sw = Gtk.ScrolledWindow() ww_sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - ww_sw.set_min_content_height(120) - ww_sw.add(ww_tree) - page.pack_start(ww_sw, True, True, 4) + ww_sw.set_min_content_height(150) + ww_sw.add(self.wwb_tree) + page.pack_start(ww_sw, True, True, 2) self.wwb_summary = Gtk.Label(xalign=0.0); self.wwb_summary.set_line_wrap(True) self.wwb_summary.set_selectable(True) @@ -2748,6 +2777,50 @@ notebook.bt-nb tab:checked label { GLib.markup_escape_text(p) for p in summary_parts)) return False + _WWB_CSV_HEADERS = ["Engine", "Wakeword", "Voice", "Detected", "Total", + "Recall %", "False fires", "Time (s)"] + + def _wwbench_csv_text(self) -> str: + import csv, io + out = io.StringIO() + w = csv.writer(out) + w.writerow(self._WWB_CSV_HEADERS) + store = self.wwb_store + it = store.get_iter_first() + while it: + w.writerow([store.get_value(it, c) for c in range(8)]) + it = store.iter_next(it) + return out.getvalue() + + def _wwbench_copy_csv(self, _b) -> None: + if not self.wwb_store.get_iter_first(): + return + Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD).set_text(self._wwbench_csv_text(), -1) + + def _wwbench_save_csv(self, _b) -> None: + if not self.wwb_store.get_iter_first(): + return + dlg = Gtk.FileChooserDialog( + title="Save wakeword results as CSV", + transient_for=self.dlg, + action=Gtk.FileChooserAction.SAVE) + dlg.add_button("_Cancel", Gtk.ResponseType.CANCEL) + dlg.add_button("_Save", Gtk.ResponseType.ACCEPT) + dlg.set_do_overwrite_confirmation(True) + dlg.set_current_name("wakeword_benchmark.csv") + f = Gtk.FileFilter(); f.set_name("CSV files (*.csv)"); f.add_pattern("*.csv") + dlg.add_filter(f) + if dlg.run() == Gtk.ResponseType.ACCEPT: + path = dlg.get_filename() + if path and not path.endswith(".csv"): + path += ".csv" + try: + with open(path, "w", encoding="utf-8", newline="") as fh: + fh.write(self._wwbench_csv_text()) + except OSError as exc: + self._error(f"Could not save: {exc}") + dlg.destroy() + # ===== Manual =========================================================== def _build_manual(self, page: Gtk.Box) -> None: text = _read_first(_app_paths()["manual"])