From 4ea027f7c2d33e75de0bd89052a02b347c235134 Mon Sep 17 00:00:00 2001 From: Glyph Date: Fri, 6 Sep 2024 12:54:54 -0700 Subject: [PATCH 01/52] interface definition for session start/end This commit was sponsored by Derek Veit, tgs, Carlton Gibson, and my other patrons. If you want to join them, you can support my work at https://glyph.im/patrons/. --- src/pomodouroboros/macos/mac_gui.py | 8 +++++++- src/pomodouroboros/model/boundaries.py | 17 +++++++++++++++++ src/pomodouroboros/model/test/test_model.py | 6 ++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/pomodouroboros/macos/mac_gui.py b/src/pomodouroboros/macos/mac_gui.py index 54d7d03..753c47e 100644 --- a/src/pomodouroboros/macos/mac_gui.py +++ b/src/pomodouroboros/macos/mac_gui.py @@ -35,7 +35,7 @@ ) from ..model.nexus import Nexus from ..model.observables import Changes, IgnoreChanges, SequenceObserver -from ..model.sessions import DailySessionRule, Weekday +from ..model.sessions import DailySessionRule, Weekday, Session from ..model.storage import loadDefaultNexus from ..model.util import ( AMPM, @@ -88,6 +88,12 @@ def startPromptUpdate(self, startPrompt: StartPrompt) -> None: def describeCurrentState(self, description: str) -> None: ... + def sessionStarted(self, session: Session) -> None: + "TODO" + + def sessionEnded(self): + "TODO" + def intervalStart(self, interval: AnyIntervalOrIdle) -> None: self.currentInterval = interval match interval: diff --git a/src/pomodouroboros/model/boundaries.py b/src/pomodouroboros/model/boundaries.py index d16aa3e..e296da8 100644 --- a/src/pomodouroboros/model/boundaries.py +++ b/src/pomodouroboros/model/boundaries.py @@ -10,6 +10,7 @@ from .intention import Estimate, Intention from .intervals import AnyIntervalOrIdle, Pomodoro from .nexus import Nexus + from .sessions import Session class IntervalType(Enum): @@ -69,6 +70,16 @@ def intervalEnd(self) -> None: The interval has ended. Hide the progress bar. """ + def sessionStarted(self, session: Session) -> None: + """ + A session has started, and is now running. + """ + + def sessionEnded(self) -> None: + """ + The currently running session has ended. + """ + def intentionListObserver(self) -> SequenceObserver[Intention]: """ Return a change observer for the full list of L{Intention}s. @@ -121,6 +132,12 @@ def intervalProgress(self, percentComplete: float) -> None: def intervalEnd(self) -> None: ... + def sessionStarted(self, session: Session) -> None: + ... + + def sessionEnded(self) -> None: + ... + def intentionListObserver(self) -> SequenceObserver[Intention]: """ Return a change observer for the full list of L{Intention}s. diff --git a/src/pomodouroboros/model/test/test_model.py b/src/pomodouroboros/model/test/test_model.py index 00601e7..d104dee 100644 --- a/src/pomodouroboros/model/test/test_model.py +++ b/src/pomodouroboros/model/test/test_model.py @@ -64,6 +64,12 @@ def intervalProgress(self, percentComplete: float) -> None: assert self.actualInterval is not None self.actualInterval.currentProgress.append(percentComplete) + def sessionStarted(self, session: Session) -> None: + ... + + def sessionEnded(self) -> None: + ... + def intervalStart(self, interval: AnyIntervalOrIdle) -> None: """ An interval has started, record it. From 0500916a026e4ce3384a4a91128bc776c1e82c2d Mon Sep 17 00:00:00 2001 From: Glyph Date: Fri, 6 Sep 2024 13:21:50 -0700 Subject: [PATCH 02/52] make it possible to get the maximum possible length of a streak This commit was sponsored by Jason Mills, Carlton Gibson, Steven S., and my other patrons. If you want to join them, you can support my work at https://glyph.im/patrons/. --- src/pomodouroboros/model/nexus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pomodouroboros/model/nexus.py b/src/pomodouroboros/model/nexus.py index 92d61da..7ed11db 100644 --- a/src/pomodouroboros/model/nexus.py +++ b/src/pomodouroboros/model/nexus.py @@ -41,7 +41,7 @@ class StreakRules: The rules for what intervals should be part of a streak. """ - streakIntervalDurations: Iterable[Duration] = field( + streakIntervalDurations: Sequence[Duration] = field( default_factory=lambda: [ each for pomMinutes, breakMinutes in [ From ec0acde194395694f2a0acd80849f5fe07899a28 Mon Sep 17 00:00:00 2001 From: Glyph Date: Mon, 9 Sep 2024 12:05:39 -0700 Subject: [PATCH 03/52] hook up next key view to tab out of the date selector This commit was sponsored by Sergio Bost, Carlton Gibson, Steven S., and my other patrons. If you want to join them, you can support my work at https://glyph.im/patrons/. --- IBFiles/GoalListWindow.xib | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/IBFiles/GoalListWindow.xib b/IBFiles/GoalListWindow.xib index 5d1fed5..eafc007 100644 --- a/IBFiles/GoalListWindow.xib +++ b/IBFiles/GoalListWindow.xib @@ -1,7 +1,7 @@ - + @@ -30,8 +30,8 @@ - - + + @@ -39,6 +39,9 @@ + + + From 5e9e005cd73df6a3af86ab6312048132ad130f11 Mon Sep 17 00:00:00 2001 From: Glyph Date: Wed, 11 Sep 2024 17:08:08 -0700 Subject: [PATCH 04/52] factor out animation into a separate function This commit was sponsored by Devin Prater, Matt Campbell, hacklschorsch, and my other patrons. If you want to join them, you can support my work at https://glyph.im/patrons/. --- src/pomodouroboros/macos/progress_hud.py | 101 ++++++++++++++--------- 1 file changed, 64 insertions(+), 37 deletions(-) diff --git a/src/pomodouroboros/macos/progress_hud.py b/src/pomodouroboros/macos/progress_hud.py index 3ac987b..3518d01 100644 --- a/src/pomodouroboros/macos/progress_hud.py +++ b/src/pomodouroboros/macos/progress_hud.py @@ -3,11 +3,10 @@ import math from dataclasses import dataclass, field from math import cos, pi, sin, sqrt -from typing import TYPE_CHECKING, Callable, List, Self +from typing import TYPE_CHECKING, Callable, List, Protocol, Self from AppKit import ( NSApp, - NSPanel, NSAttributedString, NSBackingStoreBuffered, NSBackingStoreType, @@ -21,7 +20,9 @@ NSFont, NSFontAttributeName, NSForegroundColorAttributeName, + NSHUDWindowMask, NSMakePoint, + NSPanel, NSRectFill, NSRectFillListWithColorsUsingOperation, NSScreen, @@ -29,13 +30,12 @@ NSStrokeWidthAttributeName, NSView, NSWindow, + NSWindowCollectionBehaviorAuxiliary, + NSWindowCollectionBehaviorCanJoinAllApplications, NSWindowCollectionBehaviorMoveToActiveSpace, NSWindowCollectionBehaviorStationary, - NSWindowCollectionBehaviorAuxiliary, NSWindowStyleMask, - NSHUDWindowMask, ) -from AppKit import NSWindowCollectionBehaviorCanJoinAllApplications from Foundation import NSPoint, NSRect from objc import super from twisted.internet.defer import CancelledError, Deferred @@ -45,7 +45,7 @@ from twisted.python.failure import Failure from ..model.debugger import debug -from ..model.util import showFailures, fallible +from ..model.util import fallible, showFailures from ..storage import TEST_MODE log = Logger() @@ -264,6 +264,45 @@ def textOpacityCurve(startTime: float, duration: float, now: float): return sin(((progressPercent * oomph) + oomph) * (pi / 2)) * maxOpacity +class AnimValues(Protocol): + def setPercentage(self, percentage: float) -> None: ... + + def setAlpha(self, alpha: float) -> None: ... + + +def animatePct( + values: AnimValues, + clock: IReactorTime, + percentageElapsed: float, + previousPercentageElapsed: float, + pulseTime: float, + baseAlphaValue: float, + alphaVariance: float, +) -> Deferred[None]: + if percentageElapsed < previousPercentageElapsed: + previousPercentageElapsed = 0 + elapsedDelta = percentageElapsed - previousPercentageElapsed + startTime = clock.seconds() + + def updateSome() -> None: + now = clock.seconds() + percentDone = (now - startTime) / pulseTime + easedEven = math.sin((percentDone * math.pi)) + easedUp = math.sin((percentDone * math.pi) / 2.0) + values.setPercentage( + previousPercentageElapsed + (easedUp * elapsedDelta) + ) + if percentDone >= 1.0: + alphaValue = baseAlphaValue + lc.stop() + else: + alphaValue = (easedEven * alphaVariance) + baseAlphaValue + values.setAlpha(alphaValue) + + lc = LoopingCall(updateSome) + return lc.start(1.0 / 30.0).addCallback(lambda ignored: None) + + @dataclass class ProgressController(object): """ @@ -322,14 +361,14 @@ def updateText(): lc = LoopingCall(updateText) lc.clock = clock - def clear(o: object) -> object: + def clearTRIP(o: object) -> object: self._textReminderInProgress = None return o self._textReminderInProgress = ( lc.start(1 / 30) .addErrback(lambda f: f.trap(CancelledError)) - .addBoth(clear) + .addBoth(clearTRIP) ) def animatePercentage( @@ -356,37 +395,25 @@ def animatePercentage( self.pulseCounter += 1 if self.pulseCounter % 3 == 0: self._textReminder(clock) - startTime = clock.seconds() - previousPercentageElapsed = self.percentage - if percentageElapsed < previousPercentageElapsed: - previousPercentageElapsed = 0 - elapsedDelta = percentageElapsed - previousPercentageElapsed - def updateSome() -> None: - now = clock.seconds() - percentDone = (now - startTime) / pulseTime - easedEven = math.sin((percentDone * math.pi)) - easedUp = math.sin((percentDone * math.pi) / 2.0) - self.setPercentage( - previousPercentageElapsed + (easedUp * elapsedDelta) - ) - if percentDone >= 1.0: - alphaValue = baseAlphaValue - lc.stop() - else: - alphaValue = (easedEven * alphaVariance) + baseAlphaValue - self.setAlpha(alphaValue) - - lc = LoopingCall(updateSome) - - def clear(ignored: object) -> None: + def clearAnim(result: object | Failure) -> None: self._animationInProgress = None - if isinstance(ignored, Failure): - log.failure("while animating", ignored) - - self._animationInProgress = lc.start(1.0 / 30.0).addCallback(clear) + if isinstance(result, Failure): + log.failure("while animating", result) + + animDone = animatePct( + values=self, + clock=clock, + percentageElapsed=percentageElapsed, + previousPercentageElapsed=self.percentage, + pulseTime=pulseTime, + baseAlphaValue=baseAlphaValue, + alphaVariance=alphaVariance, + ) + self._animationInProgress = animDone + animDone.addBoth(clearAnim) self.show() - return self._animationInProgress + return animDone def setPercentage(self, percentage: float) -> None: """ @@ -598,7 +625,7 @@ def _circledTextWithAlpha( aString.drawAtPoint_(stringPoint) -clear = NSColor.clearColor() +clear: NSColor = NSColor.clearColor() def pct2deg(pct: float) -> float: From c39d93d033ad07a47d507c25a1b6aada39f7365a Mon Sep 17 00:00:00 2001 From: Glyph Date: Wed, 11 Sep 2024 17:30:37 -0700 Subject: [PATCH 05/52] record session changes This commit was sponsored by Devin Prater, Matt Campbell, Jason Mills, and my other patrons. If you want to join them, you can support my work at https://glyph.im/patrons/. --- src/pomodouroboros/model/test/test_model.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/pomodouroboros/model/test/test_model.py b/src/pomodouroboros/model/test/test_model.py index d104dee..caebedb 100644 --- a/src/pomodouroboros/model/test/test_model.py +++ b/src/pomodouroboros/model/test/test_model.py @@ -42,6 +42,18 @@ class TestInterval: T = TypeVar("T") +@dataclass +class SessionChange: + session: Session + startTime: float + progress: list[float] = field(default_factory=list) + endTime: float | None = None + + def setEndTime(self, newEndTime: float) -> None: + assert self.endTime is None, f"session already ended at {self.endTime}" + self.endTime = newEndTime + + @dataclass class TestUserInterface: """ @@ -52,6 +64,7 @@ class TestUserInterface: clock: IReactorTime actions: list[TestInterval] = field(default_factory=list) actualInterval: TestInterval | None = None + sessionChanges: list[SessionChange] = field(default_factory=list) def describeCurrentState(self, description: str) -> None: ... @@ -65,10 +78,12 @@ def intervalProgress(self, percentComplete: float) -> None: self.actualInterval.currentProgress.append(percentComplete) def sessionStarted(self, session: Session) -> None: - ... + self.sessionChanges.append( + SessionChange(session, self.clock.seconds()) + ) def sessionEnded(self) -> None: - ... + self.sessionChanges[-1].setEndTime(self.clock.seconds()) def intervalStart(self, interval: AnyIntervalOrIdle) -> None: """ From 7368687aeadf48933dd8660d1f1deff143ba73d6 Mon Sep 17 00:00:00 2001 From: Glyph Date: Fri, 13 Sep 2024 17:27:06 -0700 Subject: [PATCH 06/52] line-length This commit was sponsored by Matt Campbell, Jason Mills, and my other patrons. If you want to join them, you can support my work at https://glyph.im/patrons/. --- src/pomodouroboros/model/nexus.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pomodouroboros/model/nexus.py b/src/pomodouroboros/model/nexus.py index 7ed11db..f371ed1 100644 --- a/src/pomodouroboros/model/nexus.py +++ b/src/pomodouroboros/model/nexus.py @@ -309,7 +309,8 @@ def _activeSession(self, oldTime: float, newTime: float) -> Session | None: ), f"{created.start}, {created.end}" assert newEnd > fromWhenT, f"{newEnd} <= {fromWhenT}" if created.end > newTime: - # Don't create sessions that are already over at the current moment. + # Don't create sessions that are already over at the + # current moment. self._sessions.append(created) thisOldTime = created.end else: From b3b98e2ba9766235a90cfd5608a4db54ec944633 Mon Sep 17 00:00:00 2001 From: Glyph Date: Fri, 13 Sep 2024 17:27:11 -0700 Subject: [PATCH 07/52] why use an if statement when a match will do This commit was sponsored by Jason Mills, and my other patrons. If you want to join them, you can support my work at https://glyph.im/patrons/. --- src/pomodouroboros/model/nexus.py | 120 ++++++++++++++++-------------- 1 file changed, 63 insertions(+), 57 deletions(-) diff --git a/src/pomodouroboros/model/nexus.py b/src/pomodouroboros/model/nexus.py index f371ed1..1fef3c7 100644 --- a/src/pomodouroboros/model/nexus.py +++ b/src/pomodouroboros/model/nexus.py @@ -350,65 +350,71 @@ def advanceToTime(self, newTime: float) -> None: earlyEvaluationSpecialCase = False newInterval: AnyStreakInterval | None = None currentInterval = self._activeInterval - if isinstance(currentInterval, Idle): - # If there's no current interval then there's nothing to end - # and we can skip forward to current time, and let the start - # prompt just begin at the current time, not some point in the - # past where some reminder *might* have been appropriate. - oldTime = self._lastUpdateTime - self._lastUpdateTime = newTime - debug("interval None, update to real time", newTime) - activeSession = self._activeSession(oldTime, newTime) - if activeSession is not None: - scoreInfo = activeSession.idealScoreFor(self) - nextDrop = scoreInfo.nextPointLoss - if nextDrop is not None and nextDrop > newTime: - newInterval = StartPrompt( - self._lastUpdateTime, - nextDrop, - scoreInfo.scoreBeforeLoss(), - scoreInfo.scoreAfterLoss(), - ) - else: - if newTime >= currentInterval.endTime: - self._lastUpdateTime = currentInterval.endTime - - if currentInterval.intervalType in { - GracePeriod.intervalType, - StartPrompt.intervalType, - }: - # New streaks begin when grace periods expire. - self._upcomingDurations = iter(()) - - newDuration = next(self._upcomingDurations, None) - self.userInterface.intervalProgress(1.0) - self.userInterface.intervalEnd() - if newDuration is None: - # XXX needs test coverage - previous, self._currentStreak = self._currentStreak, [] - assert ( - previous - ), "rolling off the end of a streak but the streak is empty somehow" - self._previousStreaks.append(previous) + match currentInterval: + case Idle(): + # If there's no current interval then there's nothing to end + # and we can skip forward to current time, and let the start + # prompt just begin at the current time, not some point in the + # past where some reminder *might* have been appropriate. + oldTime = self._lastUpdateTime + self._lastUpdateTime = newTime + debug("interval None, update to real time", newTime) + activeSession = self._activeSession(oldTime, newTime) + if activeSession is not None: + scoreInfo = activeSession.idealScoreFor(self) + nextDrop = scoreInfo.nextPointLoss + if nextDrop is not None and nextDrop > newTime: + newInterval = StartPrompt( + self._lastUpdateTime, + nextDrop, + scoreInfo.scoreBeforeLoss(), + scoreInfo.scoreAfterLoss(), + ) + case _: + if newTime >= currentInterval.endTime: + self._lastUpdateTime = currentInterval.endTime + + if currentInterval.intervalType in { + GracePeriod.intervalType, + StartPrompt.intervalType, + }: + # New streaks begin when grace periods expire. + self._upcomingDurations = iter(()) + + newDuration = next(self._upcomingDurations, None) + self.userInterface.intervalProgress(1.0) + self.userInterface.intervalEnd() + if newDuration is None: + # XXX needs test coverage + previous, self._currentStreak = ( + self._currentStreak, + [], + ) + assert ( + previous + ), "rolling off the end of a streak but the streak is empty somehow" + self._previousStreaks.append(previous) + else: + newInterval = preludeIntervalMap[ + newDuration.intervalType + ]( + currentInterval.endTime, + currentInterval.endTime + newDuration.seconds, + ) else: - newInterval = preludeIntervalMap[ - newDuration.intervalType - ]( - currentInterval.endTime, - currentInterval.endTime + newDuration.seconds, + # We're landing in the middle of an interval, so we need to + # update its progress. If it's in the middle then we can + # move time all the way forward. + self._lastUpdateTime = newTime + elapsedWithinInterval = ( + newTime - currentInterval.startTime + ) + intervalDuration = ( + currentInterval.endTime - currentInterval.startTime + ) + self.userInterface.intervalProgress( + elapsedWithinInterval / intervalDuration ) - else: - # We're landing in the middle of an interval, so we need to - # update its progress. If it's in the middle then we can - # move time all the way forward. - self._lastUpdateTime = newTime - elapsedWithinInterval = newTime - currentInterval.startTime - intervalDuration = ( - currentInterval.endTime - currentInterval.startTime - ) - self.userInterface.intervalProgress( - elapsedWithinInterval / intervalDuration - ) # if we created a new interval for any reason on this iteration # through the loop, then we need to mention that fact to the UI. From f64f0f89ceaa4500166443e77b0f10ee5b2690e5 Mon Sep 17 00:00:00 2001 From: Glyph Date: Wed, 9 Oct 2024 16:24:26 -0700 Subject: [PATCH 08/52] sometimes, Xcode does something This commit was sponsored by Seth Larson, Jason Mills, and my other patrons. If you want to join them, you can support my work at https://glyph.im/patrons/. --- IBFiles/GoalListWindow.xib | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/IBFiles/GoalListWindow.xib b/IBFiles/GoalListWindow.xib index eafc007..af6ded9 100644 --- a/IBFiles/GoalListWindow.xib +++ b/IBFiles/GoalListWindow.xib @@ -1,7 +1,7 @@ - + - + @@ -30,8 +30,8 @@ - - + + From 9e2c13ecff484227f0e9d87c682aeae680aeaf24 Mon Sep 17 00:00:00 2001 From: Glyph Date: Wed, 9 Oct 2024 16:24:54 -0700 Subject: [PATCH 09/52] disallow_untyped_defs This commit was sponsored by Jason Mills, and my other patrons. If you want to join them, you can support my work at https://glyph.im/patrons/. --- mypy.ini | 3 +- src/pomodouroboros/cli.py | 2 +- src/pomodouroboros/linux/gtk_progress_bar.py | 21 ++++++----- src/pomodouroboros/macos/hudmulti.py | 10 +++--- src/pomodouroboros/macos/intentions_gui.py | 37 ++++++++++---------- src/pomodouroboros/macos/mac_gui.py | 2 +- src/pomodouroboros/macos/mac_utils.py | 11 +++--- src/pomodouroboros/macos/multiple_choice.py | 2 +- src/pomodouroboros/macos/notifs.py | 10 +++--- src/pomodouroboros/macos/old_mac_gui.py | 14 ++++---- src/pomodouroboros/macos/progress_hud.py | 8 ++--- src/pomodouroboros/macos/sessions_gui.py | 2 +- src/pomodouroboros/model/intention.py | 2 +- src/pomodouroboros/pommodel.py | 5 +-- 14 files changed, 67 insertions(+), 62 deletions(-) diff --git a/mypy.ini b/mypy.ini index c2b451b..0948629 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5,17 +5,16 @@ plugins=mypy_zope:plugin # making our way to 'strict' warn_return_any = True - strict_optional = True warn_no_return = True warn_unused_configs = True warn_unused_ignores = True warn_redundant_casts = True no_implicit_optional = True +disallow_untyped_defs = True [not-yet-mypy] disallow_subclassing_any = True -disallow_untyped_defs = True disallow_any_generics = True disallow_any_unimported = True diff --git a/src/pomodouroboros/cli.py b/src/pomodouroboros/cli.py index 8ff5b88..ed37d47 100644 --- a/src/pomodouroboros/cli.py +++ b/src/pomodouroboros/cli.py @@ -1,4 +1,4 @@ -def main(): +def main() -> None: """ run stuff """ diff --git a/src/pomodouroboros/linux/gtk_progress_bar.py b/src/pomodouroboros/linux/gtk_progress_bar.py index f2dba02..a1425a1 100644 --- a/src/pomodouroboros/linux/gtk_progress_bar.py +++ b/src/pomodouroboros/linux/gtk_progress_bar.py @@ -1,4 +1,3 @@ - # installation instructions: # sudo apt install libgirepository1.0-dev gcc libcairo2-dev pkg-config python3-dev gir1.2-gtk-4.0 @@ -10,7 +9,7 @@ # six==1.16.0 # Load Gtk -import gi # type:ignore +import gi # type:ignore gi.require_version("GLib", "2.0") from gi.repository import GLib # type:ignore @@ -30,7 +29,8 @@ # button:active {background-image: image(brown);} # """) -css.load_from_data(""" +css.load_from_data( + """ progressbar text { color: yellow; font-weight: bold; @@ -46,16 +46,17 @@ background-image: none; background-color: #0f0; } -""") +""" +) from Xlib.display import Display as XOpenDisplay # type:ignore -from ewmh import EWMH # type:ignore +from ewmh import EWMH # type:ignore -from cairo import Region # type:ignore +from cairo import Region # type:ignore # When the application is launched… -def on_activate(app): +def on_activate(app: Gtk.Application) -> None: # … create a new window… win = Gtk.ApplicationWindow(application=app, title="Should Never Focus") win.set_opacity(0.25) @@ -73,14 +74,16 @@ def refraction() -> bool: prog.set_fraction(frac) return True - to = GLib.timeout_add((1000//10), refraction) + to = GLib.timeout_add((1000 // 10), refraction) prog.set_fraction(0.7) win.set_child(prog) gdisplay = prog.get_display() - Gtk.StyleContext.add_provider_for_display(gdisplay, css, Gtk.STYLE_PROVIDER_PRIORITY_USER) + Gtk.StyleContext.add_provider_for_display( + gdisplay, css, Gtk.STYLE_PROVIDER_PRIORITY_USER + ) # we can't actually avoid getting focus, but in case the compositors ever # fix themselves, let's give it our best try diff --git a/src/pomodouroboros/macos/hudmulti.py b/src/pomodouroboros/macos/hudmulti.py index d0e2b56..e16e800 100644 --- a/src/pomodouroboros/macos/hudmulti.py +++ b/src/pomodouroboros/macos/hudmulti.py @@ -1,5 +1,5 @@ from typing import Self -from Foundation import NSMakePoint +from Foundation import NSMakePoint, NSNotification from AppKit import ( NSNib, NSObject, @@ -33,7 +33,7 @@ def initWithScreen_(self, screen: NSScreen) -> Self: self.screen = screen return self - def someSpaceActivated_(self, theSpace) -> None: + def someSpaceActivated_(self, notification: NSNotification) -> None: print("IOAS", self.win.isOnActiveSpace()) self.win.setIsVisible_(False) self.win.setIsVisible_(True) @@ -50,9 +50,9 @@ def repositionWindow(self) -> None: sw = screenFrame.size.width / 2 sh = screenFrame.size.height * (5 / 6) winOrigin = NSMakePoint( - screenFrame.origin.x + (sw - (w / 2)), - screenFrame.origin.y + (sh - (h / 2)), - ) + screenFrame.origin.x + (sw - (w / 2)), + screenFrame.origin.y + (sh - (h / 2)), + ) print(f"screenOrigin: {screenFrame.origin} winOrigin: {winOrigin}") self.win.setFrameOrigin_(winOrigin) diff --git a/src/pomodouroboros/macos/intentions_gui.py b/src/pomodouroboros/macos/intentions_gui.py index ba5e4fc..a4e5584 100644 --- a/src/pomodouroboros/macos/intentions_gui.py +++ b/src/pomodouroboros/macos/intentions_gui.py @@ -29,8 +29,7 @@ class IntentionRow(NSObject): if TYPE_CHECKING: @classmethod - def alloc(cls) -> IntentionRow: - ... + def alloc(cls) -> IntentionRow: ... def initWithIntention_andNexus_( self, intention: Intention, nexus: Nexus @@ -107,7 +106,7 @@ def _getCreationText(self) -> str: return f"{creationDate.isoformat(timespec='minutes', sep=' ')}" @modificationText.getter - def _getModificationText(self): + def _getModificationText(self) -> str: modificationDate = datetime.fromtimestamp(self.intention.modified) return f"{modificationDate.isoformat(timespec='minutes', sep=' ')}" @@ -119,9 +118,9 @@ class IntentionDataSource(NSObject): # pragma mark Attributes - intentionRowMap: ModelConverter[ - Intention, IntentionRow - ] = objc.object_property() + intentionRowMap: ModelConverter[Intention, IntentionRow] = ( + objc.object_property() + ) nexus: Nexus | None = objc.object_property() selectedIntention: IntentionRow | None = objc.object_property() @@ -321,14 +320,16 @@ def tableView_objectValueForTableColumn_row_( "date": str(dt.date()), "startTime": str(dt.time().replace(microsecond=0)), "endTime": str(et.time().replace(microsecond=0)), - "evaluation": "" - if e is None - else { - EvaluationResult.distracted: "🦋", - EvaluationResult.interrupted: "🗣", - EvaluationResult.focused: "🤔", - EvaluationResult.achieved: "✅", - }[e.result], + "evaluation": ( + "" + if e is None + else { + EvaluationResult.distracted: "🦋", + EvaluationResult.interrupted: "🗣", + EvaluationResult.focused: "🤔", + EvaluationResult.achieved: "✅", + }[e.result] + ), # TODO: should be a clickable link to the session that this was in, # but first we need that feature from the model. "inSession": "???", @@ -338,9 +339,9 @@ def clearSelection(self) -> None: debug("CLEARING SELECTION/intpom data!") self.selectedPomodoro = None self.hasSelection = False - self.canEvaluateDistracted = ( - self.canEvaluateInterrupted - ) = self.canEvaluateFocused = self.canEvaluateAchieved = False + self.canEvaluateDistracted = self.canEvaluateInterrupted = ( + self.canEvaluateFocused + ) = self.canEvaluateAchieved = False # pragma mark NSTableViewDelegate @interactionRoot @@ -359,7 +360,7 @@ def tableViewSelectionDidChange_(self, notification: NSObject) -> None: # should also update this last one when reloading data? self.canEvaluateAchieved = idx == (len(self.backingData) - 1) - def doEvaluate_(self, er: EvaluationResult): + def doEvaluate_(self, er: EvaluationResult) -> None: assert ( self.selectedPomodoro is not None ), "must have a pomodorodo selected and the UI should be enforcing that" diff --git a/src/pomodouroboros/macos/mac_gui.py b/src/pomodouroboros/macos/mac_gui.py index 753c47e..905bcad 100644 --- a/src/pomodouroboros/macos/mac_gui.py +++ b/src/pomodouroboros/macos/mac_gui.py @@ -91,7 +91,7 @@ def describeCurrentState(self, description: str) -> None: ... def sessionStarted(self, session: Session) -> None: "TODO" - def sessionEnded(self): + def sessionEnded(self) -> None: "TODO" def intervalStart(self, interval: AnyIntervalOrIdle) -> None: diff --git a/src/pomodouroboros/macos/mac_utils.py b/src/pomodouroboros/macos/mac_utils.py index 54f57c0..09b35dd 100644 --- a/src/pomodouroboros/macos/mac_utils.py +++ b/src/pomodouroboros/macos/mac_utils.py @@ -45,11 +45,9 @@ class Descriptor(Protocol[ForGetting, ForSetting, SelfType]): def __get__( self, instance: SelfType, owner: type | None = None - ) -> ForGetting: - ... + ) -> ForGetting: ... - def __set__(self, instance: SelfType, value: ForSetting) -> None: - ... + def __set__(self, instance: SelfType, value: ForSetting) -> None: ... PyType = TypeVar("PyType") @@ -94,8 +92,7 @@ def forwarded( name: str, pyToC: Callable[[PyType], ObjCType], cToPy: Callable[[ObjCType], PyType], - ) -> Descriptor[ObjCType, ObjCType, SelfType]: - ... + ) -> Descriptor[ObjCType, ObjCType, SelfType]: ... def forwarded( self, @@ -255,7 +252,7 @@ def someApplicationHidden_(self, notification: Any) -> None: app = NSApplication.sharedApplication() app.unhide_(self) - def someSpaceActivated_(self, notification) -> None: + def someSpaceActivated_(self, notification: NSNotification) -> None: """ Sometimes, fullscreen application stop getting the HUD overlay. """ diff --git a/src/pomodouroboros/macos/multiple_choice.py b/src/pomodouroboros/macos/multiple_choice.py index 84890f8..8e9fa87 100644 --- a/src/pomodouroboros/macos/multiple_choice.py +++ b/src/pomodouroboros/macos/multiple_choice.py @@ -53,7 +53,7 @@ def choose_(self, sender: NSObject) -> None: def answerWith(deferred: Deferred[T], answer: T) -> Callable[[], None]: - def answerer(): + def answerer() -> None: debug("giving result", answer) deferred.callback(answer) diff --git a/src/pomodouroboros/macos/notifs.py b/src/pomodouroboros/macos/notifs.py index 6e76e1e..62a920c 100644 --- a/src/pomodouroboros/macos/notifs.py +++ b/src/pomodouroboros/macos/notifs.py @@ -61,7 +61,7 @@ def userNotificationCenter_didReceiveNotificationResponse_withCompletionHandler_ theDelegate = NotificationDelegate.alloc().init() -def askForIntent(callback: Callable[[str], None]): +def askForIntent(callback: Callable[[str], None]) -> None: # When we ask for an intention, we should remove the reminder of the intention. notificationCenter.removeDeliveredNotificationsWithIdentifiers_( [basicMessageIdentifier] @@ -97,7 +97,9 @@ def withdrawIntentPrompt() -> None: ) -def notify(title="", subtitle="", informativeText=""): +def notify( + title: str = "", subtitle: str = "", informativeText: str = "" +) -> None: withdrawIntentPrompt() content = UNMutableNotificationContent.alloc().init() content.setTitle_(title) @@ -125,7 +127,7 @@ def notificationRequestCompleted(error: Optional[NSError]) -> None: ) -def setupNotifications(): +def setupNotifications() -> None: notificationCenter.setDelegate_(theDelegate) identifier = "SET_INTENTION" title = "Set Intention" @@ -141,7 +143,7 @@ def setupNotifications(): options = 0 actions = [setIntentionAction] # I think these are mostly to do with Siri - intentIdentifiers = [] + intentIdentifiers: list[str] = [] setIntentionPromptCategory = UNNotificationCategory.categoryWithIdentifier_actions_intentIdentifiers_options_( setIntentionCategoryIdentifier, actions, intentIdentifiers, options ) diff --git a/src/pomodouroboros/macos/old_mac_gui.py b/src/pomodouroboros/macos/old_mac_gui.py index 86e924d..9b1c41b 100644 --- a/src/pomodouroboros/macos/old_mac_gui.py +++ b/src/pomodouroboros/macos/old_mac_gui.py @@ -406,7 +406,7 @@ def new( progressController = ProgressController() def listRefresher() -> None: - def refreshListOnLoop(): + def refreshListOnLoop() -> None: NSLog("refreshing list from listRefresher") self.update() @@ -450,7 +450,7 @@ def addBonusPom(self) -> None: self.observer.refreshList() def doSetIntention(self) -> None: - async def whatever(): + async def whatever() -> None: await setIntention(self.reactor, self.day, self.dayLoader) NSLog("refreshing after setting intention") self.observer.refreshList() @@ -632,7 +632,7 @@ def observeValueForKeyPath_ofObject_change_context_( keyPath: str, ofObject: Dict[str, Any], change: Dict[str, Any], - context, + context: object, ) -> None: if change.get("notificationIsPrior"): return @@ -813,9 +813,11 @@ def main(reactor: IReactorTime) -> None: ctrl = DayEditorController.alloc().initWithClock_andDayLoader_( reactor, dayLoader ) - loaded, topLevelObjects = NSNib.alloc().initWithNibNamed_bundle_( - "GoalListWindow.nib", None - ).instantiateWithOwner_topLevelObjects_(ctrl, None) + loaded, topLevelObjects = ( + NSNib.alloc() + .initWithNibNamed_bundle_("GoalListWindow.nib", None) + .instantiateWithOwner_topLevelObjects_(ctrl, None) + ) setupNotifications() withdrawIntentPrompt() dayManager = DayManager.new(reactor, ctrl, dayLoader) diff --git a/src/pomodouroboros/macos/progress_hud.py b/src/pomodouroboros/macos/progress_hud.py index 3518d01..29b5b2d 100644 --- a/src/pomodouroboros/macos/progress_hud.py +++ b/src/pomodouroboros/macos/progress_hud.py @@ -224,7 +224,7 @@ def midScreenSizer(screen: NSScreen) -> NSRect: def hudWindowOn( screen: NSScreen, sizer: Callable[[NSScreen], NSRect], - styleMask=(NSBorderlessWindowMask | NSHUDWindowMask), + styleMask: int = (NSBorderlessWindowMask | NSHUDWindowMask), ) -> HUDWindow: app = NSApp() backing = NSBackingStoreBuffered @@ -248,7 +248,7 @@ def hudWindowOn( ProgressViewFactory = Callable[[], AbstractProgressView] -def textOpacityCurve(startTime: float, duration: float, now: float): +def textOpacityCurve(startTime: float, duration: float, now: float) -> float: """ t - float from 0-1 """ @@ -349,7 +349,7 @@ def _textReminder(self, clock: IReactorTime) -> None: start = clock.seconds() endTime = start + totalTime - def updateText(): + def updateText() -> None: now = clock.seconds() if now > endTime: self.setTextAlpha(0.0) @@ -600,7 +600,7 @@ def makeText( def _circledTextWithAlpha( center: NSPoint, text: str, alpha: float, color: NSColor -): +) -> None: black = NSColor.blackColor() aString = makeText(text, color, alpha) outline = makeText(text, color, 1.0, black, alpha, 5.0) diff --git a/src/pomodouroboros/macos/sessions_gui.py b/src/pomodouroboros/macos/sessions_gui.py index 06e307f..c4b52fc 100644 --- a/src/pomodouroboros/macos/sessions_gui.py +++ b/src/pomodouroboros/macos/sessions_gui.py @@ -29,7 +29,7 @@ def numberOfRowsInTableView_(self, tableView: NSTableView) -> int: # This table is not editable. def tableView_shouldEditTableColumn_row_( - self, tableView, shouldEditTableColumn, row + self, tableView: NSTableView, shouldEditTableColumn: bool, row: int ) -> bool: return False diff --git a/src/pomodouroboros/model/intention.py b/src/pomodouroboros/model/intention.py index 9065645..28777b2 100644 --- a/src/pomodouroboros/model/intention.py +++ b/src/pomodouroboros/model/intention.py @@ -62,7 +62,7 @@ def _compref(self) -> dict[str, object]: ) ) - def __eq__(self, other: object): + def __eq__(self, other: object) -> bool: if not isinstance(other, Intention): return NotImplemented return self._compref() == other._compref() diff --git a/src/pomodouroboros/pommodel.py b/src/pomodouroboros/pommodel.py index 6fdfee1..b30edb7 100644 --- a/src/pomodouroboros/pommodel.py +++ b/src/pomodouroboros/pommodel.py @@ -12,6 +12,7 @@ - pass/fail status """ + from __future__ import annotations from dataclasses import dataclass @@ -84,7 +85,7 @@ def progressUpdate( of setting the intention. """ - def dayOver(self): + def dayOver(self) -> None: """ The day is over, so there will be no more intervals. """ @@ -546,7 +547,7 @@ def bonusPomodoro(self, currentTime: datetime) -> Pomodoro: Create a new pomodoro that doesn't overlap with existing ones. """ - def lengths(): + def lengths() -> tuple[slice, datetime, timedelta, timedelta]: allIntervals = self.elapsedIntervals + self.pendingIntervals position = slice(len(self.pendingIntervals), 0) if allIntervals: From 38493ba943cb2d2c1d46d0abf00956100c194b50 Mon Sep 17 00:00:00 2001 From: Glyph Date: Tue, 22 Oct 2024 14:11:37 -0700 Subject: [PATCH 10/52] =?UTF-8?q?how=E2=80=A6=20how=20did=20this=20ever=20?= =?UTF-8?q?work?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (make AbstractProgressView key-value coding compliant for the attributes in bindings) This commit was sponsored by Jacob Kaplan-Moss, and my other patrons. If you want to join them, you can support my work at https://glyph.im/patrons/. --- src/pomodouroboros/macos/progress_hud.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/pomodouroboros/macos/progress_hud.py b/src/pomodouroboros/macos/progress_hud.py index 29b5b2d..7e9196a 100644 --- a/src/pomodouroboros/macos/progress_hud.py +++ b/src/pomodouroboros/macos/progress_hud.py @@ -142,6 +142,9 @@ def setTextAlpha_(self, newAlpha: float) -> None: self._textAlpha = newAlpha self.setNeedsDisplay_(True) + def percentage(self) -> float: + return self._percentage + def setPercentage_(self, newPercentage: float) -> None: """ Set the percentage-full here. @@ -149,12 +152,18 @@ def setPercentage_(self, newPercentage: float) -> None: self._percentage = newPercentage self.setNeedsDisplay_(True) + def bonusPercentage1(self) -> float: + return self._bonusPercentage1 + def setBonusPercentage1_(self, newBonusPercentage: float) -> None: if newBonusPercentage is None: return self._bonusPercentage1 = newBonusPercentage self.setNeedsDisplay_(True) + def bonusPercentage2(self) -> float: + return self._bonusPercentage2 + def setBonusPercentage2_(self, newBonusPercentage: float) -> None: if newBonusPercentage is None: # TODO: why??? From cd3cca0c9deeb85d09c80890b4d205421536a10a Mon Sep 17 00:00:00 2001 From: Glyph Date: Tue, 22 Oct 2024 16:07:59 -0700 Subject: [PATCH 11/52] flail around trying to prevent ghost windows from sticking around refs #70 This commit was sponsored by Jacob Kaplan-Moss, and my other patrons. If you want to join them, you can support my work at https://glyph.im/patrons/. --- src/pomodouroboros/macos/progress_hud.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pomodouroboros/macos/progress_hud.py b/src/pomodouroboros/macos/progress_hud.py index 7e9196a..e85d9ef 100644 --- a/src/pomodouroboros/macos/progress_hud.py +++ b/src/pomodouroboros/macos/progress_hud.py @@ -740,5 +740,6 @@ def _removeWindows(self: ProgressController) -> None: self.progressViews = [] self.hudWindows, oldHudWindows = [], self.hudWindows for eachWindow in oldHudWindows: + eachWindow.setIsVisible_(False) eachWindow.close() eachWindow.setContentView_(None) From 03c0179753b9a58306152462fc8dcec5d9b70030 Mon Sep 17 00:00:00 2001 From: Glyph Date: Tue, 22 Oct 2024 22:51:27 -0700 Subject: [PATCH 12/52] document what's going on here This commit was sponsored by Jacob Kaplan-Moss, and my other patrons. If you want to join them, you can support my work at https://glyph.im/patrons/. --- src/pomodouroboros/model/nexus.py | 34 ++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/pomodouroboros/model/nexus.py b/src/pomodouroboros/model/nexus.py index 1fef3c7..f4d061c 100644 --- a/src/pomodouroboros/model/nexus.py +++ b/src/pomodouroboros/model/nexus.py @@ -285,8 +285,22 @@ def availableIntentions(self) -> Sequence[Intention]: ] def _activeSession(self, oldTime: float, newTime: float) -> Session | None: + """ + Determine what the current active session is. + + @param oldTime: the time that we have already considered. We need to + use some reference point to start searching for new automatic + sessions, so this sets a lower bound on the time we have to search + from. - oldTime = max(oldTime, newTime - (86400 * 7)) + @param newTime: the time it is now. + """ + # an absurdly high bound for a session length, 7 days; we could + # probably dial this down to 18 hours just based on, like, human + # physiology. + MAX_SESSION_LENGTH = (86400 * 7) + + oldTime = max(oldTime, newTime - MAX_SESSION_LENGTH) for rule in self._sessionRules: thisOldTime = oldTime @@ -337,11 +351,9 @@ def advanceToTime(self, newTime: float) -> None: earlyEvaluationSpecialCase = ( # if our current streak is not empty (i.e. we are continuing it) self._currentStreak - # and the end time of the current interval in the current streak is - # not set - and (currentEndTime := self._currentStreak[-1].endTime) is not None - # and the current end time happens to correspond *exactly* to the last update time - and currentEndTime == self._lastUpdateTime + # and the current end time happens to correspond *exactly* to the + # last update time + and self._currentStreak[-1].endTime == self._lastUpdateTime # then even if the new time has not moved and we are still on the # last update time exactly, we need to process a loop update # because the timer at the end of the interval has moved. @@ -500,15 +512,17 @@ def evaluatePomodoro( # special case. Evaluating it in other ways allows it to # continue. (Might want an 'are you sure' in the UI for this, # since other evaluations can be reversed.) - assert pomodoro is self._activeInterval + assert pomodoro is ( + active := self._activeInterval + ), f""" + the pomodoro {pomodoro} is not ended yet, but it is not the + active interval {active} + """ pomodoro.endTime = timestamp # We now need to advance back to the current time since we've # changed the landscape; there's a new interval that now starts # there, and we need to emit our final progress notification # and build that new interval. - - # XXX this doesn't work any more, since we drive the loop based - # on being out of date on the actual time. self.advanceToTime(self._lastUpdateTime) From 4907395809677c17295bc543a05a10d97b4ed03b Mon Sep 17 00:00:00 2001 From: Glyph Date: Tue, 22 Oct 2024 22:51:40 -0700 Subject: [PATCH 13/52] rename endTime to sessionEndTime to avoid confusion with attribute-set This commit was sponsored by Jacob Kaplan-Moss, and my other patrons. If you want to join them, you can support my work at https://glyph.im/patrons/. --- src/pomodouroboros/model/test/test_model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pomodouroboros/model/test/test_model.py b/src/pomodouroboros/model/test/test_model.py index caebedb..7974fa7 100644 --- a/src/pomodouroboros/model/test/test_model.py +++ b/src/pomodouroboros/model/test/test_model.py @@ -47,11 +47,11 @@ class SessionChange: session: Session startTime: float progress: list[float] = field(default_factory=list) - endTime: float | None = None + sessionEndTime: float | None = None def setEndTime(self, newEndTime: float) -> None: - assert self.endTime is None, f"session already ended at {self.endTime}" - self.endTime = newEndTime + assert self.sessionEndTime is None, f"session already ended at {self.sessionEndTime}" + self.sessionEndTime = newEndTime @dataclass From 7e7ab79f43773856de2f7078f7a7a375f02aaa6e Mon Sep 17 00:00:00 2001 From: Glyph Date: Tue, 22 Oct 2024 22:52:20 -0700 Subject: [PATCH 14/52] why is Xcode This commit was sponsored by Jacob Kaplan-Moss, and my other patrons. If you want to join them, you can support my work at https://glyph.im/patrons/. --- IBFiles/GoalListWindow.xib | 4 +-- IBFiles/IntentionEditor.xib | 68 ++++++++++++++++++------------------- IBFiles/MainMenu.xib | 4 +-- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/IBFiles/GoalListWindow.xib b/IBFiles/GoalListWindow.xib index af6ded9..8707860 100644 --- a/IBFiles/GoalListWindow.xib +++ b/IBFiles/GoalListWindow.xib @@ -30,8 +30,8 @@ - - + + diff --git a/IBFiles/IntentionEditor.xib b/IBFiles/IntentionEditor.xib index 0c0a0a9..51a2117 100644 --- a/IBFiles/IntentionEditor.xib +++ b/IBFiles/IntentionEditor.xib @@ -1,7 +1,7 @@ - + - + @@ -489,7 +489,7 @@