diff options
| -rw-r--r-- | HANDOFF.md | 10 | ||||
| -rw-r--r-- | TODO.md | 6 | ||||
| -rw-r--r-- | ui/hugo_panel.py | 70 | ||||
| -rw-r--r-- | workers/npm_worker.py | 38 |
4 files changed, 109 insertions, 15 deletions
@@ -1,14 +1,14 @@ Who this is for: - Danilo is a developer and blogger running a bilingual (IT/EN) Hugo site at danix.xyz. He maintains a PyQt6 desktop app called my-publisher that centralizes his blog publishing workflow, and is continuing to extend it post-v1.2. + Danilo is a developer and blogger running a bilingual (IT/EN) Hugo site at danix.xyz. He maintains a PyQt6 desktop app called my-publisher that centralizes his blog publishing workflow, and is continuing to extend it post-v1.3. What we covered: - Three changes were implemented this session. First, multi-token tag autocomplete was added to FrontmatterEditor: a MultiTokenCompleter subclass of QCompleter intercepts textEdited, extracts the last comma-separated token as the completion prefix, and on activation inserts the completed tag followed by ", " so the user can immediately type the next tag. Does not call setCompleter() — wires via setWidget() + signals to bypass Qt's built-in prefix detection. Second, tag rename propagation was added to TaxonomyView: _save() now snapshots the old it_to_en dict before modifying, calls _detect_renames() (static method, compares old vs new dicts to find IT and EN renames), then _propagate_renames() scans all articles via scan_articles() and rewrites frontmatter for any article whose tags include a renamed value. Status bar shows count of updated articles. Propagation only runs after save_taxonomy() succeeds. Third, a pre-existing bug was fixed in FrontmatterEditor._save(): it was passing empty string as body to write_frontmatter(), erasing all article content below the +++ delimiter. Fixed by reading body via parse_frontmatter() first. + Four bugs were fixed this session. First, the categories field in FrontmatterEditor was displaying Python list repr (e.g. "['cat1', 'cat2']") instead of a comma-separated string, and saving it back as a corrupted TOML string. Fixed by adding an isinstance(val, list) branch in _build_ui() and generalising the save logic to detect list-typed fields by checking the original frontmatter value type rather than hardcoding "tags". Second, the article type combo box was defaulting to "Life" instead of the article's actual type, because ARTICLE_TYPES used titlecase while files store lowercase values. Fixed by lowercasing ARTICLE_TYPES and doing a case-insensitive match on load. Third, on save the type was being written back in titlecase instead of lowercase. Fixed by the same ARTICLE_TYPES change. Fourth, categories autocomplete was added to FrontmatterEditor: a MultiTokenCompleter (already used for tags) is now also attached to the categories QLineEdit, loading suggestions from docs/categories.txt via load_categories(). All 34 tests pass. Three commits were made and pushed. v1.4 was tagged and pushed. What was confirmed: - All 34 tests pass. pytest-qt added to requirements.txt for the _detect_renames unit tests. The body-erasure bug was pre-existing since v1.0 — any article saved through FrontmatterEditor before this fix lost its content. MultiTokenCompleter does not call widget.setCompleter() because that re-enables Qt's built-in single-token prefix logic. Tag rename propagation is transactional: taxonomy save failure aborts propagation entirely. v1.3 tagged and pushed. + All 34 tests pass after every change. No articles in the blog repo had corrupted categories fields — the corruption seen in earlier exploration was not present in the actual files. ARTICLE_TYPES is now ["life", "photo", "link", "quote", "tech"]. The save path for both tags and categories now uses isinstance(self._article.frontmatter.get(key), list) to detect list fields generically. MultiTokenCompleter loads categories from docs/categories.txt (a single non-lang-split file, unlike tags which have tags-it.txt and tags-en.txt). v1.4 is live on origin/master. Still in progress: - Nothing was left open in this session. Continue with items from the TODO.md list. + Nothing was left open in this session. Next steps: - Many TODO items remain. Suggested order: (1) fix the taxonomies bug when modifying from the frontmatter editor; (2) transart backend availability check using OLLAMA_HOST — validate before translation starts or disable the translate button with a popup reminder to activate RunPod. (3) font size spinner in SetupDialog (ui/setup_dialog.py) — add QSpinBox, save via Config.save(), re-apply with app.setFont(); + Remaining TODO items include: (1) npm run build integration before launching hugo server, with a button to trigger it while the server is running; (2) transart backend availability check using OLLAMA_HOST — validate before translation starts or disable the translate button with a popup; (3) font size spinner in SetupDialog — add QSpinBox, save via Config.save(), re-apply with app.setFont(); (4) article editor language flag in the sidebar; (5) manage static pages in addition to articles.
\ No newline at end of file @@ -21,10 +21,10 @@ ### taxonomies - [✅] when modifying the frontmatter, the square brackets around the tags or categories lists are not maintained. ### article types -- [ ] the program should respect the article type already set in the frontmatter and don't default to "Life" if the article is set to something else. -- [ ] the theme expects the article type to be lowercase. The program currently writes them as uppercase +- [✅] the program should respect the article type already set in the frontmatter and don't default to "Life" if the article is set to something else. +- [✅] the theme expects the article type to be lowercase. The program currently writes them as uppercase ### categories dropdown -- [ ] when adding a category to the article, offer a dropdown of the categories or autocomplete like we do for tags. +- [✅] when adding a category to the article, offer a dropdown of the categories or autocomplete like we do for tags. ## Settings - [ ] **Font size spinner in UI** — `font_size` is in config but only editable via `~/.config/my-publisher/config.toml`. Add a `QSpinBox` to `SetupDialog` (or a dedicated Settings dialog) so it's changeable without editing the file. Save via `Config.save()`, re-apply with `app.setFont()`. File: `ui/setup_dialog.py`. diff --git a/ui/hugo_panel.py b/ui/hugo_panel.py index fafe59c..bf13bd8 100644 --- a/ui/hugo_panel.py +++ b/ui/hugo_panel.py @@ -7,15 +7,21 @@ from PyQt6.QtWidgets import ( from PyQt6.QtCore import QUrl from PyQt6.QtGui import QDesktopServices from workers.hugo_worker import HugoWorker +from workers.npm_worker import NpmWorker class HugoPanel(QWidget): def __init__(self, repo_path: Path, parent=None): super().__init__(parent) self._worker = HugoWorker(repo_path, parent=self) self._worker.log_line.connect(self._on_log) - self._worker.started.connect(self._on_started) - self._worker.stopped.connect(self._on_stopped) + self._worker.started.connect(self._on_hugo_started) + self._worker.stopped.connect(self._on_hugo_stopped) self._worker.error.connect(lambda e: self._log.appendPlainText(f"[ERRORE] {e}")) + + self._npm = NpmWorker(repo_path, parent=self) + self._npm.log_line.connect(self._on_log) + self._npm.finished.connect(self._on_build_finished) + self._build_ui() def _build_ui(self): @@ -28,12 +34,15 @@ class HugoPanel(QWidget): top.addStretch() self._start_btn = QPushButton("▶ Avvia") - self._start_btn.clicked.connect(self._worker.start) + self._start_btn.clicked.connect(self._on_start_clicked) self._stop_btn = QPushButton("⏹ Ferma") self._stop_btn.clicked.connect(self._worker.stop) self._stop_btn.setEnabled(False) + self._build_btn = QPushButton("🔨 Build CSS") + self._build_btn.clicked.connect(self._on_build_clicked) top.addWidget(self._start_btn) top.addWidget(self._stop_btn) + top.addWidget(self._build_btn) self._url_btn = QPushButton("🌐 http://localhost:1313") self._url_btn.setFlat(True) @@ -49,23 +58,70 @@ class HugoPanel(QWidget): self._log.setStyleSheet("background:#0a0a12;color:#00ff88;font-family:monospace;font-size:11px;") layout.addWidget(self._log) - def _on_log(self, line: str): - self._log.appendPlainText(line) + # --- button handlers --- + + def _on_start_clicked(self): + self._status_label.setText("🟡 Build CSS...") + self._status_label.setStyleSheet("color:#fbbf24;font-weight:bold;") + self._start_btn.setEnabled(False) + self._stop_btn.setEnabled(False) + self._build_btn.setEnabled(False) + self._log.appendPlainText("─── npm run build ───") + self._npm.run() + + def _on_build_clicked(self): + self._build_btn.setEnabled(False) + self._log.appendPlainText("─── npm run build ───") + self._npm.run() - def _on_started(self): + # --- npm worker callbacks --- + + def _on_build_finished(self, success: bool): + if success: + self._log.appendPlainText("─── build OK ───") + if not self._worker.is_running: + # launched via Avvia → now start hugo + self._log.appendPlainText("─── hugo server avvia ───") + self._worker.start() + else: + # manual rebuild while server running + self._build_btn.setEnabled(True) + else: + self._log.appendPlainText("─── build FALLITO ───") + if not self._worker.is_running: + # failed before hugo started → restore idle state + self._status_label.setText("🔴 Hugo server: fermo") + self._status_label.setStyleSheet("color:#ff6b6b;font-weight:bold;") + self._start_btn.setEnabled(True) + self._build_btn.setEnabled(True) + else: + self._build_btn.setEnabled(True) + + # --- hugo worker callbacks --- + + def _on_hugo_started(self): self._status_label.setText("🟢 Hugo server: in esecuzione") self._status_label.setStyleSheet("color:#00ff88;font-weight:bold;") self._start_btn.setEnabled(False) self._stop_btn.setEnabled(True) + self._build_btn.setEnabled(True) self._url_btn.setVisible(True) - def _on_stopped(self): + def _on_hugo_stopped(self): self._status_label.setText("🔴 Hugo server: fermo") self._status_label.setStyleSheet("color:#ff6b6b;font-weight:bold;") self._start_btn.setEnabled(True) self._stop_btn.setEnabled(False) + self._build_btn.setEnabled(True) self._url_btn.setVisible(False) + # --- log --- + + def _on_log(self, line: str): + self._log.appendPlainText(line) + + # --- lifecycle --- + def stop_server(self): self._worker.stop() diff --git a/workers/npm_worker.py b/workers/npm_worker.py new file mode 100644 index 0000000..b8c04af --- /dev/null +++ b/workers/npm_worker.py @@ -0,0 +1,38 @@ +from __future__ import annotations +from PyQt6.QtCore import QObject, QProcess, pyqtSignal +from pathlib import Path + +class NpmWorker(QObject): + log_line = pyqtSignal(str) + finished = pyqtSignal(bool) # True = success + + def __init__(self, repo_path: Path, parent=None): + super().__init__(parent) + self._repo_path = repo_path + self._process = QProcess(self) + self._process.readyReadStandardOutput.connect(self._on_stdout) + self._process.readyReadStandardError.connect(self._on_stderr) + self._process.finished.connect(self._on_finished) + + @property + def is_running(self) -> bool: + return self._process.state() == QProcess.ProcessState.Running + + def run(self): + if self.is_running: + return + self._process.setWorkingDirectory(str(self._repo_path)) + self._process.start("npm", ["run", "build"]) + + def _on_stdout(self): + data = self._process.readAllStandardOutput().data().decode("utf-8", errors="replace") + for line in data.splitlines(): + self.log_line.emit(line) + + def _on_stderr(self): + data = self._process.readAllStandardError().data().decode("utf-8", errors="replace") + for line in data.splitlines(): + self.log_line.emit(line) + + def _on_finished(self, exit_code: int, _): + self.finished.emit(exit_code == 0) |
