v0.1.0: version in the status bar + one-click self-update via git pull

__version__ in lintunes/__init__.py (setup.py regex-reads it) shows
bottom-left via the new VersionButton; the tooltip carries the git short
hash so machines on the same version but different commits are
distinguishable. The new Updater fetches upstream 10s after launch and
every 4h on daemon threads (lastfm.py pattern); commits behind put a *
on the button, and clicking it confirms, runs `git pull --ff-only`,
quits cleanly (library flush + player shutdown), and re-execs
`python -m lintunes.main` on the new code. Positional file args are
stripped from the re-exec so they don't re-import as duplicates. Pull
failures surface the git error: line in the status bar and leave the
running app untouched; outside a git checkout the button is a plain
label.

Also fixes a Round-5 regression found while verifying: the totals label
was a permanent status-bar widget with stretch=1, which squeezed the
transient-message area to zero width — every showMessage (scrobbles,
tag-write errors, import status) has been invisible since. Both
readouts are now non-permanent widgets, so a transient message
temporarily replaces them and they return.
This commit is contained in:
2026-07-03 11:30:37 -04:00
parent 4fc630f00b
commit 98a20eead0
7 changed files with 425 additions and 7 deletions
+24 -3
View File
@@ -21,6 +21,8 @@ from lintunes.gui.transport import TransportBar
from lintunes.gui.info_dialog import InfoDialog
from lintunes.gui.preferences_dialog import PreferencesDialog
from lintunes.gui.track_table import format_total_time
from lintunes.gui.version_button import VersionButton
from lintunes.updater import Updater
def album_tracks(library, artist: str, album: str) -> list:
@@ -81,11 +83,23 @@ class MainWindow(QMainWindow):
self._content.addWidget(self._playlist_view)
self._content.setCurrentWidget(self._library_view)
# Bottom status bar: a permanent, centered totals readout (transient
# import/scrobble/error messages still use the left showMessage area).
# Bottom status bar, left to right: version/self-update button, then a
# centered totals readout. Both are non-permanent widgets, so a
# transient showMessage (import/scrobble/error/update text) replaces
# them for its duration and they come back — a permanent widget with
# stretch would squeeze the message area to zero width and swallow
# every message.
self.restart_requested = False # run_gui re-execs when True on quit
self._updater = Updater(self)
self._version_button = VersionButton(self._updater)
self.statusBar().addWidget(self._version_button)
self._totals_label = QLabel()
self._totals_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.statusBar().addPermanentWidget(self._totals_label, 1)
self.statusBar().addWidget(self._totals_label, 1)
self._version_button.status_message.connect(
lambda msg: self.statusBar().showMessage(msg, 8000))
self._updater.update_applied.connect(self._restart_for_update)
self._updater.start_checking()
# Wiring
self._transport.play_clicked.connect(self.play_pause)
@@ -127,6 +141,12 @@ class MainWindow(QMainWindow):
self.apply_ui_metrics()
self._update_totals()
def _restart_for_update(self):
# The pull already succeeded; quit through the normal path (flushes
# the library, tears down the player) and let main() re-exec us.
self.restart_requested = True
QApplication.instance().quit()
# ---- status bar totals ----
def _update_totals(self):
@@ -145,6 +165,7 @@ class MainWindow(QMainWindow):
theme.apply_theme(QApplication.instance(), self._prefs)
self.apply_ui_metrics()
self._transport.refresh_theme()
self._version_button.refresh_theme()
def apply_ui_metrics(self):
metrics = theme.scale_metrics(self._prefs)
+70
View File
@@ -0,0 +1,70 @@
"""The little version readout in the status bar's bottom-left corner.
Normally an inert label ("v0.1.0"; tooltip shows the git commit so identical
versions on different machines are still distinguishable). When the Updater
finds upstream commits it grows a "*" and becomes a button: click → confirm →
git pull → the main window restarts the app on `update_applied`.
"""
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import QMessageBox, QPushButton
from lintunes import __version__
class VersionButton(QPushButton):
status_message = pyqtSignal(str)
def __init__(self, updater, parent=None):
super().__init__(parent)
self._updater = updater
self._update_ready = False
self.setFlat(True)
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.refresh_theme()
self.setText(f"v{__version__}")
commit = updater.short_hash()
self._base_tooltip = (f"LinTunes v{__version__} ({commit})" if commit
else f"LinTunes v{__version__}")
self.setToolTip(self._base_tooltip)
updater.update_available.connect(self._on_update_available)
updater.update_failed.connect(self._on_update_failed)
self.clicked.connect(self._on_clicked)
def refresh_theme(self):
# Subtle but readable: the theme's text color at ~55% opacity
# (palette(mid) is too close to the light backgrounds to read).
text = self.palette().color(QPalette.ColorRole.WindowText)
self.setStyleSheet(
"QPushButton { border: none; padding: 0 6px; "
f"color: rgba({text.red()}, {text.green()}, {text.blue()}, 140); }}")
def _on_update_available(self, commits: int):
self._update_ready = True
self.setText(f"v{__version__}*")
noun = "commit" if commits == 1 else "commits"
self.setToolTip(f"Update available ({commits} {noun}) — "
"click to update and restart")
self.setCursor(Qt.CursorShape.PointingHandCursor)
def _on_clicked(self):
if not self._update_ready or not self.isEnabled():
return
if not self._confirm():
return
self.setEnabled(False)
self.setText("updating…")
self._updater.update_async()
def _confirm(self) -> bool:
answer = QMessageBox.question(
self.window(), "Update LinTunes",
"Pull the latest version and restart LinTunes?")
return answer == QMessageBox.StandardButton.Yes
def _on_update_failed(self, message: str):
self.setEnabled(True)
self.setText(f"v{__version__}*")
self.status_message.emit(f"Update failed: {message}")