What this page does
Introduces all external modules required to build the graphical interface and drive real-time behavior.
Part: Foundation
Section: Module & Runtime Boundary
Depends on: None
This step is part of the larger process of establishing which tools and capabilities the application is allowed to use.
Code (this page)
import tkinter as tk
from tkinter import messagebox
import time
from typing import Optional
Explanation
tkinter provides the windowing system and widgets
messagebox enables modal dialogs for user feedback
time supplies both wall-clock time and a monotonic clock
Optional is used to express values that may be unset during runtime
No application logic runs yet. This page only establishes what tools are allowed to exist.
Why this matters
Without explicit imports, Python cannot access GUI capabilities, time measurement, or type hints. This page creates a clear boundary between "what the language provides" and "what this application uses." Every capability introduced later traces back to these four imports.
This page will later include a diagram showing the dependency relationship between modules.
⚠ If something breaks here
Common issues at this step:
ModuleNotFoundError: No module named 'tkinter' — Tkinter not installed with Python
- Typo in import statement (e.g.,
Tkinter instead of tkinter)
How to recover:
- Verify Python installation includes Tkinter (standard on most systems)
- Check spelling and capitalization of all imports
What this page does
Creates a dedicated object to hold all mutable runtime state of the countdown timer.
Part: Foundation
Section: Domain State
Depends on: Page 1 (imports)
This step is part of the larger process of separating runtime behavior from configuration and persistence.
Code (this page)
class TimerState:
"""Transient runtime state for the countdown timer."""
def __init__(self, duration_seconds: int):
self.remaining_seconds: int = duration_seconds
self.is_running: bool = False
self.alarm_triggered: bool = False
Explanation
remaining_seconds tracks how much time is left on the countdown
is_running indicates whether the timer is actively counting down
alarm_triggered prevents the alarm from firing more than once
This object is never saved to disk. It exists only while the application is running.
Why this matters
Without a dedicated state object, timer variables would be scattered across the application. This creates a single source of truth for runtime behavior. State transitions (start, pause, reset, alarm) can be reasoned about explicitly because they all modify this one object.
This page will later include a state diagram showing the three fields and their relationships.
⚠ If something breaks here
Common issues at this step:
- Missing colon after class definition
- Incorrect indentation inside
__init__
- Forgetting
self parameter
How to recover:
- Verify class syntax matches exactly
- Check that all three attributes are assigned inside
__init__
What this page does
Defines the main application controller that owns the window, state, and behavior.
Part: Foundation
Section: Application Shell
Depends on: Page 2 (TimerState exists)
This step is part of the larger process of establishing architectural authority for all application behavior.
Code (this page)
class StudyTimerApp:
"""Main application controller."""
Explanation
- All application behavior will live inside this class
- This object coordinates UI, timing, and state
- External code interacts only through this controller
No logic is executed yet. This page establishes architectural authority.
Why this matters
Without a central controller, behavior would be distributed across functions and global variables. This class becomes the single entry point for understanding the application. Every method, every widget, every state change traces back to this controller.
This page will later include a box diagram showing StudyTimerApp as the container for all subsystems.
⚠ If something breaks here
Common issues at this step:
- Missing colon after class name
- Incorrect capitalization (
studytimerapp vs StudyTimerApp)
How to recover:
- Verify class definition syntax
- Use PascalCase for class names
What this page does
Declares fixed timing values used throughout the application.
Part: Foundation
Section: Application Shell
Depends on: Page 3 (StudyTimerApp class exists)
This step is part of the larger process of centralizing magic numbers into named constants.
Code (this page)
class StudyTimerApp:
TICK_INTERVAL_MS = 200
CLOCK_UPDATE_MS = 1000
DEFAULT_DURATION = 25 * 60
Explanation
TICK_INTERVAL_MS controls how often the timer logic runs (200ms = 5 times per second)
CLOCK_UPDATE_MS controls how often the clock display updates (1000ms = once per second)
DEFAULT_DURATION defines the initial countdown length (25 × 60 = 1500 seconds = 25 minutes)
Constants are defined once and reused everywhere.
Why this matters
Without named constants, the values 200, 1000, and 1500 would appear scattered throughout the code. Changing timing behavior would require hunting through multiple files. This page ensures timing decisions are made in exactly one place.
This page will later include a table showing each constant, its value, and where it's used.
⚠ If something breaks here
Common issues at this step:
- Constants defined outside the class (missing indentation)
- Using lowercase names (violates convention)
- Missing multiplication in
DEFAULT_DURATION
How to recover:
- Ensure constants are indented inside the class body
- Use SCREAMING_SNAKE_CASE for constants
- Verify
25 * 60 evaluates to 1500
What this page does
Creates the main window and applies basic window-level configuration.
Part: Foundation
Section: Application Shell
Depends on: Page 4 (class with constants exists)
This step is part of the larger process of establishing the visual container before building the UI.
Code (this page)
def __init__(self, root: tk.Tk):
self.root = root
self.root.title("Academic Study Timer")
self.root.resizable(False, False)
Explanation
- Stores the Tk root window for later use
- Sets a descriptive window title
- Prevents resizing to preserve layout integrity
This step does not build the UI yet. It only defines the container the UI will live in.
Why this matters
Without storing the root window, later methods cannot schedule callbacks or modify the window. Without disabling resize, users could break the carefully designed layout. This page establishes the application's visual boundary.
This page will later include a screenshot of the empty window with title bar visible.
⚠ If something breaks here
Common issues at this step:
- Forgetting
self parameter in __init__
- Passing wrong type to constructor (not a
tk.Tk instance)
- Typo in method name (
__init_ vs __init__)
How to recover:
- Verify
__init__ has both self and root parameters
- Create root with
tk.Tk() before passing to constructor
What this page does
Creates the initial runtime timer state using the default duration.
Part: Foundation
Section: Application Initialization
Depends on: Page 5 (window initialized), Page 2 (TimerState class exists)
This step is part of the larger process of preparing all runtime data before the UI is built.
Code (this page)
# Timer state
self.timer_state = TimerState(self.DEFAULT_DURATION)
Explanation
- Instantiates
TimerState with the default countdown duration (1500 seconds)
- Sets
remaining_seconds to 25 minutes
- Initializes the timer in a non-running, non-alarmed state
This establishes the starting condition of the timer.
Why this matters
Without initialized state, the UI would have nothing to display. Without using `DEFAULT_DURATION`, the initial value would be a magic number. This page ensures the timer always starts from a known, consistent state.
This page will later include a diagram showing TimerState initialized with default values.
⚠ If something breaks here
Common issues at this step:
NameError: name 'TimerState' is not defined — class not defined or import missing
- Using wrong constant name
- Forgetting
self. prefix
How to recover:
- Ensure
TimerState class appears before StudyTimerApp
- Verify constant is
DEFAULT_DURATION (not DEFAULT_TIME or similar)
What this page does
Sets up internal variables used for accurate time tracking.
Part: Foundation
Section: Application Initialization
Depends on: Page 6 (timer state exists), Page 1 (Optional imported)
This step is part of the larger process of enabling drift-free countdown timing.
Code (this page)
# Track last tick time for accurate countdown
self._last_tick_time: Optional[float] = None
self._accumulated_time: float = 0.0
Explanation
_last_tick_time stores the previous tick timestamp (None when not running)
_accumulated_time stores fractional seconds between ticks
- Both values are internal (prefixed with
_) and never exposed externally
These variables prevent timing drift and jitter.
Why this matters
Without tracking elapsed time between ticks, the countdown would drift based on UI responsiveness. Without accumulating fractional seconds, the timer would lose precision. This page implements the **Accumulated Time Pattern** from the reference patterns.
This page will later include a timeline diagram showing tick intervals and accumulated time.
⚠ If something breaks here
Common issues at this step:
- Missing
Optional import from typing
- Using wrong type annotation syntax
- Forgetting underscore prefix (convention violation)
How to recover:
- Verify
from typing import Optional appears in imports
- Use
Optional[float] syntax (brackets, not parentheses)
What this page does
Delegates all UI creation to a dedicated method.
Part: Foundation
Section: Application Initialization
Depends on: Page 7 (all state variables initialized)
This step is part of the larger process of separating initialization from layout.
Code (this page)
# Build UI
self._build_ui()
Explanation
- Calls a separate method to construct widgets
- Keeps
__init__ readable and linear
- Ensures UI is built after state initialization
This marks the transition from logic setup to visual structure.
Why this matters
Without delegation, `__init__` would grow to hundreds of lines. Without calling after state initialization, widgets might reference undefined variables. This page enforces a clean separation between "prepare data" and "build visuals."
This page will later include a flowchart showing __init__ calling _build_ui.
⚠ If something breaks here
Common issues at this step:
AttributeError: 'StudyTimerApp' object has no attribute '_build_ui' — method not yet defined
- Missing parentheses (calling vs referencing)
How to recover:
- This error is expected until
_build_ui is implemented
- Ensure parentheses are present:
_build_ui() not _build_ui
What this page does
Starts the recurring clock update and timer tick loops.
Part: Foundation
Section: Application Initialization
Depends on: Page 8 (UI construction triggered)
This step is part of the larger process of enabling continuous, non-blocking updates.
Code (this page)
# Start background updates
self._update_clock()
self._tick()
Explanation
_update_clock() updates the live time display every second
_tick() drives the countdown logic at regular intervals
- Both methods reschedule themselves internally using
root.after()
No threads are created. All execution remains inside Tkinter's event loop.
Why this matters
Without background updates, the clock would freeze and the timer would never count down. Without using `root.after()`, updates would block the UI. This page implements the **Non-Blocking UI Loop Pattern** from the reference patterns.
This page will later include a diagram showing the event loop with scheduled callbacks.
⚠ If something breaks here
Common issues at this step:
- Methods not yet defined (expected at this stage)
- Missing parentheses on method calls
How to recover:
- Errors are expected until
_update_clock and _tick are implemented
- Ensure both calls include parentheses
What this page does
Creates the top-level layout frame that holds all widgets.
Part: Foundation
Section: UI Composition
Depends on: Page 8 (_build_ui is called)
This step is part of the larger process of establishing the widget hierarchy.
Code (this page)
def _build_ui(self):
main_frame = tk.Frame(self.root, padx=20, pady=20)
main_frame.pack(fill=tk.BOTH, expand=True)
Explanation
main_frame serves as the root container for all UI elements
- Padding (
padx, pady) creates visual spacing from window edges
fill and expand allow the frame to occupy all available space
All subsequent UI elements will attach to this frame.
Why this matters
Without a container frame, widgets would attach directly to the root window, making layout changes difficult. Without padding, content would touch the window edges. This page establishes the single containment hierarchy for all UI elements.
This page will later include a wireframe showing the main_frame with padding margins.
⚠ If something breaks here
Common issues at this step:
- Forgetting
self parameter in method definition
- Using wrong parent (not
self.root)
- Missing
pack() call (frame exists but isn't visible)
How to recover:
- Verify method signature is
def _build_ui(self):
- Ensure frame's parent is
self.root
- Always call
pack() after creating widgets
What this page does
Creates a horizontal container for the live clock display.
Part: Foundation
Section: UI Composition → Header
Depends on: Page 10 (main_frame exists)
This step is part of the larger process of building the header section of the UI.
Code (this page)
clock_frame = tk.Frame(main_frame)
clock_frame.pack(fill=tk.X, pady=(0, 15))
Explanation
clock_frame contains the clock label and value
fill=tk.X stretches horizontally to match parent width
pady=(0, 15) adds 15 pixels of space below (asymmetric padding)
This separates the clock from the rest of the UI.
Why this matters
Without a dedicated frame, clock elements would mix with timer elements. Without bottom padding, the clock would crowd the timer display. This page creates clear visual separation between the header and body.
This page will later include a wireframe showing clock_frame position within main_frame.
⚠ If something breaks here
Common issues at this step:
- Using wrong parent (
self.root instead of main_frame)
- Symmetric padding instead of asymmetric
(0, 15)
- Missing
pack() call
How to recover:
- Ensure parent is
main_frame (the local variable)
- Use tuple syntax for asymmetric padding:
pady=(top, bottom)
What this page does
Adds the "Current Time:" text label that never changes.
Part: Foundation
Section: UI Composition → Header
Depends on: Page 11 (clock_frame exists)
This step is part of the larger process of labeling dynamic UI elements.
Code (this page)
tk.Label(
clock_frame,
text="Current Time:",
font=("TkDefaultFont", 10)
).pack(side=tk.LEFT)
Explanation
- Creates a Label widget with static text
- Uses system default font at size 10
- Packs to the left side of clock_frame
- Not stored in a variable (never needs updating)
This provides context for the live time value.
Why this matters
Without a label, users would see a time value with no explanation. Without left-packing, the label would center instead of aligning with the value. This page follows the pattern of labeling all dynamic content.
This page will later include a screenshot showing "Current Time:" text in position.
⚠ If something breaks here
Common issues at this step:
- Wrong parent (not
clock_frame)
- Missing
pack() call
- Font tuple syntax error
How to recover:
- Ensure parent is
clock_frame
- Chain
.pack() directly after Label creation
- Use tuple for font:
("FontName", size)
What this page does
Creates the label that displays the actual current time, updated every second.
Part: Foundation
Section: UI Composition → Header
Depends on: Page 12 (static label exists)
This step is part of the larger process of displaying dynamic content.
Code (this page)
self.clock_label = tk.Label(
clock_frame,
text="",
font=("TkDefaultFont", 10, "bold")
)
self.clock_label.pack(side=tk.LEFT, padx=(5, 0))
Explanation
- Stored as instance variable
self.clock_label for later updates
- Bold font differentiates dynamic content from static label
- Left padding of 5 pixels separates it from "Current Time:"
- Initial text is empty (populated by
_update_clock)
This is where the live time appears.
Why this matters
Without storing as instance variable, `_update_clock` couldn't modify the text. Without bold styling, static and dynamic text would be visually identical. This page creates the target for the clock update loop.
This page will later include a screenshot showing the clock value next to its label.
⚠ If something breaks here
Common issues at this step:
- Forgetting
self. prefix (variable not accessible later)
- Wrong parent widget
- Bold not in font tuple
How to recover:
- Use
self.clock_label to store on instance
- Ensure parent is
clock_frame
- Font tuple with bold:
("FontName", size, "bold")
What this page does
Creates the bordered container that holds the countdown display and status.
Part: Foundation
Section: UI Composition → Countdown Display
Depends on: Page 11 (header section complete)
This step is part of the larger process of visually grouping the central timer area.
Code (this page)
self.timer_frame = tk.Frame(
main_frame,
bd=2,
relief=tk.GROOVE,
padx=20,
pady=20
)
self.timer_frame.pack(fill=tk.X, pady=(0, 15))
Explanation
- Stored as
self.timer_frame for alarm color changes later
bd=2 creates a 2-pixel border
relief=tk.GROOVE gives an inset appearance
- Internal padding separates content from borders
- Bottom margin separates from controls below
This is the visual focus of the application.
Why this matters
Without a bordered frame, the countdown would float in empty space. Without storing as instance variable, alarm state couldn't change the background color. This page creates the primary visual anchor of the application.
This page will later include a screenshot showing the grooved border around the timer area.
⚠ If something breaks here
Common issues at this step:
- Forgetting
self. prefix
- Using wrong relief style
- Missing internal padding
How to recover:
- Store as
self.timer_frame for alarm functionality
- Use
tk.GROOVE (not string "groove")
- Include both
padx and pady for internal spacing
What this page does
Creates the large label that shows the remaining time in MM:SS format.
Part: Foundation
Section: UI Composition → Countdown Display
Depends on: Page 14 (timer_frame exists)
This step is part of the larger process of displaying the countdown value prominently.
Code (this page)
self.countdown_label = tk.Label(
self.timer_frame,
text="25:00",
font=("TkDefaultFont", 48, "bold")
)
self.countdown_label.pack()
Explanation
- Stored as
self.countdown_label for countdown updates
- Large font size (48) makes time highly visible
- Bold weight emphasizes importance
- Initial text shows formatted default duration
- Centered within timer_frame by default pack behavior
This is the primary user feedback element.
Why this matters
Without a large, prominent display, users couldn't see the countdown at a glance. Without storing as instance variable, the tick loop couldn't update the display. This page creates the most important visual element in the application.
This page will later include a screenshot showing "25:00" in large text within the timer frame.
⚠ If something breaks here
Common issues at this step:
- Wrong parent (should be
self.timer_frame)
- Font size too small to read at a glance
- Forgetting
self. prefix
How to recover:
- Ensure parent is
self.timer_frame
- Use size 48 or larger for visibility
- Store as
self.countdown_label for tick updates
What this page does
Creates the label that shows the current timer state (Ready, Running, Paused, Complete).
Part: Foundation
Section: UI Composition → Countdown Display
Depends on: Page 15 (countdown label exists)
This step is part of the larger process of communicating timer state to the user.
Code (this page)
self.status_label = tk.Label(
self.timer_frame,
text="Ready",
font=("TkDefaultFont", 12)
)
self.status_label.pack(pady=(10, 0))
Explanation
- Stored as
self.status_label for state change updates
- Smaller font than countdown (12 vs 48) — secondary information
- Initial text "Ready" matches initial state
- Top padding separates from countdown display
This tells users what the timer is doing.
Why this matters
Without a status label, users would have to infer state from button availability. Without storing as instance variable, state transitions couldn't update the display. This page implements the **Visual State Encoding Pattern** from the reference patterns.
This page will later include a screenshot showing "Ready" below the countdown.
⚠ If something breaks here
Common issues at this step:
- Wrong parent widget
- No visual separation from countdown
- Forgetting
self. prefix
How to recover:
- Ensure parent is
self.timer_frame
- Add
pady=(10, 0) for top spacing
- Store as
self.status_label
What this page does
Creates a horizontal container for the control buttons.
Part: Foundation
Section: UI Composition → Controls
Depends on: Page 14 (timer display section complete)
This step is part of the larger process of organizing control elements.
Code (this page)
button_frame = tk.Frame(main_frame)
button_frame.pack(fill=tk.X)
Explanation
- Local variable (not stored) — buttons are stored individually
fill=tk.X stretches to parent width for button distribution
- No padding — buttons define their own spacing
- Contains all three control buttons
This groups controls separately from display elements.
Why this matters
Without a container frame, buttons would need individual positioning. Without horizontal fill, buttons couldn't be distributed evenly. This page creates the layout structure for all control interactions.
This page will later include a wireframe showing button_frame below the timer display.
⚠ If something breaks here
Common issues at this step:
- Wrong parent (should be
main_frame, not timer_frame)
- Missing
pack() call
How to recover:
- Ensure parent is
main_frame
- Always call
pack() to make frame visible
What this page does
Creates the button that starts or resumes the countdown.
Part: Foundation
Section: UI Composition → Controls
Depends on: Page 17 (button_frame exists)
This step is part of the larger process of implementing user control.
Code (this page)
self.start_button = tk.Button(
button_frame,
text="Start",
width=10,
command=self._on_start
)
self.start_button.pack(side=tk.LEFT, expand=True)
Explanation
- Stored as
self.start_button for state-based enabling/disabling
- Fixed width of 10 characters for consistent sizing
command binds to _on_start method (defined later)
expand=True with side=tk.LEFT distributes space evenly
This initiates the countdown.
Why this matters
Without storing as instance variable, the button couldn't be disabled during countdown. Without `expand=True`, buttons would cluster to one side. This page creates the primary action trigger for the application.
This page will later include a screenshot showing the Start button in position.
⚠ If something breaks here
Common issues at this step:
_on_start not yet defined (expected)
- Wrong parent widget
- Missing
expand=True (buttons cluster)
How to recover:
- Command reference errors resolve when method is implemented
- Ensure parent is
button_frame
- Include
expand=True for even distribution
What this page does
Creates the button that pauses the countdown.
Part: Foundation
Section: UI Composition → Controls
Depends on: Page 18 (Start button exists)
This step is part of the larger process of implementing user control.
Code (this page)
self.stop_button = tk.Button(
button_frame,
text="Stop",
width=10,
command=self._on_stop,
state=tk.DISABLED
)
self.stop_button.pack(side=tk.LEFT, expand=True)
Explanation
- Stored as
self.stop_button for state management
- Initially disabled (
state=tk.DISABLED) — can't stop what isn't running
- Same width as Start for visual consistency
command binds to _on_stop method (defined later)
This pauses the countdown without resetting.
Why this matters
Without initial disabled state, users could click Stop before Start. Without storing as instance variable, the running state couldn't enable it. This page implements the **Guard Clause Pattern** at the UI level.
This page will later include a screenshot showing the disabled Stop button.
⚠ If something breaks here
Common issues at this step:
- Forgetting
state=tk.DISABLED
- Wrong constant (
DISABLED vs tk.DISABLED)
How to recover:
- Always use
tk.DISABLED (not string "disabled")
- Verify button appears grayed out visually
What this page does
Creates the button that resets the timer to its initial state.
Part: Foundation
Section: UI Composition → Controls
Depends on: Page 19 (Stop button exists)
This step is part of the larger process of implementing user control.
Code (this page)
self.reset_button = tk.Button(
button_frame,
text="Reset",
width=10,
command=self._on_reset
)
self.reset_button.pack(side=tk.LEFT, expand=True)
Explanation
- Stored as
self.reset_button for potential state management
- Initially enabled — reset is always valid from Ready state
- Same width as other buttons for consistency
command binds to _on_reset method (defined later)
This returns the timer to its starting condition.
Why this matters
Without a reset button, users would need to restart the application. Without consistent sizing, the button row would look unbalanced. This page completes the control surface for the timer.
This page will later include a screenshot showing all three buttons in a row.
⚠ If something breaks here
Common issues at this step:
- Buttons not evenly distributed (missing
expand=True)
- Buttons too narrow or too wide (wrong
width value)
How to recover:
- Ensure all three buttons have
expand=True
- Use
width=10 for all buttons
What this page does
Converts raw seconds into human-readable MM:SS display format.
Part: Foundation
Section: Helper Utilities
Depends on: None (pure function)
This step is part of the larger process of displaying countdown values to the user.
Code (this page)
def _format_time(self, seconds: int) -> str:
minutes = seconds // 60
secs = seconds % 60
return f"{minutes:02d}:{secs:02d}"
Explanation
- Integer division (
//) extracts whole minutes
- Modulo (
%) extracts remaining seconds
- f-string with
:02d ensures zero-padded two-digit output
- Returns string like "25:00" or "04:37"
This is a pure function with no side effects.
Why this matters
Without formatting, users would see raw seconds (1500) instead of readable time (25:00). Without zero-padding, the display would shift width as digits change (5:9 vs 05:09). This helper ensures consistent, professional time display throughout the application.
This page will later include examples showing input/output pairs (e.g., 1500 → "25:00", 65 → "01:05").
⚠ If something breaks here
Common issues at this step:
- Using
/ instead of // (returns float, not int)
- Missing zero-padding in format string
- Forgetting
self parameter
How to recover:
- Use
// for integer division
- Use
:02d format specifier for two-digit padding
- Verify method signature includes
self
What this page does
Creates the method that updates the live clock display every second.
Part: Foundation
Section: Background Processes
Depends on: Page 13 (clock_label exists)
This step is part of the larger process of providing real-time feedback to the user.
Code (this page)
def _update_clock(self):
current_time = time.strftime("%H:%M:%S")
self.clock_label.config(text=current_time)
self.root.after(self.CLOCK_UPDATE_MS, self._update_clock)
Explanation
time.strftime("%H:%M:%S") gets current local time in 24-hour format
config(text=...) updates the label's displayed text
root.after() schedules the next update without blocking
- Method calls itself recursively through the event loop
This creates a self-sustaining update cycle.
Why this matters
Without `root.after()`, the update would block the UI or require threading. Without self-rescheduling, the clock would update only once. This implements the **Non-Blocking UI Loop Pattern** that keeps the interface responsive.
This page will later include a sequence diagram showing the after() → update → after() cycle.
⚠ If something breaks here
Common issues at this step:
- Missing
self. on clock_label reference
- Wrong time format string
- Forgetting to reschedule (clock updates once then stops)
How to recover:
- Ensure
self.clock_label matches the instance variable name
- Use
%H:%M:%S for 24-hour time with seconds
- Always include the
root.after() call at the end
What this page does
Creates the entry point for the recurring timer tick loop.
Part: Foundation
Section: Time Engine (Tick Loop)
Depends on: Page 9 (tick loop started)
This step is part of the larger process of driving the countdown mechanism.
Code (this page)
def _tick(self):
"""Process one tick of the countdown timer."""
Explanation
- Method runs continuously via
root.after()
- Contains all countdown logic
- Executes regardless of timer state (running check happens inside)
- Docstring clarifies single-tick responsibility
This is the heartbeat of the timer.
Why this matters
Without a dedicated tick method, countdown logic would be scattered or coupled to UI events. This centralizes all timing behavior in one predictable location. The tick loop is the core of the **Time Engine**.
This page will later include a flowchart showing _tick as the central coordinator.
⚠ If something breaks here
Common issues at this step:
- Typo in method name (
_tick vs _Tick)
- Missing
self parameter
How to recover:
- Use lowercase with underscore prefix
- Always include
self as first parameter
What this page does
Guards countdown logic to only execute when the timer is actively running.
Part: Foundation
Section: Time Engine (Tick Loop)
Depends on: Page 23 (tick method exists)
This step is part of the larger process of implementing state-aware countdown behavior.
Code (this page)
def _tick(self):
if self.timer_state.is_running:
current_time = time.monotonic()
Explanation
is_running check prevents countdown when paused or stopped
time.monotonic() provides a clock that never goes backward
- Monotonic time is immune to system clock adjustments
- Only captures timestamp when timer is actually running
This implements the Guard Clause Pattern.
Why this matters
Without the running check, the timer would count down even when paused. Without monotonic time, daylight saving changes or NTP adjustments could corrupt the countdown. This page ensures timing accuracy and state correctness.
This page will later include a diagram comparing wall-clock time vs monotonic time.
⚠ If something breaks here
Common issues at this step:
- Using
time.time() instead of time.monotonic()
- Checking wrong state variable
How to recover:
- Always use
time.monotonic() for elapsed time calculations
- Verify
timer_state.is_running is the correct attribute
What this page does
Computes the real time elapsed since the previous tick.
Part: Foundation
Section: Time Engine (Tick Loop)
Depends on: Page 24 (current_time captured)
This step is part of the larger process of implementing drift-free countdown timing.
Code (this page)
if self._last_tick_time is not None:
elapsed = current_time - self._last_tick_time
self._accumulated_time += elapsed
Explanation
- Checks that a previous tick exists (not first tick after start)
- Calculates actual elapsed time between ticks
- Accumulates fractional seconds for precision
- Handles variable tick intervals gracefully
This implements the Accumulated Time Pattern.
Why this matters
Without elapsed time calculation, the countdown would assume perfect 200ms ticks (which never happen). Without accumulation, fractional seconds would be lost. This page ensures the countdown matches real wall-clock time regardless of system load.
This page will later include a timeline showing actual tick intervals vs accumulated time.
⚠ If something breaks here
Common issues at this step:
- Not checking for
None on first tick
- Using
= instead of += for accumulation
How to recover:
- Always check
_last_tick_time is not None before subtraction
- Use
+= to add to accumulated time, not replace it
What this page does
Converts accumulated fractional time into countdown second decrements.
Part: Foundation
Section: Time Engine (Tick Loop)
Depends on: Page 25 (accumulated_time updated)
This step is part of the larger process of maintaining whole-second countdown display.
Code (this page)
while self._accumulated_time >= 1.0 and self.timer_state.remaining_seconds > 0:
self.timer_state.remaining_seconds -= 1
self._accumulated_time -= 1.0
Explanation
while loop handles multiple seconds if tick was delayed
- Only decrements when a full second has accumulated
- Stops at zero to prevent negative countdown
- Preserves fractional remainder for next tick
This ensures countdown changes in whole-second increments.
Why this matters
Without the while loop, a delayed tick could skip seconds. Without the zero check, the countdown could go negative. Without preserving the remainder, timing precision would degrade over time. This page handles edge cases that simple implementations miss.
This page will later include a diagram showing accumulated time converting to countdown decrements.
⚠ If something breaks here
Common issues at this step:
- Using
if instead of while (misses multiple seconds)
- Missing zero check (negative countdown)
- Using
= instead of -=
How to recover:
- Use
while to handle multiple accumulated seconds
- Always check
remaining_seconds > 0 in condition
- Use
-= for both remaining_seconds and accumulated_time
What this page does
Checks if the countdown has reached zero and the alarm hasn't fired yet.
Part: Foundation
Section: Time Engine (Tick Loop)
Depends on: Page 26 (countdown decremented)
This step is part of the larger process of triggering completion feedback.
Code (this page)
if self.timer_state.remaining_seconds == 0 and not self.timer_state.alarm_triggered:
self._trigger_alarm()
Explanation
- Double condition prevents repeated alarm triggers
remaining_seconds == 0 detects completion
not alarm_triggered ensures one-time execution
- Delegates actual alarm behavior to dedicated method
This implements the Sticky Alarm Pattern.
Why this matters
Without the `alarm_triggered` check, the alarm would fire every tick after reaching zero. Without delegation, alarm logic would clutter the tick loop. This page ensures clean separation and correct one-time behavior.
This page will later include a state diagram showing the alarm transition.
⚠ If something breaks here
Common issues at this step:
- Missing
not before alarm_triggered
- Using
<= instead of == for zero check
How to recover:
- Use
not self.timer_state.alarm_triggered for negation
- Use
== 0 for exact zero check (countdown shouldn't go negative)
What this page does
Records the current timestamp for the next tick's elapsed time calculation.
Part: Foundation
Section: Time Engine (Tick Loop)
Depends on: Page 27 (alarm check complete)
This step is part of the larger process of maintaining timing continuity.
Code (this page)
self._last_tick_time = current_time
Explanation
- Stores current monotonic time for next iteration
- Must happen after all time calculations for this tick
- Only executes when timer is running (inside the
if block)
- Enables accurate elapsed time on next tick
This completes the timing portion of each tick.
Why this matters
Without updating last_tick_time, elapsed time would always be calculated from the original start time, causing exponential drift. This single line is critical for the **Monotonic Clock Pattern** to function correctly.
This page will later include a diagram showing last_tick_time updating each cycle.
⚠ If something breaks here
Common issues at this step:
- Placing this line outside the
if is_running block
- Forgetting
self. prefix
How to recover:
- Ensure proper indentation inside the running check
- Use
self._last_tick_time to reference instance variable
What this page does
Triggers a visual refresh of the timer UI after processing the tick.
Part: Foundation
Section: Time Engine (Tick Loop)
Depends on: Page 28 (timing complete)
This step is part of the larger process of keeping UI synchronized with state.
Code (this page)
self._update_timer_display()
Explanation
- Called every tick regardless of running state
- Updates countdown display, status text, and colors
- Placed outside the
is_running block (note indentation)
- Ensures UI always reflects current state
This decouples display logic from timing logic.
Why this matters
Without calling display update, the UI would freeze even as internal state changes. Without updating every tick, paused or stopped states wouldn't render correctly. This page ensures visual feedback is always current.
This page will later include a diagram showing tick → state change → display update flow.
⚠ If something breaks here
Common issues at this step:
- Wrong indentation (inside
if block instead of after)
- Method name typo
How to recover:
- Verify indentation matches
_tick method level, not inside if
- Ensure method name matches exactly:
_update_timer_display
What this page does
Schedules the next execution of the tick loop.
Part: Foundation
Section: Time Engine (Tick Loop)
Depends on: Page 29 (display updated)
This step is part of the larger process of creating a continuous timing loop.
Code (this page)
self.root.after(self.TICK_INTERVAL_MS, self._tick)
Explanation
root.after() schedules callback without blocking
TICK_INTERVAL_MS (200ms) provides smooth updates
- Method schedules itself, creating a perpetual loop
- Runs at method level (outside all conditionals)
This completes the tick loop structure.
Why this matters
Without rescheduling, the tick loop would run once and stop. Without using `after()`, the UI would block or require threading. This implements the **Non-Blocking UI Loop Pattern** that keeps the application responsive.
This page will later include a timeline showing tick scheduling intervals.
⚠ If something breaks here
Common issues at this step:
- Placing inside a conditional (tick stops under certain conditions)
- Using wrong constant name
- Forgetting
self. on method reference
How to recover:
- Ensure
after() call is at method level, not inside if
- Use
self.TICK_INTERVAL_MS (class constant)
- Reference
self._tick (not _tick)
What this page does
Creates the method that executes when the countdown reaches zero.
Part: Foundation
Section: Control Semantics & Alarm
Depends on: Page 27 (alarm detection calls this method)
This step is part of the larger process of handling timer completion.
Code (this page)
def _trigger_alarm(self):
"""Handle alarm when countdown reaches zero."""
Explanation
- Centralizes all alarm behavior in one location
- Called only from the tick loop's alarm detection
- Docstring clarifies purpose
- Method will contain state transitions and feedback
This establishes the alarm handler's authority.
Why this matters
Without a dedicated method, alarm logic would be scattered or inlined in the tick loop. This separation makes alarm behavior testable, modifiable, and understandable. Single responsibility is preserved.
This page will later include a diagram showing _trigger_alarm as the alarm coordinator.
⚠ If something breaks here
Common issues at this step:
- Typo in method name
- Missing
self parameter
How to recover:
- Match name exactly:
_trigger_alarm
- Always include
self as first parameter
What this page does
Transitions the timer into a completed (alarmed) state.
Part: Foundation
Section: Control Semantics & Alarm
Depends on: Page 31 (alarm handler exists)
This step is part of the larger process of managing timer lifecycle states.
Code (this page)
self.timer_state.alarm_triggered = True
self.timer_state.is_running = False
Explanation
alarm_triggered = True makes alarm state "sticky"
is_running = False stops countdown progression
- Order matters: mark triggered before stopping
- Prevents accidental restart without explicit reset
This implements the Sticky Alarm Pattern.
Why this matters
Without `alarm_triggered = True`, the alarm could fire repeatedly. Without `is_running = False`, the tick loop would continue processing. These two lines establish the terminal state of a completed timer.
This page will later include a state machine diagram showing the alarm transition.
⚠ If something breaks here
Common issues at this step:
- Setting only one of the two flags
- Wrong order (stopping before marking triggered)
How to recover:
- Always set both
alarm_triggered and is_running
- Set
alarm_triggered = True first
What this page does
Resets timing bookkeeping after the alarm fires.
Part: Foundation
Section: Control Semantics & Alarm
Depends on: Page 32 (state transitioned)
This step is part of the larger process of ensuring clean restart capability.
Code (this page)
self._last_tick_time = None
self._accumulated_time = 0.0
Explanation
_last_tick_time = None signals no active timing
_accumulated_time = 0.0 clears fractional seconds
- Ensures next Start (after Reset) begins with clean state
- Prevents leftover time from corrupting next countdown
This is critical for correctness after completion.
Why this matters
Without clearing timing variables, a subsequent start could inherit stale timing data. This would cause the first second of the next countdown to be incorrect. Clean state is essential for accurate timing.
This page will later include a diagram showing state cleanup after alarm.
⚠ If something breaks here
Common issues at this step:
- Forgetting one of the two variables
- Using wrong initial values
How to recover:
- Clear both
_last_tick_time and _accumulated_time
- Use
None and 0.0 respectively
What this page does
Plays an audible notification when the timer completes.
Part: Foundation
Section: Control Semantics & Alarm
Depends on: Page 33 (state cleaned up)
This step is part of the larger process of providing completion feedback.
Code (this page)
Explanation
- Uses Tkinter's built-in system bell
- No external dependencies required
- Cross-platform compatible
- Synchronous but instantaneous (no blocking)
Foundation always plays the sound; Configuration adds the option to disable it.
Why this matters
Without audible feedback, users might miss timer completion if not watching the screen. The system bell is the simplest, most reliable cross-platform sound. More sophisticated audio is intentionally deferred.
This page will later include a note about platform-specific bell behavior.
⚠ If something breaks here
Common issues at this step:
- System audio muted (not a code issue)
- Missing
self.root reference
How to recover:
- Check system volume settings
- Ensure
self.root is the Tk root window
What this page does
Creates the method that handles the Start button click.
Part: Foundation
Section: Control Semantics & Alarm
Depends on: Page 18 (Start button references this method)
This step is part of the larger process of implementing user control flow.
Code (this page)
def _on_start(self):
"""Handle Start button click."""
Explanation
- Connected to Start button via
command=self._on_start
- Will contain guard clauses and state transitions
- Docstring clarifies purpose
- Name convention:
_on_ prefix for event handlers
This establishes the start handler's structure.
Why this matters
Without a dedicated handler, button logic would be anonymous lambdas or scattered code. Named methods are testable, debuggable, and self-documenting. The `_on_` prefix signals event-driven behavior.
This page will later include a flowchart of the start handler logic.
⚠ If something breaks here
Common issues at this step:
- Method name doesn't match button's
command reference
- Missing
self parameter
How to recover:
- Ensure name matches:
_on_start
- Include
self as first parameter
What this page does
Prevents invalid start actions with guard clauses.
Part: Foundation
Section: Control Semantics & Alarm
Depends on: Page 35 (start handler exists)
This step is part of the larger process of enforcing valid state transitions.
Code (this page)
def _on_start(self):
if self.timer_state.is_running:
return
if self.timer_state.alarm_triggered:
return
if self.timer_state.remaining_seconds == 0:
return
Explanation
is_running check prevents double-start
alarm_triggered check enforces reset after completion
remaining_seconds == 0 check prevents starting exhausted timer
- Early returns keep code flat and readable
This implements the Guard Clause Pattern.
Why this matters
Without guards, users could start an already-running timer (causing timing bugs), start after alarm (undefined behavior), or start with zero time (immediate alarm). Guards enforce the **Ready / Running / Paused State Model**.
This page will later include a decision tree showing each guard condition.
⚠ If something breaks here
Common issues at this step:
- Missing one of the three guards
- Using
elif instead of separate if statements
How to recover:
- Include all three guard conditions
- Use separate
if/return for each (clearer than elif)
What this page does
Begins countdown timing after passing all guards.
Part: Foundation
Section: Control Semantics & Alarm
Depends on: Page 36 (guards passed)
This step is part of the larger process of transitioning to running state.
Code (this page)
self.timer_state.is_running = True
self._last_tick_time = time.monotonic()
self._accumulated_time = 0.0
Explanation
is_running = True activates the tick loop's countdown logic
_last_tick_time establishes the timing reference point
_accumulated_time = 0.0 ensures clean start
- Monotonic time prevents system clock interference
This is the actual "start" action.
Why this matters
Without setting `is_running`, the tick loop would ignore countdown logic. Without capturing monotonic time, elapsed calculations would fail. Without clearing accumulated time, leftover fractions could cause immediate decrement.
This page will later include a state diagram showing Ready → Running transition.
⚠ If something breaks here
Common issues at this step:
- Using
time.time() instead of time.monotonic()
- Forgetting to clear
_accumulated_time
How to recover:
- Always use
time.monotonic() for elapsed time
- Set
_accumulated_time = 0.0 on every start
What this page does
Creates the method that handles the Stop button click, pausing the timer.
Part: Foundation
Section: Control Semantics & Alarm
Depends on: Page 19 (Stop button references this method)
This step is part of the larger process of implementing pause functionality.
Code (this page)
def _on_stop(self):
if not self.timer_state.is_running:
return
self.timer_state.is_running = False
self._last_tick_time = None
self._accumulated_time = 0.0
Explanation
- Guard clause: stopping a stopped timer does nothing (idempotent)
is_running = False pauses countdown logic
_last_tick_time = None signals no active timing
_accumulated_time = 0.0 clears partial seconds
remaining_seconds is preserved (pause, not reset)
This is a clean pause that preserves progress.
Why this matters
Without the guard, stop could interfere with non-running states. Without clearing timing variables, resume would have stale data. Without preserving `remaining_seconds`, pause would become reset. This implements true pause semantics.
This page will later include a state diagram showing Running → Paused transition.
⚠ If something breaks here
Common issues at this step:
- Missing guard clause (stop affects non-running state)
- Accidentally resetting
remaining_seconds
How to recover:
- Always check
is_running before stopping
- Only modify timing variables, never
remaining_seconds
What this page does
Creates the method that returns the timer to its initial ready state.
Part: Foundation
Section: Control Semantics & Alarm
Depends on: Page 20 (Reset button references this method)
This step is part of the larger process of implementing full state reset.
Code (this page)
def _on_reset(self):
self.timer_state.is_running = False
self.timer_state.alarm_triggered = False
self.timer_state.remaining_seconds = self.DEFAULT_DURATION
self._last_tick_time = None
self._accumulated_time = 0.0
Explanation
- Stops any running countdown
- Clears the sticky alarm flag
- Restores default duration (25 minutes)
- Clears all timing bookkeeping
- Safe to call from any state
This is a complete state reset.
Why this matters
Without reset, users would need to restart the application after completion. Reset must clear the alarm flag (otherwise Start would be blocked). Using `DEFAULT_DURATION` ensures consistency with initial state.
This page will later include a diagram showing any state → Ready transition.
⚠ If something breaks here
Common issues at this step:
- Forgetting to clear
alarm_triggered
- Using wrong duration constant
- Missing timing variable cleanup
How to recover:
- Always clear all five values shown
- Use
self.DEFAULT_DURATION (not a literal number)
What this page does
Creates the method that synchronizes UI elements with timer state.
Part: Foundation
Section: Control Semantics & Alarm
Depends on: Page 29 (called from tick loop)
This step is part of the larger process of maintaining visual feedback.
Code (this page)
def _update_timer_display(self):
"""Update all timer-related UI elements."""
Explanation
- Called every tick regardless of timer state
- Will update countdown display, status text, button states, and colors
- Docstring clarifies comprehensive responsibility
- Separates display logic from timing logic
This is the visual synchronization hub.
Why this matters
Without a dedicated update method, display logic would be duplicated in start/stop/reset handlers. Centralizing updates ensures UI is always consistent with state. The tick loop drives updates; this method renders them.
This page will later include a diagram showing state → display mapping.
⚠ If something breaks here
Common issues at this step:
- Method name doesn't match tick loop's call
- Missing
self parameter
How to recover:
- Ensure name matches:
_update_timer_display
- Include
self as first parameter
What this page does
Renders the current remaining time in the countdown label.
Part: Foundation
Section: Control Semantics & Alarm
Depends on: Page 40 (display update method exists), Page 21 (format helper exists)
This step is part of the larger process of providing visual countdown feedback.
Code (this page)
# Update countdown display
time_text = self._format_time(self.timer_state.remaining_seconds)
self.countdown_label.config(text=time_text)
Explanation
- Calls
_format_time to convert seconds to "MM:SS"
- Updates the large countdown label
- Runs every tick for smooth updates
- No conditional logic—always shows current state
This is the primary visual feedback element.
Why this matters
Without updating the label, users wouldn't see time passing. Without using the formatter, raw seconds would display. This is the most important piece of user feedback in the entire application.
This page will later include a screenshot showing the countdown updating.
⚠ If something breaks here
Common issues at this step:
- Wrong label variable name
- Missing
_format_time call
How to recover:
- Use
self.countdown_label (match Page 15)
- Always format before displaying
What this page does
Computes the appropriate status message based on timer state.
Part: Foundation
Section: Control Semantics & Alarm
Depends on: Page 41 (countdown updated)
This step is part of the larger process of communicating state to users.
Code (this page)
# Determine status text
if self.timer_state.alarm_triggered:
status = "Time's Up!"
elif self.timer_state.is_running:
status = "Running"
elif self.timer_state.remaining_seconds < self.DEFAULT_DURATION:
status = "Paused"
else:
status = "Ready"
Explanation
- Priority order matters: alarm > running > paused > ready
- "Time's Up!" when alarm has fired
- "Running" when actively counting down
- "Paused" when stopped with progress
- "Ready" when at default duration
This implements the Visual State Encoding Pattern.
Why this matters
Without status text, users must infer state from countdown behavior. The priority order prevents incorrect messages (e.g., showing "Running" when alarm has fired). Explicit state communication reduces confusion.
This page will later include a state diagram showing status text mappings.
⚠ If something breaks here
Common issues at this step:
- Wrong condition order (running before alarm)
- Missing one of the four states
How to recover:
- Check
alarm_triggered first, always
- Include all four status values
What this page does
Applies the computed status text to the status label.
Part: Foundation
Section: Control Semantics & Alarm
Depends on: Page 42 (status computed)
This step is part of the larger process of rendering status feedback.
Code (this page)
self.status_label.config(text=status)
Explanation
- Updates the status label below the countdown
- Uses the
status variable computed in previous step
- Single line for clarity
- Runs every tick to stay synchronized
This completes the status text update.
Why this matters
Without applying the text to the label, the computed status would be discarded. This line is the connection between state logic and visual output.
This page will later include a screenshot showing status text below countdown.
⚠ If something breaks here
Common issues at this step:
- Wrong label variable name
status variable not in scope
How to recover:
- Use
self.status_label (match Page 16)
- Ensure status computation precedes this line
What this page does
Computes the appropriate background color based on timer state.
Part: Foundation
Section: Control Semantics & Alarm
Depends on: Page 43 (status updated)
This step is part of the larger process of providing visual state feedback.
Code (this page)
# Determine display color
if self.timer_state.alarm_triggered:
bg_color = "#ffcccc" # Light red for alarm
else:
bg_color = self.root.cget("bg") # Default background
Explanation
- Light red (
#ffcccc) signals alarm state visually
- Default background for all other states
cget("bg") retrieves system default color
- Binary logic: alarm or not alarm
This implements visual urgency for completion.
Why this matters
Without color change, users might miss the alarm state, especially if sound is muted. The red background provides unmistakable visual feedback that demands attention. Color is secondary to sound but critical for accessibility.
This page will later include before/after screenshots showing color change.
⚠ If something breaks here
Common issues at this step:
- Invalid hex color code
- Missing
# prefix in color string
How to recover:
- Use valid 6-digit hex:
#ffcccc
- Always prefix hex colors with
#
What this page does
Applies the computed background color to the timer frame and labels.
Part: Foundation
Section: Control Semantics & Alarm
Depends on: Page 44 (color computed)
This step is part of the larger process of rendering visual feedback.
Code (this page)
self.timer_frame.config(bg=bg_color)
self.countdown_label.config(bg=bg_color)
self.status_label.config(bg=bg_color)
Explanation
- Updates frame background (the bordered container)
- Updates countdown label background (large time display)
- Updates status label background (state text)
- All three must match for visual consistency
This completes the color update.
Why this matters
Without updating all three elements, the color would be inconsistent (e.g., red frame with white labels). Visual coherence reinforces state communication. Labels inherit frame color only if explicitly set.
This page will later include a screenshot showing uniform color across timer area.
⚠ If something breaks here
Common issues at this step:
- Forgetting one of the three elements
- Wrong attribute name (
bg vs background)
How to recover:
- Update all three:
timer_frame, countdown_label, status_label
- Use
bg (short form) consistently
What this page does
Enables or disables buttons based on current timer state.
Part: Foundation
Section: Control Semantics & Alarm
Depends on: Page 45 (colors updated)
This step is part of the larger process of preventing invalid user actions.
Code (this page)
# Update button states
if self.timer_state.is_running:
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
else:
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
Explanation
- When running: disable Start, enable Stop
- When not running: enable Start, disable Stop
- Reset button always enabled (not shown, handled separately if needed)
- Visual feedback matches logical constraints
This implements UI-level guards.
Why this matters
Without button state management, users could click Start while running or Stop while stopped. While guards in handlers prevent actual errors, disabled buttons communicate what actions are valid. This reduces user confusion.
This page will later include screenshots showing button state changes.
⚠ If something breaks here
Common issues at this step:
- Using string "disabled" instead of
tk.DISABLED
- Wrong button variable names
How to recover:
- Use
tk.DISABLED and tk.NORMAL constants
- Match button names from Pages 18-19
What this page does
Creates the main execution block that launches the application.
Part: Foundation
Section: Module & Runtime Boundary
Depends on: All previous pages (complete application)
This step is part of the larger process of making the application runnable.
Code (this page)
if __name__ == "__main__":
Explanation
- Standard Python entry point guard
- Code inside only runs when file is executed directly
- Importing the module does not trigger execution
- Enables testing and reuse of classes
This is the execution boundary.
Why this matters
Without the guard, importing the module would launch the application. This pattern allows the file to be both a runnable program and an importable module. It's a Python best practice.
This page will later include a diagram showing direct execution vs import behavior.
⚠ If something breaks here
Common issues at this step:
- Typo in
__name__ or __main__
- Wrong comparison operator
How to recover:
- Use double underscores:
__name__ and "__main__"
- Use
== for comparison
What this page does
Instantiates the Tkinter root window that hosts the application.
Part: Foundation
Section: Module & Runtime Boundary
Depends on: Page 47 (entry point guard)
This step is part of the larger process of launching the GUI application.
Code (this page)
Explanation
- Creates the main Tkinter window instance
- This is the top-level container for all widgets
- Must be created before any other Tkinter objects
- Stored in local variable for passing to app
This is the foundation of the GUI.
Why this matters
Without a root window, no Tkinter widgets can exist. This single line creates the entire windowing context. All subsequent UI elements descend from this root.
This page will later include a diagram showing root as the widget hierarchy top.
⚠ If something breaks here
Common issues at this step:
- Using
Tk() without tk. prefix
- Creating multiple root windows
How to recover:
- Use
tk.Tk() (module prefix required)
- Create exactly one root window per application
What this page does
Creates the application controller instance with the root window.
Part: Foundation
Section: Module & Runtime Boundary
Depends on: Page 48 (root exists), Page 5 (StudyTimerApp defined)
This step is part of the larger process of connecting the controller to the window.
Code (this page)
app = StudyTimerApp(root)
Explanation
- Instantiates the application controller
- Passes root window as required argument
- Triggers
__init__, which builds UI and starts loops
app variable maintains reference (prevents garbage collection)
This brings everything together.
Why this matters
Without instantiation, the class definition is inert code. This line activates all the initialization logic: state creation, UI building, background loop starting. The application comes to life here.
This page will later include a sequence diagram of initialization.
⚠ If something breaks here
Common issues at this step:
- Class name typo
- Forgetting to pass
root argument
How to recover:
- Use exact class name:
StudyTimerApp
- Always pass
root as the argument
What this page does
Begins the Tkinter main event loop, giving control to the GUI framework.
Part: Foundation
Section: Module & Runtime Boundary
Depends on: Page 49 (application instantiated)
This step is part of the larger process of running the interactive application.
Code (this page)
Explanation
- Starts Tkinter's event processing loop
- Handles user input, redraws, and scheduled callbacks
- Blocks until window is closed
- After this line, the application is interactive
This is the final line of the application.
Why this matters
Without `mainloop()`, the window would appear and immediately close. The event loop keeps the application alive and responsive. All user interaction flows through this loop.
This page will later include a diagram showing the event loop processing cycle.
⚠ If something breaks here
Common issues at this step:
- Forgetting to call
mainloop()
- Calling on wrong object (not
root)
How to recover:
- Always end with
root.mainloop()
- Ensure
root is the Tk instance from Page 48
What this page does
Creates the method that redraws the countdown and status display.
Part: Foundation
Section: UI Rendering
Depends on: Page 29 (called from tick loop)
This step is part of the larger process of keeping UI synchronized with timer state.
Code (this page)
def _update_timer_display(self):
"""Refresh countdown display and status."""
Explanation
- Centralizes all UI redraw rules in one place
- Called every tick to keep UI consistent
- Separates rendering from timer engine logic
- Method signature establishes the rendering coordinator
This is the visual synchronization hub.
Why this matters
Without a dedicated rendering method, display logic would be duplicated across handlers. Centralizing updates ensures the UI is always consistent with state regardless of how that state changed.
This page will later include a diagram showing _update_timer_display as the rendering coordinator.
⚠ If something breaks here
Common issues at this step:
- Method name typo
- Missing
self parameter
How to recover:
- Match name exactly:
_update_timer_display
- Include
self as first parameter
What this page does
Synchronizes the large countdown label with remaining seconds.
Part: Foundation
Section: UI Rendering → Countdown Display
Depends on: Page 51 (display method exists), Page 21 (format helper exists)
This step is part of the larger process of providing visual countdown feedback.
Code (this page)
self.countdown_label.config(
text=self._format_time(self.timer_state.remaining_seconds)
)
Explanation
- Converts seconds to MM:SS using
_format_time()
- Updates the displayed value every tick
- Keeps UI consistent whether running or paused
- Uses
timer_state.remaining_seconds as the source of truth
This is the primary visual feedback element.
Why this matters
Without updating the countdown label, users wouldn't see time passing. The format helper ensures consistent MM:SS display regardless of the raw seconds value.
This page will later include a screenshot showing the countdown updating.
⚠ If something breaks here
Common issues at this step:
- Wrong label variable name (
countdown_label vs countdown_label)
- Missing
_format_time call
How to recover:
- Use the label name defined in your UI construction
- Always format before displaying
What this page does
Applies "Time's Up" styling when the alarm state is active.
Part: Foundation
Section: UI Rendering → Alarm State
Depends on: Page 52 (countdown updated)
This step is part of the larger process of providing completion feedback.
Code (this page)
if self.timer_state.alarm_triggered:
self.timer_frame.config(bg="#ffcccc")
self.countdown_label.config(bg="#ffcccc")
self.status_label.config(bg="#ffcccc", text="Time's Up!", fg="#cc0000")
Explanation
- Alarm visuals override normal rendering
- Light red background (
#ffcccc) provides immediate completion feedback
- Dark red text (
#cc0000) emphasizes urgency
- Status text switches to "Time's Up!"
- Uses
alarm_triggered gate to prevent repeated updates
This implements the Visual State Encoding Pattern for completion.
Why this matters
Without visual alarm feedback, users might miss completion if sound is muted. The red background and text color provide unmistakable visual indication that demands attention.
This page will later include a screenshot showing the alarm state styling.
⚠ If something breaks here
Common issues at this step:
- Invalid hex color codes
- Missing
# prefix on colors
- Wrong attribute name (
fg vs foreground)
How to recover:
- Use valid 6-digit hex:
#ffcccc, #cc0000
- Use
fg for foreground color in Tkinter
What this page does
Restores default visuals when not in alarm state.
Part: Foundation
Section: UI Rendering → Normal State
Depends on: Page 53 (alarm branch defined)
This step is part of the larger process of maintaining visual consistency.
Code (this page)
else:
self.timer_frame.config(bg=self.root.cget("bg"))
self.countdown_label.config(bg=self.root.cget("bg"))
self.status_label.config(bg=self.root.cget("bg"), fg="black")
Explanation
- Clears alarm styling after reset
root.cget("bg") retrieves the system default background color
- Restores black text for readability
- Ensures visual state matches logical state
This completes the alarm/normal visual toggle.
Why this matters
Without restoring normal colors, the red alarm styling would persist forever. Using `cget("bg")` ensures the app respects the system theme rather than hardcoding a color.
This page will later include before/after screenshots showing color restoration.
⚠ If something breaks here
Common issues at this step:
- Forgetting to restore
fg to black
- Hardcoding background color instead of using
cget
How to recover:
- Always set
fg="black" in the else branch
- Use
self.root.cget("bg") for system-native appearance
What this page does
Shows "Running" while the timer is actively counting down.
Part: Foundation
Section: UI Rendering → Status Logic
Depends on: Page 54 (visual styling complete)
This step is part of the larger process of communicating timer state to users.
Code (this page)
if self.timer_state.is_running:
self.status_label.config(text="Running")
Explanation
- Highest-priority normal-state label
- Reflects current operational mode
- Only evaluated when not in alarm state (due to prior
if structure)
- Clear, unambiguous status message
This begins the status text selection ladder.
Why this matters
Without explicit status text, users must infer state from button availability or countdown movement. "Running" provides immediate confirmation that the timer is active.
This page will later include a screenshot showing "Running" status.
⚠ If something breaks here
Common issues at this step:
- Wrong condition (checking wrong state variable)
- Status text not updating (display method not called)
How to recover:
- Use
self.timer_state.is_running for the condition
- Ensure
_update_timer_display() is called every tick
What this page does
Shows "Paused" when timer is stopped mid-countdown.
Part: Foundation
Section: UI Rendering → Status Logic
Depends on: Page 55 (running branch defined)
This step is part of the larger process of communicating timer state.
Code (this page)
elif self.timer_state.remaining_seconds < self.DEFAULT_DURATION:
self.status_label.config(text="Paused")
Explanation
- Detects "paused" by comparing remaining time to default duration
- Works without needing a dedicated
is_paused flag
- Only valid in non-alarm, non-running state
- Timer has been started but is now stopped
This identifies the paused state through inference.
Why this matters
Without a "Paused" status, users couldn't distinguish between a fresh timer and one that's been stopped mid-countdown. The comparison to `DEFAULT_DURATION` elegantly detects this state.
This page will later include a screenshot showing "Paused" status.
⚠ If something breaks here
Common issues at this step:
- Using
<= instead of < (would show Paused at full duration)
- Wrong constant name
How to recover:
- Use
< to detect time has passed
- Use
self.DEFAULT_DURATION (Foundation constant)
What this page does
Shows "Ready" when timer is at full default duration and not running.
Part: Foundation
Section: UI Rendering → Status Logic
Depends on: Page 56 (paused branch defined)
This step is part of the larger process of communicating timer state.
Code (this page)
else:
self.status_label.config(text="Ready")
Explanation
- "Ready" indicates baseline idle state
- Occurs after reset or on app launch
- Completes the status selection ladder
- Final
else catches all remaining cases
This implements the Ready / Running / Paused State Model.
Why this matters
Without "Ready" status, users wouldn't know if the timer is prepared to start. This completes the four-state model: Ready → Running → Paused → Time's Up.
This page will later include a state diagram showing all four status values.
⚠ If something breaks here
Common issues at this step:
- Missing
else (no Ready status ever shown)
- Wrong indentation (else attached to wrong if)
How to recover:
- Ensure
else is at same indentation as elif
- Verify complete if/elif/else chain
What this page does
Creates the startup function that builds the window and app instance.
Part: Foundation
Section: Module & Runtime Boundary
Depends on: All previous pages (complete application)
This step is part of the larger process of making the application runnable.
Code (this page)
def main():
root = tk.Tk()
app = StudyTimerApp(root)
root.mainloop()
Explanation
- Creates the Tk root window
- Instantiates the application controller
- Starts Tkinter's event loop (blocks until window closes)
- Encapsulates all startup logic in one function
This is the complete startup sequence.
Why this matters
Without a main function, startup logic would be at module level, making testing harder. The function encapsulates the three-step launch sequence: create window, create app, run loop.
This page will later include a sequence diagram of the startup process.
⚠ If something breaks here
Common issues at this step:
- Forgetting to call
mainloop()
- Wrong order of operations
How to recover:
- Always end with
root.mainloop()
- Create root before app, start loop after app
What this page does
Ensures the app runs only when executed directly, not when imported.
Part: Foundation
Section: Module & Runtime Boundary
Depends on: Page 58 (main function exists)
This step is part of the larger process of making the module reusable.
Code (this page)
if __name__ == "__main__":
main()
Explanation
- Allows importing this file without auto-launching GUI
- Standard Python script pattern
- Keeps module reusable and testable
- Calls the main function only on direct execution
This completes the Foundation application.
Why this matters
Without the guard, importing the module for testing or extension would immediately launch the GUI. This pattern is a Python best practice for any executable module.
This page will later include a diagram showing import vs execution behavior.
⚠ If something breaks here
Common issues at this step:
- Typo in
__name__ or "__main__"
- Forgetting to call
main() (just referencing it)
How to recover:
- Use double underscores:
__name__ and "__main__"
- Include parentheses:
main()
What this page does
Defines what "done" means for the Foundation stage.
Part: Foundation
Section: Completion Criteria
Depends on: All Foundation pages (1–59)
This step is part of the larger process of verifying milestone completion.
Code (this page)
# No new code on this page.
Explanation
Why this matters
Without explicit completion criteria, "done" is ambiguous. This checklist ensures Foundation is fully functional before proceeding to Configuration. Every item maps to specific code implemented in pages 1–59.
This page will later include annotated screenshots demonstrating each checklist item.
⚠ If something breaks here
Common issues at this step:
- One or more checklist items fails
- Application crashes under certain conditions
How to recover:
- Identify which checklist item fails
- Review the corresponding pages for that feature
- Test in isolation before re-testing the full checklist
What this page does
Adds the import needed for the Settings configuration object.
Part: Configuration
Section: Domain State
Depends on: Page 1 (imports section)
This step is part of the larger process of enabling user-configurable settings.
Code (this page)
from dataclasses import dataclass
Explanation
dataclass provides a clean way to define data-holding classes
- Reduces boilerplate compared to manual
__init__
- Automatically generates
__repr__ and other useful methods
- Standard library module (no external dependencies)
This prepares for the Settings class definition.
Why this matters
Without dataclasses, the Settings class would require manual `__init__`, making it more verbose and error-prone. Dataclasses are the modern Python way to define simple data containers.
This page will later include a comparison of dataclass vs manual class definition.
⚠ If something breaks here
Common issues at this step:
- Python version < 3.7 (dataclasses not available)
- Typo in import statement
How to recover:
- Ensure Python 3.7+ is being used
- Verify spelling:
from dataclasses import dataclass
What this page does
Creates a single object to hold all configurable options.
Part: Configuration
Section: Domain State
Depends on: Page 61 (dataclass imported)
This step is part of the larger process of making behavior user-adjustable.
Code (this page)
@dataclass
class Settings:
"""Configuration state."""
default_duration_seconds: int = 25 * 60 # 25 minutes
alarm_enabled: bool = True
Explanation
- Replaces magic constants with explicit configuration state
default_duration_seconds stores the countdown duration (in seconds)
alarm_enabled controls whether the bell plays on completion
- Default values match Foundation behavior
- Creates a natural "source of truth" for UI defaults and runtime behavior
This implements the Configuration Object Pattern.
Why this matters
Without a Settings object, configuration would be scattered across multiple variables. This groups related settings together, making them easy to display in UI, validate, and eventually persist.
This page will later include a diagram showing Settings as the configuration hub.
⚠ If something breaks here
Common issues at this step:
- Missing
@dataclass decorator
- Wrong default value types
How to recover:
- Ensure
@dataclass appears directly above class Settings:
- Use
int for duration, bool for alarm_enabled
What this page does
Creates the Settings instance used by the application.
Part: Configuration
Section: Application Initialization
Depends on: Page 62 (Settings class exists)
This step is part of the larger process of connecting configuration to runtime behavior.
Code (this page)
# Settings
self.settings = Settings()
Explanation
- Establishes defaults immediately on startup
- Becomes the canonical object that the Settings UI reads from and writes to
- Keeps Foundation timer logic intact while adding a controlled extension point
- Instance variable accessible throughout the application
This creates the runtime configuration instance.
Why this matters
Without instantiating Settings, the class definition would be inert. This line creates the actual object that stores configuration and makes it accessible to all application methods.
This page will later include a diagram showing self.settings as the configuration source.
⚠ If something breaks here
Common issues at this step:
- Forgetting
self. prefix
- Instantiating before class is defined
How to recover:
- Use
self.settings to store on instance
- Ensure Settings class is defined before StudyTimerApp
What this page does
Sets the timer's starting duration from the configured default instead of a fixed constant.
Part: Configuration
Section: Application Initialization
Depends on: Page 63 (settings exists)
This step is part of the larger process of making duration configurable.
Code (this page)
# Timer state
self.timer_state = TimerState(self.settings.default_duration_seconds)
Explanation
- "Default duration" now comes from Settings (configuration source of truth)
- The timer still runs exactly like Foundation—only the initial value is configurable
- Keeps runtime TimerState transient (still not saved to disk)
- Replaces the hardcoded
DEFAULT_DURATION constant
This connects configuration to runtime state.
Why this matters
Without this change, the timer would ignore user configuration. This is the bridge between what the user configures and what the timer actually does.
This page will later include a diagram showing Settings → TimerState initialization flow.
⚠ If something breaks here
Common issues at this step:
- Using old
DEFAULT_DURATION constant instead of settings
- Settings not initialized before this line
How to recover:
- Use
self.settings.default_duration_seconds
- Ensure Settings() is created before TimerState()
What this page does
Adds a labeled UI area that holds all configuration controls.
Part: Configuration
Section: UI Composition → Settings Section
Depends on: Page 10 (main_frame exists)
This step is part of the larger process of building the configuration interface.
Code (this page)
settings_frame = tk.LabelFrame(main_frame, text="Settings", padx=10, pady=10)
settings_frame.pack(fill=tk.X, pady=(0, 15))
Explanation
LabelFrame provides a bordered container with a title
- "Settings" label clearly identifies the section purpose
- Internal padding creates visual breathing room
- Bottom margin separates from subsequent content
- Does not affect timer behavior until Apply is used
This creates the visual grouping for configuration controls.
Why this matters
Without a labeled container, configuration controls would blend with timer controls. The labeled frame provides clear visual hierarchy and groups related inputs together.
This page will later include a screenshot showing the Settings section.
⚠ If something breaks here
Common issues at this step:
- Using
Frame instead of LabelFrame (no title)
- Wrong parent widget
How to recover:
- Use
tk.LabelFrame for titled container
- Ensure parent is
main_frame
What this page does
Creates the row that holds the duration label and entry field side-by-side.
Part: Configuration
Section: UI Composition → Settings Section → Duration Setting
Depends on: Page 65 (settings_frame exists)
This step is part of the larger process of building the duration input.
Code (this page)
duration_row = tk.Frame(settings_frame)
duration_row.pack(fill=tk.X, pady=(0, 10))
Explanation
- Row frame enables side-by-side (horizontal) layout
fill=tk.X stretches to match parent width
- Bottom padding separates from next row
- Container for label and entry widgets
This establishes the duration input layout.
Why this matters
Without a row container, the label and entry would stack vertically. The frame enables the natural "Label: [Input]" horizontal layout that users expect.
This page will later include a wireframe showing the duration row layout.
⚠ If something breaks here
Common issues at this step:
- Wrong parent (should be
settings_frame)
- Missing
pack() call
How to recover:
- Ensure parent is
settings_frame
- Always call
pack() to make frame visible
What this page does
Adds the "Default Duration (minutes):" text label.
Part: Configuration
Section: UI Composition → Settings Section → Duration Setting
Depends on: Page 66 (duration_row exists)
This step is part of the larger process of building the duration input.
Code (this page)
tk.Label(duration_row, text="Default Duration (minutes):").pack(side=tk.LEFT)
Explanation
- Static label that never changes
- Explicitly states the unit (minutes) to avoid confusion
- Packed to the left side of the row
- Not stored in variable (never needs updating)
This provides context for the duration entry.
Why this matters
Without a label, users wouldn't know what the entry field is for or what unit to use. The explicit "(minutes)" prevents confusion between minutes and seconds.
This page will later include a screenshot showing the label in position.
⚠ If something breaks here
Common issues at this step:
- Wrong parent widget
- Missing
pack() call
- Forgetting
side=tk.LEFT
How to recover:
- Ensure parent is
duration_row
- Chain
.pack(side=tk.LEFT) after Label creation
What this page does
Creates the variable that backs the duration entry field and initializes it from Settings.
Part: Configuration
Section: UI Composition → Settings Section → Duration Setting
Depends on: Page 67 (label exists), Page 63 (settings exists)
This step is part of the larger process of enabling two-way UI binding.
Code (this page)
self.duration_var = tk.StringVar(
value=str(self.settings.default_duration_seconds // 60)
)
Explanation
- UI shows minutes, settings store seconds (unit conversion)
// 60 converts seconds → minutes for display
StringVar enables two-way binding with Entry widget
- Initial value reflects current settings
- Stored as instance variable for access in
_on_apply
This implements the Two-Way UI Binding Pattern.
Why this matters
Without a StringVar, reading the entry value would require calling `.get()` on the widget directly. The StringVar provides a cleaner abstraction and enables automatic UI updates.
This page will later include a diagram showing StringVar ↔ Entry binding.
⚠ If something breaks here
Common issues at this step:
- Using
/ instead of // (produces float string)
- Forgetting
str() conversion
- Wrong source for initial value
How to recover:
- Use
// for integer division
- Wrap in
str() for StringVar
- Use
self.settings.default_duration_seconds
What this page does
Adds the text field where the user types the default duration.
Part: Configuration
Section: UI Composition → Settings Section → Duration Setting
Depends on: Page 68 (duration_var exists)
This step is part of the larger process of capturing user input.
Code (this page)
self.duration_entry = tk.Entry(duration_row, textvariable=self.duration_var, width=8)
self.duration_entry.pack(side=tk.LEFT, padx=(10, 0))
Explanation
textvariable creates two-way binding with duration_var
- Width of 8 characters supports typical values ("999") comfortably
- Left padding separates from the label
- Changes do not affect timer until Apply is pressed
- Stored as instance variable for potential validation styling
This is where users enter their desired duration.
Why this matters
Without an entry field, users couldn't change the duration. The `textvariable` binding ensures the StringVar always reflects the current entry content.
This page will later include a screenshot showing the entry field.
⚠ If something breaks here
Common issues at this step:
- Missing
textvariable parameter (no binding)
- Wrong parent widget
How to recover:
- Include
textvariable=self.duration_var
- Ensure parent is
duration_row
What this page does
Creates the variable that tracks the alarm checkbox state.
Part: Configuration
Section: UI Composition → Settings Section → Alarm Toggle
Depends on: Page 65 (settings_frame exists), Page 63 (settings exists)
This step is part of the larger process of enabling the alarm toggle.
Code (this page)
self.alarm_var = tk.BooleanVar(value=self.settings.alarm_enabled)
Explanation
BooleanVar is the appropriate type for checkbox binding
- Initial value reflects current settings (True by default)
- Stored as instance variable for access in
_on_apply
- Will be read when Apply button is clicked
This prepares for the checkbox widget.
Why this matters
Without a BooleanVar, reading the checkbox state would be more complex. The variable provides a clean interface for getting and setting the checkbox value.
This page will later include a diagram showing BooleanVar ↔ Checkbutton binding.
⚠ If something breaks here
Common issues at this step:
- Using StringVar instead of BooleanVar
- Wrong initial value source
How to recover:
- Use
tk.BooleanVar for boolean values
- Use
self.settings.alarm_enabled for initial value
What this page does
Adds the checkbox that controls whether the alarm sound is enabled.
Part: Configuration
Section: UI Composition → Settings Section → Alarm Toggle
Depends on: Page 70 (alarm_var exists)
This step is part of the larger process of building the alarm toggle.
Code (this page)
self.alarm_check = tk.Checkbutton(
settings_frame,
text="Enable alarm sound",
variable=self.alarm_var
)
self.alarm_check.pack(anchor=tk.W)
Explanation
Checkbutton provides a standard checkbox widget
variable binding connects to alarm_var
- Text describes what the checkbox controls
anchor=tk.W aligns to the left (west)
- Stored for potential styling changes
This allows users to toggle the alarm sound.
Why this matters
Without a checkbox, users couldn't disable the alarm sound. This provides a simple on/off toggle for the audio feedback feature.
This page will later include a screenshot showing the checkbox.
⚠ If something breaks here
Common issues at this step:
- Missing
variable parameter (no binding)
- Wrong parent widget
How to recover:
- Include
variable=self.alarm_var
- Ensure parent is
settings_frame
What this page does
Creates the frame that holds the Apply Settings button.
Part: Configuration
Section: UI Composition → Apply Controls
Depends on: Page 65 (settings section complete)
This step is part of the larger process of building the configuration commit mechanism.
Code (this page)
apply_frame = tk.Frame(main_frame)
apply_frame.pack(fill=tk.X)
Explanation
- Separate frame keeps Apply button distinct from settings inputs
fill=tk.X allows for future additional buttons
- No padding needed (button defines its own spacing)
- Positioned after settings section
This creates the container for configuration actions.
Why this matters
Without a container, the Apply button would need to be placed directly in settings_frame, mixing action buttons with input controls. This separation maintains clean visual hierarchy.
This page will later include a wireframe showing the apply frame position.
⚠ If something breaks here
Common issues at this step:
- Wrong parent (should be
main_frame, not settings_frame)
- Missing
pack() call
How to recover:
- Ensure parent is
main_frame
- Always call
pack() to make frame visible
What this page does
Adds the button that validates and applies UI settings into the live application.
Part: Configuration
Section: UI Composition → Apply Controls
Depends on: Page 72 (apply_frame exists)
This step is part of the larger process of implementing the configuration commit workflow.
Code (this page)
self.apply_button = tk.Button(
apply_frame,
text="Apply Settings",
width=15,
command=self._on_apply
)
self.apply_button.pack(side=tk.LEFT)
Explanation
- Establishes an explicit "commit" action for configuration changes
command connects to _on_apply handler (defined later)
- Width standardized for visual consistency
- Allows validation before the app updates its state
- Keeps user flow deterministic (no surprise updates while typing)
This is the configuration commit point.
Why this matters
Without an Apply button, changes would either auto-apply (confusing) or never apply. The explicit commit gives users control and enables validation before accepting changes.
This page will later include a screenshot showing the Apply button.
⚠ If something breaks here
Common issues at this step:
_on_apply not yet defined (expected at this stage)
- Wrong parent widget
How to recover:
- Method reference errors resolve when handler is implemented
- Ensure parent is
apply_frame
What this page does
Creates the helper method that parses and validates the minutes input from the UI.
Part: Configuration
Section: Input & Validation Pipeline
Depends on: Page 68 (duration_var exists)
This step is part of the larger process of validating user input.
Code (this page)
def _parse_duration_input(self) -> Optional[int]:
"""Parse and validate duration entry. Returns seconds or None if invalid."""
Explanation
- Centralizes input validation in one place
- Returns seconds (internal unit) or None (invalid)
- Type hint clarifies return contract
- Keeps
_on_apply() small and readable
This implements the Validation-as-Gate Pattern.
Why this matters
Without centralized validation, parsing logic would be duplicated or inlined in the handler. This helper makes validation testable, reusable, and clearly documented.
This page will later include a flowchart of the validation logic.
⚠ If something breaks here
Common issues at this step:
- Missing
Optional import
- Wrong return type annotation
How to recover:
- Ensure
from typing import Optional is in imports
- Use
Optional[int] for nullable int return
What this page does
Reads the Entry's StringVar and attempts to convert it to an integer minutes value.
Part: Configuration
Section: Input & Validation Pipeline
Depends on: Page 74 (parse method exists)
This step is part of the larger process of validating duration input.
Code (this page)
try:
minutes = int(self.duration_var.get().strip())
Explanation
.get() reads the Entry content via StringVar
.strip() removes whitespace around the number
int(...) enforces numeric input
- Invalid input (letters, symbols) will raise
ValueError
try block catches conversion failures
This is the first step of validation.
Why this matters
Without parsing, the string value couldn't be used for calculations. Without `try/except`, invalid input would crash the application. This safely handles user input of any kind.
This page will later include examples of valid and invalid inputs.
⚠ If something breaks here
Common issues at this step:
- Forgetting
.strip() (whitespace causes errors)
- Wrong variable reference
How to recover:
- Always strip input:
.get().strip()
- Use
self.duration_var for the duration entry
What this page does
Rejects duration values that are too small.
Part: Configuration
Section: Input & Validation Pipeline
Depends on: Page 75 (minutes parsed)
This step is part of the larger process of enforcing valid ranges.
Code (this page)
if minutes < 1:
return None
Explanation
- Rejects zero and negative values
- Minimum useful duration is 1 minute
- Returns
None to signal invalid input
- Early return keeps code flat
This enforces the lower bound.
Why this matters
Without a minimum check, users could set 0 minutes (immediate alarm) or negative values (undefined behavior). One minute is the smallest practical study session.
This page will later include a number line showing valid range.
⚠ If something breaks here
Common issues at this step:
- Using
<= instead of < (rejects 1)
- Forgetting
return
How to recover:
- Use
< 1 to allow 1 as valid
- Always
return None for invalid input
What this page does
Rejects duration values that are too large.
Part: Configuration
Section: Input & Validation Pipeline
Depends on: Page 76 (minimum validated)
This step is part of the larger process of enforcing valid ranges.
Code (this page)
if minutes > 999:
return None
Explanation
- Rejects unreasonably large values
- 999 minutes (~16.5 hours) is a practical upper limit
- Entry field width (8 chars) accommodates this range
- Returns
None to signal invalid input
This enforces the upper bound.
Why this matters
Without a maximum check, users could enter values that overflow displays or represent impractical durations. 999 minutes provides ample range for any reasonable study session.
This page will later include the complete valid range visualization.
⚠ If something breaks here
Common issues at this step:
- Using
>= instead of > (rejects 999)
- Wrong threshold value
How to recover:
- Use
> 999 to allow 999 as valid
- Ensure threshold matches UI field width
What this page does
Returns the validated duration converted to seconds.
Part: Configuration
Section: Input & Validation Pipeline
Depends on: Page 77 (range validated)
This step is part of the larger process of providing validated output.
Code (this page)
Explanation
- Internal model stores duration in seconds
- Conversion happens once, at validation time
- Returns the value ready for use in Settings
- Only reached if all validation passed
This completes the successful validation path.
Why this matters
Without conversion, the caller would need to handle unit conversion. Doing it here ensures the return value is always in the correct unit (seconds) for internal use.
This page will later include a unit conversion diagram.
⚠ If something breaks here
Common issues at this step:
- Forgetting
return (function returns None)
- Wrong conversion factor
How to recover:
- Always
return the calculated value
- Use
* 60 to convert minutes to seconds
What this page does
Converts invalid parse attempts into None return value.
Part: Configuration
Section: Input & Validation Pipeline
Depends on: Page 75 (try block started)
This step is part of the larger process of handling invalid input gracefully.
Code (this page)
except ValueError:
return None
Explanation
- Catches
ValueError from int() conversion
- Non-numeric input (letters, symbols, empty) becomes
None
- No crash, no exception propagation
- Caller handles
None as "invalid input"
This completes the validation method.
Why this matters
Without exception handling, typing "abc" in the duration field would crash the application. This ensures any input is handled gracefully with clear invalid signaling.
This page will later include examples of inputs that trigger ValueError.
⚠ If something breaks here
Common issues at this step:
- Catching wrong exception type
- Missing
return None
How to recover:
- Catch
ValueError specifically
- Always
return None in the except block
What this page does
Creates the Apply button handler that commits UI settings into runtime settings.
Part: Configuration
Section: Application Logic → Apply Handler
Depends on: Page 73 (Apply button references this method)
This step is part of the larger process of implementing the configuration commit workflow.
Code (this page)
def _on_apply(self):
"""Apply settings button handler."""
Explanation
- Apply is the explicit "commit point" in Configuration
- No auto-apply behavior introduced
- Handler will validate, update settings, and provide feedback
- Docstring clarifies purpose
This establishes the apply handler structure.
Why this matters
Without a handler, the Apply button would do nothing. This method is the central coordinator for all configuration changes.
This page will later include a flowchart of the apply handler logic.
⚠ If something breaks here
Common issues at this step:
- Method name doesn't match button's
command reference
- Missing
self parameter
How to recover:
- Ensure name matches:
_on_apply
- Include
self as first parameter
What this page does
Runs validation and blocks Apply if input is invalid.
Part: Configuration
Section: Application Logic → Apply Handler
Depends on: Page 80 (handler exists), Page 74 (parse method exists)
This step is part of the larger process of preventing invalid configuration.
Code (this page)
duration_seconds = self._parse_duration_input()
if duration_seconds is None:
messagebox.showerror(
"Invalid Input",
"Duration must be a number between 1 and 999 minutes."
)
return
Explanation
- Single validation path through
_parse_duration_input()
None result triggers error dialog
- Clear error message to user with valid range
- Early return prevents updating settings with invalid data
- Uses
messagebox.showerror for error feedback
This implements the Validation-as-Gate Pattern.
Why this matters
Without validation gating, invalid values could corrupt settings. The error dialog provides immediate, clear feedback about what went wrong and how to fix it.
This page will later include a screenshot of the error dialog.
⚠ If something breaks here
Common issues at this step:
- Missing
messagebox import
- Wrong comparison (using
== None instead of is None)
How to recover:
- Ensure
from tkinter import messagebox is in imports
- Use
is None for None comparison (PEP 8)
What this page does
Stores the prior default duration for later "smart sync" rules.
Part: Configuration
Section: Application Logic → Apply Handler
Depends on: Page 81 (validation passed)
This step is part of the larger process of safely updating timer state.
Code (this page)
old_default = self.settings.default_duration_seconds
Explanation
- Needed to determine whether the timer display is still at the old default
- Supports safe updates without unexpectedly changing a paused countdown
- Must be captured before settings are updated
- Simple snapshot of current value
This enables the Ready-State-Only Sync Pattern.
Why this matters
Without capturing the old value, we couldn't distinguish between a timer at default (safe to update) and a timer that's been modified (unsafe to update). This protects user progress.
This page will later include a diagram showing the old vs new comparison.
⚠ If something breaks here
Common issues at this step:
- Capturing after settings update (wrong value)
- Wrong attribute name
How to recover:
- Place this line before any settings modification
- Use
self.settings.default_duration_seconds
What this page does
Commits the validated duration into the Settings object.
Part: Configuration
Section: Application Logic → Apply Handler
Depends on: Page 82 (old value captured)
This step is part of the larger process of applying configuration changes.
Code (this page)
self.settings.default_duration_seconds = duration_seconds
Explanation
- Duration comes from validated parse (already in seconds)
- Settings becomes the new runtime source of truth
- Reset and Paused logic depend on this value
- Single point of assignment
This updates the configuration.
Why this matters
Without updating settings, the validated input would be discarded. This line is the actual "apply" action for duration—the moment configuration changes take effect.
This page will later include a diagram showing the apply flow.
⚠ If something breaks here
Common issues at this step:
- Using raw minutes instead of validated seconds
- Wrong attribute name
How to recover:
- Use
duration_seconds (already converted)
- Use
default_duration_seconds attribute
What this page does
Commits the alarm toggle state into the Settings object.
Part: Configuration
Section: Application Logic → Apply Handler
Depends on: Page 83 (duration updated)
This step is part of the larger process of applying configuration changes.
Code (this page)
self.settings.alarm_enabled = self.alarm_var.get()
Explanation
- Alarm comes directly from checkbox BooleanVar
.get() retrieves current checkbox state
- Settings stores the authoritative value
- Alarm behavior reads from Settings (not BooleanVar directly)
This updates the alarm configuration.
Why this matters
Without updating settings, the checkbox change wouldn't affect behavior. The separation ensures UI state and runtime state are synchronized through Settings.
This page will later include a diagram showing checkbox → settings flow.
⚠ If something breaks here
Common issues at this step:
- Forgetting
.get() (assigns BooleanVar, not bool)
- Wrong variable reference
How to recover:
- Always call
.get() to extract the value
- Use
self.alarm_var for the alarm checkbox
What this page does
Checks if it's safe to update the countdown to the new default duration.
Part: Configuration
Section: Application Logic → Apply Handler
Depends on: Page 84 (settings updated), Page 82 (old_default captured)
This step is part of the larger process of protecting user progress.
Code (this page)
if (not self.timer_state.is_running and
not self.timer_state.alarm_triggered and
self.timer_state.remaining_seconds == old_default):
Explanation
not is_running: Don't interrupt an active countdown
not alarm_triggered: Don't reset after completion without explicit Reset
remaining_seconds == old_default: Only update if still at default (Ready state)
- All three conditions must be true for safe update
This implements the Ready-State-Only Sync Pattern.
Why this matters
Without these guards, Apply would overwrite a paused session or reset progress unexpectedly. This three-part condition ensures Apply only affects timers that haven't been used yet.
This page will later include a decision tree for the sync logic.
⚠ If something breaks here
Common issues at this step:
- Missing one of the three conditions
- Wrong comparison (new default instead of old)
How to recover:
- Include all three guards
- Compare against
old_default, not current settings
What this page does
Updates the countdown to the new default duration when safe.
Part: Configuration
Section: Application Logic → Apply Handler
Depends on: Page 85 (condition checked)
This step is part of the larger process of synchronizing configuration with timer state.
Code (this page)
self.timer_state.remaining_seconds = self.settings.default_duration_seconds
Explanation
- Only executes if all three safety conditions passed
- Updates countdown to match new settings
- Keeps UI consistent after Apply when timer is idle
- Display will update on next tick
This is the safe timer update.
Why this matters
Without this update, users would need to click Reset after every Apply to see the new duration. The smart sync provides a better user experience while protecting active sessions.
This page will later include before/after screenshots of smart sync.
⚠ If something breaks here
Common issues at this step:
- Wrong indentation (executes without condition)
- Using old_default instead of new settings
How to recover:
- Ensure proper indentation inside the
if block
- Use
self.settings.default_duration_seconds (new value)
What this page does
Confirms that Apply succeeded with an info dialog.
Part: Configuration
Section: Application Logic → Apply Handler
Depends on: Page 86 (sync complete)
This step is part of the larger process of providing user feedback.
Code (this page)
messagebox.showinfo("Settings", "Settings applied!")
Explanation
- Confirms the change was accepted
- Uses info dialog (not error) for success
- Brief, clear message
- Completes the Apply workflow cleanly
This provides positive feedback to the user.
Why this matters
Without confirmation, users wouldn't know if Apply worked. The info dialog provides clear feedback that the action completed successfully.
This page will later include a screenshot of the success dialog.
⚠ If something breaks here
Common issues at this step:
- Using
showerror instead of showinfo
- Missing import
How to recover:
- Use
messagebox.showinfo for success messages
- Ensure messagebox is imported
What this page does
Changes alarm behavior so sound only plays if enabled in settings.
Part: Configuration
Section: Behavior Change → Alarm
Depends on: Page 34 (original alarm sound), Page 63 (settings exists)
This step is part of the larger process of making alarm behavior configurable.
Code (this page)
if self.settings.alarm_enabled:
self.root.bell()
Explanation
- In Foundation: bell always plays
- In Configuration: bell respects user toggle
- Simple conditional wrapping the existing call
- No change to alarm state logic, only to sound
This is the core "alarm toggle" feature.
Why this matters
Without this conditional, the alarm checkbox would have no effect. This connects the UI setting to actual behavior—the entire point of the Configuration stage.
This page will later include a comparison of Foundation vs Configuration alarm behavior.
⚠ If something breaks here
Common issues at this step:
- Checking
alarm_var instead of settings.alarm_enabled
- Wrong indentation (not inside
_trigger_alarm)
How to recover:
- Use
self.settings.alarm_enabled (runtime setting)
- Ensure this is inside
_trigger_alarm method
What this page does
Confirms that alarm state transition logic remains identical to Foundation.
Part: Configuration
Section: Alarm State
Depends on: Page 88 (sound conditional added)
This step is part of the larger process of ensuring correctness.
Code (this page)
self.timer_state.alarm_triggered = True
self.timer_state.is_running = False
self._last_tick_time = None
self._accumulated_time = 0.0
Explanation
- The toggle only affects sound, not timer correctness
- Alarm state still becomes "sticky" (prevents repeat triggers)
- Timer still stops running
- Timing variables still clear
- State machine is unchanged from Foundation
This preserves Foundation correctness.
Why this matters
Configuration should only add features, not break existing behavior. This verification ensures the alarm toggle doesn't interfere with proper timer state management.
This page will later include a state diagram showing unchanged alarm transition.
⚠ If something breaks here
Common issues at this step:
- Accidentally moving state logic inside the sound conditional
- Missing one of the four state updates
How to recover:
- Keep state logic outside/after the sound conditional
- Verify all four lines are present
What this page does
Changes Reset to use the configured default duration instead of a constant.
Part: Configuration
Section: Behavior Change → Reset
Depends on: Page 39 (original reset handler), Page 63 (settings exists)
This step is part of the larger process of making duration configurable.
Code (this page)
self.timer_state.remaining_seconds = self.settings.default_duration_seconds
Explanation
- In Foundation: Reset returned to fixed
DEFAULT_DURATION
- In Configuration: Reset returns to user-configured default
- Single line change in
_on_reset handler
- Rest of reset logic unchanged
This connects Reset to configuration.
Why this matters
Without this change, Reset would ignore user configuration and always return to 25 minutes. This ensures the configured duration becomes the new "default" for the application.
This page will later include a before/after comparison of Reset behavior.
⚠ If something breaks here
Common issues at this step:
- Still using
DEFAULT_DURATION constant
- Wrong attribute name
How to recover:
- Replace constant with
self.settings.default_duration_seconds
- Verify attribute name matches Settings class
What this page does
Changes "Paused" detection to compare against the configured default duration.
Part: Configuration
Section: UI Rendering → Status Logic
Depends on: Page 56 (original paused logic), Page 63 (settings exists)
This step is part of the larger process of making status logic consistent with configuration.
Code (this page)
elif self.timer_state.remaining_seconds < self.settings.default_duration_seconds:
self.status_label.config(text="Paused")
Explanation
- "Paused" means: not running + timer not at full default
- Must compare against Settings default in Configuration stage
- If duration is 10 minutes, "Ready" means 10:00, not 25:00
- Ensures status correctly reflects configured behavior
This makes status consistent with configuration.
Why this matters
Without this change, "Paused" detection would use the old constant, causing incorrect status when duration is changed. Status must reflect the user's configured expectations.
This page will later include examples of status with different durations.
⚠ If something breaks here
Common issues at this step:
- Still using
DEFAULT_DURATION constant
- Comparing against wrong value
How to recover:
- Replace constant with
self.settings.default_duration_seconds
- Verify comparison is
<, not <=
What this page does
Confirms that core timing logic remains identical to Foundation.
Part: Configuration
Section: Timer Core Verification
Depends on: All tick loop pages
This step is part of the larger process of ensuring correctness.
Code (this page)
if self.timer_state.is_running:
current_time = time.monotonic()
Explanation
- Monotonic timing prevents clock-change issues
- Configuration does not alter countdown correctness
- Running check still guards countdown logic
- Foundation timing model preserved exactly
This verifies core timing integrity.
Why this matters
Configuration adds features but must not break the timing engine. This verification ensures the countdown remains accurate regardless of configuration changes.
This page will later include a verification checklist for timing accuracy.
⚠ If something breaks here
Common issues at this step:
- Accidentally modifying tick logic during Configuration changes
- Using wrong time function
How to recover:
- Compare tick method against Foundation version
- Always use
time.monotonic() for elapsed time
What this page does
Confirms that fractional-time accumulator behavior is unchanged.
Part: Configuration
Section: Timer Core Verification
Depends on: Page 25 (original accumulation logic)
This step is part of the larger process of ensuring correctness.
Code (this page)
if self._last_tick_time is not None:
elapsed = current_time - self._last_tick_time
self._accumulated_time += elapsed
Explanation
- Handles tick jitter properly
- Countdown remains stable even with delayed UI scheduling
- Accumulation logic unchanged from Foundation
- Configuration doesn't affect timing precision
This verifies accumulation integrity.
Why this matters
The accumulated time pattern ensures accurate countdown regardless of system load. Configuration must not interfere with this precision mechanism.
This page will later include a timing precision verification diagram.
⚠ If something breaks here
Common issues at this step:
- Modifying accumulation during other changes
- Wrong operator (
= instead of +=)
How to recover:
- Compare against Foundation version
- Use
+= for accumulation
What this page does
Confirms that timer decrements whole seconds correctly.
Part: Configuration
Section: Timer Core Verification
Depends on: Page 26 (original decrement logic)
This step is part of the larger process of ensuring correctness.
Code (this page)
while self._accumulated_time >= 1.0 and self.timer_state.remaining_seconds > 0:
self.timer_state.remaining_seconds -= 1
self._accumulated_time -= 1.0
Explanation
- Converts elapsed time into integer countdown steps
- While loop handles delayed ticks correctly
- Zero guard prevents negative countdown
- Avoids "drift" from fractional tick intervals
This verifies decrement integrity.
Why this matters
The while loop ensures no seconds are lost even if a tick is delayed. Configuration must preserve this correctness guarantee.
This page will later include a decrement verification diagram.
⚠ If something breaks here
Common issues at this step:
- Using
if instead of while
- Missing zero check
How to recover:
- Use
while for multiple-second handling
- Include
remaining_seconds > 0 guard
What this page does
Confirms that the alarm trigger guard is unchanged.
Part: Configuration
Section: Timer Core Verification
Depends on: Page 27 (original alarm detection)
This step is part of the larger process of ensuring correctness.
Code (this page)
if self.timer_state.remaining_seconds == 0 and not self.timer_state.alarm_triggered:
self._trigger_alarm()
Explanation
- Prevents repeated triggering while at 00:00
- Works with alarm toggle safely
- Alarm sound may be disabled, but trigger logic is the same
- State transition always happens, sound is optional
This verifies trigger integrity.
Why this matters
The alarm must trigger exactly once regardless of sound settings. This verification ensures the toggle only affects audio, not the alarm state machine.
This page will later include a verification of single-trigger behavior.
⚠ If something breaks here
Common issues at this step:
- Moving trigger condition inside sound conditional
- Modifying the condition itself
How to recover:
- Keep trigger condition unchanged
- Sound conditional is inside
_trigger_alarm, not here
What this page does
Confirms that Apply handler uses `_parse_duration_input()` output correctly.
Part: Configuration
Section: Apply Handler Verification
Depends on: Page 81 (parse call in handler)
This step is part of the larger process of ensuring correct data flow.
Code (this page)
duration_seconds = self._parse_duration_input()
Explanation
- Keeps internal unit consistent (seconds everywhere)
- Avoids duplicating validation logic in handler
- Single validation path through helper method
- Result is either valid seconds or None
This verifies data flow integrity.
Why this matters
Without centralized parsing, validation could be inconsistent. Using the helper ensures all duration input goes through the same validation pipeline.
This page will later include a data flow diagram.
⚠ If something breaks here
Common issues at this step:
- Inline parsing instead of using helper
- Ignoring None return value
How to recover:
- Always use
_parse_duration_input() for duration parsing
- Always check for None before using result
What this page does
Confirms that Apply correctly updates settings duration.
Part: Configuration
Section: Apply Handler Verification
Depends on: Page 83 (duration update)
This step is part of the larger process of ensuring correct configuration flow.
Code (this page)
self.settings.default_duration_seconds = duration_seconds
Explanation
- Settings becomes runtime reference for default duration
- Reset and Paused logic depend on this value
- Value comes from validated parse (already in seconds)
- Single point of assignment
This verifies settings update integrity.
Why this matters
Without correct settings update, configuration changes would be lost. This is the core of the Apply functionality.
This page will later include a settings update flow diagram.
⚠ If something breaks here
Common issues at this step:
- Wrong attribute name
- Using minutes instead of seconds
How to recover:
- Use
default_duration_seconds attribute
- Use validated
duration_seconds variable
What this page does
Confirms that Apply correctly updates settings alarm state.
Part: Configuration
Section: Apply Handler Verification
Depends on: Page 84 (alarm update)
This step is part of the larger process of ensuring correct configuration flow.
Code (this page)
self.settings.alarm_enabled = self.alarm_var.get()
Explanation
- Alarm behavior reads from Settings (not BooleanVar directly)
- BooleanVar is just the UI source
.get() extracts boolean value from variable
- Settings is the runtime authority
This verifies alarm update integrity.
Why this matters
Without correct settings update, the checkbox would have no effect. This connects UI state to runtime behavior.
This page will later include a checkbox → settings flow diagram.
⚠ If something breaks here
Common issues at this step:
- Forgetting
.get() call
- Wrong variable name
How to recover:
- Always call
.get() on Tkinter variables
- Use
self.alarm_var for the alarm checkbox
What this page does
Confirms that smart sync compares against the old default value.
Part: Configuration
Section: Apply Handler Verification
Depends on: Page 85 (sync condition)
This step is part of the larger process of protecting user progress.
Code (this page)
self.timer_state.remaining_seconds == old_default
Explanation
- Only "Ready" timers equal the old default
- Paused timers have different remaining time
- Must compare against captured old value, not current settings
- This is the key to protecting user progress
This verifies sync logic integrity.
Why this matters
Without comparing against the old default, the sync would use circular logic (comparing new settings to new settings). The captured snapshot enables correct detection.
This page will later include a comparison diagram.
⚠ If something breaks here
Common issues at this step:
- Comparing against current settings instead of old value
- Forgetting to capture old value first
How to recover:
- Use
old_default variable from earlier capture
- Ensure capture happens before settings update
What this page does
Defines what "done" means for the Configuration stage.
Part: Configuration
Section: Completion Criteria
Depends on: All Configuration pages (61–99)
This step is part of the larger process of verifying milestone completion.
Code (this page)
# No new code on this page.
Explanation
Why this matters
Without explicit completion criteria, Configuration could accidentally include Persistence features. This checklist ensures Configuration is complete but properly scoped.
This page will later include annotated screenshots demonstrating each checklist item.
⚠ If something breaks here
Common issues at this step:
- One or more checklist items fails
- Persistence features accidentally included
How to recover:
- Identify which checklist item fails
- Review the corresponding pages for that feature
- Remove any premature Persistence code
What this page does
Confirms that Apply shows a success dialog after completing.
Part: Configuration
Section: Apply Handler Verification
Depends on: Page 87 (success feedback)
This step is part of the larger process of verifying complete Configuration implementation.
Code (this page)
messagebox.showinfo("Settings", "Settings applied!")
Explanation
- Signals completion to user
- Helps confirm changes took effect
- Uses
showinfo (not showerror) for success
- Completes the Apply workflow with positive feedback
This verifies UX completeness.
Why this matters
Without confirmation feedback, users wouldn't know if Apply worked. This verification ensures the user experience is complete.
This page will later include a screenshot of the success dialog.
⚠ If something breaks here
Common issues at this step:
- Wrong dialog type used
- Dialog shown before settings update
How to recover:
- Use
messagebox.showinfo for success
- Ensure dialog is last step in Apply
What this page does
Ensures Configuration imports include only what is needed for this stage.
Part: Configuration
Section: Module Setup Verification
Depends on: Page 61 (dataclass import)
This step is part of the larger process of maintaining stage boundaries.
Code (this page)
from dataclasses import dataclass
Explanation
- Needed for the Settings dataclass
- Still no JSON / Path imports (those belong to Persistence)
- Configuration uses only standard library features
- Clean separation by stage
This verifies import scope.
Why this matters
Premature imports signal scope creep. Configuration should not import persistence-related modules like `json` or `pathlib`.
This page will later include a comparison of imports across stages.
⚠ If something breaks here
Common issues at this step:
- Accidentally importing persistence modules early
- Missing dataclass import
How to recover:
- Remove any
json or pathlib imports
- Ensure
from dataclasses import dataclass is present
What this page does
Confirms Settings contains only the two configuration fields.
Part: Configuration
Section: Domain State Verification
Depends on: Page 62 (Settings class)
This step is part of the larger process of maintaining clean domain boundaries.
Code (this page)
default_duration_seconds: int = 25 * 60
alarm_enabled: bool = True
Explanation
- Exactly the two configuration knobs needed
- No save status field (that's Persistence)
- No file path field (that's Persistence)
- No copy snapshot (that's Persistence)
This verifies domain scope.
Why this matters
Extra fields in Settings would indicate Persistence features leaking into Configuration. The dataclass should be minimal for this stage.
This page will later include a comparison of Settings across stages.
⚠ If something breaks here
Common issues at this step:
- Extra fields added prematurely
- Wrong default values
How to recover:
- Remove any persistence-related fields
- Verify defaults match Foundation behavior
What this page does
Confirms TimerState remains runtime-only and unchanged from Foundation.
Part: Configuration
Section: Domain State Verification
Depends on: Page 2 (TimerState class)
This step is part of the larger process of maintaining domain boundaries.
Code (this page)
self.alarm_triggered: bool = False
Explanation
- Still used to prevent re-triggering alarm
- Not stored in Settings
- Not persisted to disk (ever, in any stage)
- Runtime behavior only
This verifies domain separation.
Why this matters
TimerState must never be confused with Settings. Timer runtime state (remaining time, running, alarm triggered) is ephemeral; only configuration is persisted.
This page will later include a diagram showing TimerState vs Settings boundaries.
⚠ If something breaks here
Common issues at this step:
- Accidentally adding persistence to TimerState
- Confusing Settings and TimerState
How to recover:
- Keep TimerState exactly as defined in Foundation
- Only Settings is persisted (and only in Persistence stage)
What this page does
Ensures Reset returns the application to a clean state.
Part: Configuration
Section: Reset Handler Verification
Depends on: Page 39 (reset handler)
This step is part of the larger process of verifying complete state management.
Code (this page)
self.timer_state.alarm_triggered = False
Explanation
- Allows a fresh run after alarm completion
- Restores normal rendering (removes red background)
- Restores start eligibility (Start button works again)
- Consistent with Foundation behavior
This verifies reset completeness.
Why this matters
Without clearing alarm_triggered, users couldn't start a new session after completion. Reset must fully restore Ready state.
This page will later include a state diagram showing Reset transition.
⚠ If something breaks here
Common issues at this step:
- Forgetting to clear alarm_triggered
- Clearing in wrong method
How to recover:
- Ensure
alarm_triggered = False is in _on_reset
- Verify Reset returns to Ready state
What this page does
Ensures start transition rules remain stable from Foundation.
Part: Configuration
Section: Start Handler Verification
Depends on: Page 36 (start guards)
This step is part of the larger process of verifying state machine integrity.
Code (this page)
if self.timer_state.alarm_triggered:
return
Explanation
- Prevents starting after alarm without reset
- Keeps state machine clean
- Part of the three-guard protection
- Unchanged from Foundation
This verifies guard integrity.
Why this matters
Without this guard, users could start from alarmed state, causing undefined behavior. Guards enforce valid state transitions.
This page will later include a state machine diagram showing blocked transitions.
⚠ If something breaks here
Common issues at this step:
- Missing alarm_triggered guard
- Guards in wrong order
How to recover:
- Ensure all three guards from Foundation are present
- Verify early return pattern
What this page does
Confirms alarm red styling remains consistent across stages.
Part: Configuration
Section: Display Verification
Depends on: Page 53 (alarm visuals)
This step is part of the larger process of verifying visual consistency.
Code (this page)
self.status_label.config(bg="#ffcccc", text="Time's Up!", fg="#cc0000")
Explanation
- Alarm visuals remain the same as Foundation
- Only alarm sound is now conditional (respects settings)
- Visual feedback is always provided
- Color scheme unchanged
This verifies visual consistency.
Why this matters
The alarm toggle affects sound only, not visuals. Users always see red background and "Time's Up!" regardless of sound setting.
This page will later include a screenshot of alarm state.
⚠ If something breaks here
Common issues at this step:
- Accidentally making visuals conditional on sound
- Wrong color values
How to recover:
- Keep visual code outside any sound conditional
- Verify exact hex values match
What this page does
Confirms live clock behavior is unchanged from Foundation.
Part: Configuration
Section: Background Process Verification
Depends on: Page 22 (clock update)
This step is part of the larger process of verifying core functionality.
Code (this page)
self.root.after(self.CLOCK_UPDATE_MS, self._update_clock)
Explanation
- Keeps UI responsive
- Updates once per second
- Non-blocking via
root.after()
- Unchanged from Foundation
This verifies background process integrity.
Why this matters
The clock update loop must continue working identically. Configuration should not affect core timing behavior.
This page will later include timing verification notes.
⚠ If something breaks here
Common issues at this step:
- Accidentally modifying clock update timing
- Breaking the self-rescheduling pattern
How to recover:
- Compare against Foundation clock code
- Ensure
after() call is always present
What this page does
Confirms startup remains the standard Tkinter pattern.
Part: Configuration
Section: Entry Point Verification
Depends on: Pages 58-59 (main function and guard)
This step is part of the larger process of verifying application structure.
Code (this page)
root = tk.Tk()
app = StudyTimerApp(root)
root.mainloop()
Explanation
- Creates root window
- Instantiates app
- Runs the UI event loop
- No persistence bootstrap yet (that's next stage)
This verifies entry point integrity.
Why this matters
The entry point should remain simple in Configuration. Persistence will add load-on-startup, but Configuration just creates and runs.
This page will later include a comparison of entry points across stages.
⚠ If something breaks here
Common issues at this step:
- Premature persistence code in entry point
- Wrong order of operations
How to recover:
- Keep entry point minimal for Configuration
- Persistence loading is added in next stage
What this page does
Explicitly verifies the Configuration stage includes no Persistence behaviors.
Part: Configuration
Section: Stage Boundary Verification
Depends on: All Configuration pages (61–109)
This step is part of the larger process of maintaining stage isolation.
Code (this page)
# No new code on this page.
Explanation
Why this matters
Stage boundaries prevent scope creep and ensure each stage is self-contained. Configuration must be complete and working without any disk persistence.
This page will later include a stage boundary diagram.
⚠ If something breaks here
Common issues at this step:
- Persistence features accidentally included
- Configuration features missing
How to recover:
- Remove any premature Persistence code
- Review Configuration pages for missing features
What this page does
Brings in the modules required to read and write settings to disk.
Part: Persistence
Section: Module & Runtime Boundary
Depends on: Page 1 (imports section)
This step is part of the larger process of enabling durable configuration storage.
Code (this page)
import json
from pathlib import Path
Explanation
json is used to serialize/deserialize the settings file
Path makes the settings path clean and cross-platform
- These imports appear only in Persistence stage
- Standard library modules (no external dependencies)
This enables file-based persistence.
Why this matters
Without `json`, settings couldn't be stored in a human-readable format. Without `Path`, file paths would be OS-specific strings. These are the foundation of the persistence layer.
This page will later include a diagram of the persistence data flow.
⚠ If something breaks here
Common issues at this step:
- These imports appearing in earlier stages (stage violation)
- Typo in import statements
How to recover:
- Verify these imports only exist in Persistence stage
- Check spelling:
import json and from pathlib import Path
What this page does
Adds a copy method so Settings can create independent snapshots for dirty tracking.
Part: Persistence
Section: Domain State Enhancement
Depends on: Page 62 (Settings class)
This step is part of the larger process of enabling dirty tracking.
Code (this page)
def copy(self) -> "Settings":
return Settings(
default_duration_seconds=self.default_duration_seconds,
alarm_enabled=self.alarm_enabled
)
Explanation
- Creates a new Settings instance with same values
- Prevents aliasing (two variables pointing to same object)
- Enables reliable dirty tracking via snapshot comparison
- Returns type annotated as
"Settings" (forward reference)
This implements the Snapshot Dirty Tracking Pattern.
Why this matters
Without copy(), `saved_settings = self.settings` would create an alias, not a snapshot. Changes to `settings` would also change `saved_settings`, breaking dirty detection.
This page will later include a diagram showing copy vs alias behavior.
⚠ If something breaks here
Common issues at this step:
- Shallow copy issues (not applicable here, but worth noting)
- Missing field in copy
How to recover:
- Ensure all Settings fields are included in copy
- Verify copy creates new instance, not reference
What this page does
Defines a dedicated class to handle all file I/O operations.
Part: Persistence
Section: Persistence Boundary
Depends on: Page 111 (json and Path imported)
This step is part of the larger process of separating concerns.
Code (this page)
class PersistenceManager:
"""Handles reading/writing settings to disk."""
Explanation
- Separates file I/O from UI/controller logic
- Keeps responsibilities clean (StudyTimerApp is not file code)
- All disk operations go through this class
- Class methods (no instance needed)
This implements the Persistence Boundary Pattern.
Why this matters
Without a dedicated persistence class, file operations would be scattered throughout the application. This creates a clean boundary between "app logic" and "storage logic."
This page will later include an architecture diagram showing PersistenceManager.
⚠ If something breaks here
Common issues at this step:
- Putting file code directly in StudyTimerApp
- Missing docstring
How to recover:
- Keep all file I/O in PersistenceManager
- Add descriptive docstring
What this page does
Sets a single canonical path for the settings JSON file.
Part: Persistence
Section: Persistence Boundary → Constants
Depends on: Page 113 (PersistenceManager class)
This step is part of the larger process of establishing storage location.
Code (this page)
DATA_FILE = Path.home() / ".academic_study_timer_settings.json"
Explanation
- Stored in the user's home directory
- Hidden file by convention (leading dot on Unix)
- Descriptive filename identifies the application
- Class constant (shared across all operations)
This establishes the storage target.
Why this matters
Without a defined location, settings would be lost or inconsistent. The home directory ensures settings persist across working directories and are user-specific.
This page will later include examples of the path on different operating systems.
⚠ If something breaks here
Common issues at this step:
- Hardcoded path instead of
Path.home()
- Missing leading dot (file visible in directory listings)
How to recover:
- Use
Path.home() for cross-platform home directory
- Include
. prefix for hidden file
What this page does
Creates the load method with a return contract that never crashes.
Part: Persistence
Section: Load Pipeline
Depends on: Page 114 (DATA_FILE defined)
This step is part of the larger process of implementing safe file loading.
Code (this page)
@classmethod
def load(cls) -> tuple[Settings, Optional[str]]:
Explanation
@classmethod allows calling without an instance
- Returns a tuple: (Settings, error_message)
error_message is None when successful
- Always returns valid Settings (defaults on failure)
This implements the Load Contract Pattern.
Why this matters
Without this contract, load failures could crash the application. The tuple return allows both success (settings, None) and failure (defaults, error_message) to be handled gracefully.
This page will later include a flowchart of the load contract.
⚠ If something breaks here
Common issues at this step:
- Missing
@classmethod decorator
- Wrong return type annotation
How to recover:
- Always use
@classmethod for load/save
- Return
tuple[Settings, Optional[str]]
What this page does
Treats "no file yet" as a normal first-run condition.
Part: Persistence
Section: Load Pipeline
Depends on: Page 115 (load method started)
This step is part of the larger process of handling all load scenarios.
Code (this page)
if not cls.DATA_FILE.exists():
return Settings(), None
Explanation
- No file = first run, use defaults
- No warning or error needed
- Returns fresh Settings with default values
None error message indicates success
This handles the first-run scenario.
Why this matters
Without this check, first-time users would see an error. Missing file is expected on first run and should silently use defaults.
This page will later include a decision tree for load scenarios.
⚠ If something breaks here
Common issues at this step:
- Raising exception on missing file
- Showing error dialog for first run
How to recover:
- Check existence before reading
- Return defaults with no error message
What this page does
Opens the settings file and parses its JSON content.
Part: Persistence
Section: Load Pipeline
Depends on: Page 116 (file exists)
This step is part of the larger process of loading persisted settings.
Code (this page)
with open(cls.DATA_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
Explanation
with ensures file is properly closed
encoding="utf-8" ensures consistent text handling
json.load() parses file content into Python structures
data will be a dictionary if file is valid
This reads the persisted configuration.
Why this matters
Without explicit encoding, different systems might interpret the file differently. The `with` statement ensures resources are cleaned up even if parsing fails.
This page will later include an example of the JSON file format.
⚠ If something breaks here
Common issues at this step:
- Wrong encoding (non-UTF-8)
- Not using
with statement
How to recover:
- Always specify
encoding="utf-8"
- Always use context manager (
with)
What this page does
Ensures the JSON root element is a dictionary.
Part: Persistence
Section: Load Pipeline
Depends on: Page 117 (JSON parsed)
This step is part of the larger process of validating loaded data.
Code (this page)
if not isinstance(data, dict):
raise ValueError("Root element must be object")
Explanation
- Prevents unexpected JSON formats (array, string, number)
- Keeps downstream
.get() usage safe
- Raises ValueError for consistent error handling
- Defensive programming against file corruption
This implements the Corruption Recovery Pattern.
Why this matters
Without type validation, a JSON array or string would cause crashes when accessing keys. This ensures the data structure is what we expect.
This page will later include examples of valid vs invalid JSON roots.
⚠ If something breaks here
Common issues at this step:
- Not checking type before using dict methods
- Wrong exception type
How to recover:
- Always validate type before accessing
- Raise
ValueError for semantic errors
What this page does
Reads configuration keys safely, providing defaults for missing fields.
Part: Persistence
Section: Load Pipeline
Depends on: Page 118 (root validated)
This step is part of the larger process of forward-compatible loading.
Code (this page)
duration = data.get("default_duration_seconds", 25 * 60)
alarm = data.get("alarm_enabled", True)
Explanation
.get() returns default if key is missing
- Forward-compatible: old files without new keys still work
- Defaults align with Settings class defaults
- Duration is in seconds (internal unit)
This enables graceful schema evolution.
Why this matters
Without defaults, adding new settings would break old files. Using `.get()` with defaults ensures backward compatibility as the application evolves.
This page will later include examples of partial JSON files.
⚠ If something breaks here
Common issues at this step:
- Using
data["key"] instead of data.get()
- Wrong default values
How to recover:
- Always use
.get(key, default) pattern
- Defaults should match Settings class
What this page does
Rejects invalid duration values loaded from disk.
Part: Persistence
Section: Load Pipeline
Depends on: Page 119 (values extracted)
This step is part of the larger process of validating persisted data.
Code (this page)
if not isinstance(duration, int) or duration < 1:
raise ValueError("Invalid duration")
Explanation
- Requires integer type (not string or float)
- Prevents zero or negative durations
- Matches UI validation constraints
- Raises ValueError for consistent error handling
This prevents corrupted values from affecting runtime.
Why this matters
Without validation, a manually edited file with "duration": "abc" would crash the application. Type and range checks ensure only valid data is used.
This page will later include examples of invalid duration values.
⚠ If something breaks here
Common issues at this step:
- Not checking type (just range)
- Wrong minimum value
How to recover:
- Check both type AND range
- Minimum is 1 (matching UI validation)
What this page does
Rejects invalid alarm toggle values loaded from disk.
Part: Persistence
Section: Load Pipeline
Depends on: Page 120 (duration validated)
This step is part of the larger process of validating persisted data.
Code (this page)
if not isinstance(alarm, bool):
raise ValueError("Invalid alarm_enabled")
Explanation
- Requires actual boolean type
- Prevents truthy strings like "true" or "yes"
- Keeps state strictly typed
- Raises ValueError for consistent error handling
This ensures type safety for boolean settings.
Why this matters
Without type checking, JSON string "true" would pass but behave incorrectly. Python's `"true" != True` would cause subtle bugs.
This page will later include examples of valid vs invalid boolean values.
⚠ If something breaks here
Common issues at this step:
- Accepting truthy values instead of strict bool
- Missing type check
How to recover:
- Use
isinstance(alarm, bool) for strict checking
- Reject strings, numbers, and other types
What this page does
Constructs and returns Settings from validated disk values.
Part: Persistence
Section: Load Pipeline
Depends on: Page 121 (all values validated)
This step is part of the larger process of completing successful load.
Code (this page)
return Settings(default_duration_seconds=duration, alarm_enabled=alarm), None
Explanation
- Creates Settings with loaded values
None error message indicates success
- Tuple return matches the load contract
- Settings is fully initialized and valid
This completes the happy path.
Why this matters
This is the successful completion of load. The Settings object is ready for use, and the None error signals no problems occurred.
This page will later include a complete load flow diagram.
⚠ If something breaks here
Common issues at this step:
- Wrong tuple order (error, settings instead of settings, error)
- Forgetting None for success
How to recover:
- Return
(Settings(...), None) for success
- Error message is always second element
What this page does
Recovers gracefully from malformed JSON or invalid values.
Part: Persistence
Section: Load Pipeline
Depends on: Pages 117-121 (try block)
This step is part of the larger process of implementing recovery.
Code (this page)
except (json.JSONDecodeError, ValueError, TypeError, KeyError) as e:
return Settings(), f"Settings file was corrupt and has been reset. ({type(e).__name__})"
Explanation
- Catches all data corruption scenarios
- Falls back to defaults (never crashes)
- Returns warning message for user notification
- Includes exception type for debugging
This implements the Corruption Recovery Pattern.
Why this matters
Without recovery, a manually edited or corrupted file would crash the application. Users should see their settings reset with a warning, not an error dialog that blocks startup.
This page will later include examples of corrupted files and recovery.
⚠ If something breaks here
Common issues at this step:
- Not catching all error types
- Crashing instead of recovering
How to recover:
- Catch
JSONDecodeError, ValueError, TypeError, KeyError
- Always return defaults with error message
What this page does
Handles permission, filesystem, and I/O failures separately.
Part: Persistence
Section: Load Pipeline
Depends on: Page 123 (data errors handled)
This step is part of the larger process of comprehensive error handling.
Code (this page)
except OSError as e:
return Settings(), f"Could not read settings file: {e}"
Explanation
- Catches file system errors (permissions, disk issues)
- Still returns defaults (never crashes)
- Different message distinguishes from corruption
- Makes the failure visible to user
This handles system-level failures.
Why this matters
OS errors (permission denied, disk full) are different from data corruption. Separate handling allows appropriate user messaging and future logging.
This page will later include examples of OS error scenarios.
⚠ If something breaks here
Common issues at this step:
- Catching OSError in wrong except block
- Not distinguishing from data errors
How to recover:
- Keep OSError in separate except block
- Use different message text
What this page does
Creates the save method with a return contract for error reporting.
Part: Persistence
Section: Save Pipeline
Depends on: Page 113 (PersistenceManager class)
This step is part of the larger process of implementing settings persistence.
Code (this page)
@classmethod
def save(cls, settings: Settings) -> Optional[str]:
Explanation
@classmethod allows calling without an instance
- Takes Settings object to save
- Returns
None for success
- Returns error message string for failure
This implements the Save Contract Pattern.
Why this matters
The return contract matches load's error handling style. Callers can check for None (success) or display the error message (failure).
This page will later include a save flow diagram.
⚠ If something breaks here
Common issues at this step:
- Missing
@classmethod decorator
- Wrong return type
How to recover:
- Use
@classmethod for save
- Return
Optional[str] (None or error)
What this page does
Converts Settings into a JSON-friendly dictionary.
Part: Persistence
Section: Save Pipeline
Depends on: Page 125 (save method started)
This step is part of the larger process of preparing data for disk.
Code (this page)
data = {
"default_duration_seconds": settings.default_duration_seconds,
"alarm_enabled": settings.alarm_enabled
}
Explanation
- Explicit keys define the file schema
- Only persists configuration fields
- Avoids storing runtime or incidental fields
- Dictionary is directly JSON-serializable
This creates the storage format.
Why this matters
Without explicit conversion, dataclass fields might include unwanted data. The explicit dictionary ensures only intended fields are persisted.
This page will later include the JSON file schema.
⚠ If something breaks here
Common issues at this step:
- Including extra fields accidentally
- Wrong key names (must match load)
How to recover:
- List only configuration fields
- Key names must match exactly with load
What this page does
Writes the settings dictionary to the JSON file.
Part: Persistence
Section: Save Pipeline
Depends on: Page 126 (data dict created)
This step is part of the larger process of persisting configuration.
Code (this page)
with open(cls.DATA_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
return None
Explanation
"w" mode overwrites existing file (expected for settings)
encoding="utf-8" ensures consistent text handling
indent=2 makes file human-readable
- Returns
None to signal success
This completes the successful save.
Why this matters
Without indentation, the JSON would be a single line. Human-readable format helps with debugging and manual inspection.
This page will later include an example of the formatted JSON output.
⚠ If something breaks here
Common issues at this step:
- Wrong file mode (append instead of write)
- Forgetting return None
How to recover:
- Use
"w" for overwrite
- Always return None on success
What this page does
Returns an explicit failure message for write errors.
Part: Persistence
Section: Save Pipeline
Depends on: Page 127 (try block)
This step is part of the larger process of handling save failures.
Code (this page)
except OSError as e:
return f"Save failed: {e}"
Explanation
- Catches file system errors (permissions, disk full)
- Prevents crash on save failure
- Returns error message for UI display
- Allows caller to show Save Error dialog
This handles write failures gracefully.
Why this matters
Without error handling, a full disk or permission issue would crash the application. The error message allows the UI to inform the user appropriately.
This page will later include examples of save error scenarios.
⚠ If something breaks here
Common issues at this step:
- Not catching OSError
- Returning None on failure
How to recover:
- Wrap save in try/except OSError
- Return error string, not None
What this page does
Changes application startup to load settings from disk instead of using defaults.
Part: Persistence
Section: Startup Integration
Depends on: Page 115 (load method exists)
This step is part of the larger process of integrating persistence into the application.
Code (this page)
loaded_settings, load_error = PersistenceManager.load()
self.settings = loaded_settings
self.saved_settings = loaded_settings.copy()
Explanation
PersistenceManager.load() retrieves or creates settings
self.settings becomes the active configuration
self.saved_settings snapshots the "Saved" state for dirty tracking
- Copy prevents aliasing between the two
This is the defining Persistence initialization change.
Why this matters
Without loading from disk, settings would reset on every restart. The `saved_settings` snapshot enables detecting when user has made unsaved changes.
This page will later include a startup flow diagram.
⚠ If something breaks here
Common issues at this step:
- Forgetting to copy for saved_settings
- Not unpacking load return tuple
How to recover:
- Use
.copy() to create independent snapshot
- Unpack both values from load return
What this page does
Starts the timer at the persisted default duration.
Part: Persistence
Section: Startup Integration
Depends on: Page 129 (settings loaded)
This step is part of the larger process of applying persisted configuration.
Code (this page)
self.timer_state = TimerState(self.settings.default_duration_seconds)
Explanation
- Uses loaded settings for initial duration
- Preserves behavior across restarts
- Same line as Configuration, but now uses loaded value
- TimerState itself is still not persisted
This connects persistence to runtime behavior.
Why this matters
Without this connection, loading settings wouldn't affect the timer display. The loaded duration becomes the actual countdown value.
This page will later include a diagram showing settings → TimerState flow.
⚠ If something breaks here
Common issues at this step:
- Using hardcoded duration instead of settings
- Initializing TimerState before settings load
How to recover:
- Use
self.settings.default_duration_seconds
- Ensure load happens before TimerState creation
What this page does
Displays load errors safely after the window is created.
Part: Persistence
Section: Startup Integration
Depends on: Page 129 (load_error captured)
This step is part of the larger process of user-friendly error reporting.
Code (this page)
if load_error:
self.root.after(100, lambda: messagebox.showwarning("Settings", load_error))
Explanation
- Only shows warning if there was a load error
after(100, ...) delays until window exists
- Uses
showwarning (not error) for non-fatal issues
- Lambda captures the error message
This implements the Deferred Dialog Pattern.
Why this matters
Without deferral, showing a dialog before the window exists would crash or behave unexpectedly. The short delay ensures the UI is ready.
This page will later include a timing diagram of startup.
⚠ If something breaks here
Common issues at this step:
- Showing dialog immediately (before window)
- Using wrong dialog type
How to recover:
- Use
root.after() to defer dialog
- Use
showwarning for non-fatal issues
What this page does
Marks settings as "unsaved" immediately when duration is edited.
Part: Persistence
Section: Dirty Tracking
Depends on: Page 68 (duration_var exists)
This step is part of the larger process of real-time dirty detection.
Code (this page)
self.duration_var.trace_add("write", self._on_setting_changed)
Explanation
trace_add fires on every keystroke in the Entry
- "write" mode triggers when value changes
- Calls
_on_setting_changed to update status
- Provides immediate feedback to user
This implements real-time dirty tracking.
Why this matters
Without trace, users wouldn't see "Unsaved" until clicking elsewhere. Immediate feedback helps users know they have pending changes.
This page will later include a demo of typing and seeing status change.
⚠ If something breaks here
Common issues at this step:
- Using wrong trace mode
- Missing handler method
How to recover:
- Use "write" for value changes
- Ensure
_on_setting_changed exists
What this page does
Updates "Saved/Unsaved" whenever the alarm checkbox is toggled.
Part: Persistence
Section: Dirty Tracking
Depends on: Page 71 (alarm checkbox exists)
This step is part of the larger process of tracking all settings changes.
Code (this page)
command=self._on_setting_changed
Explanation
- Checkbutton uses
command for click events
trace_add doesn't work well with Checkbutton
- Both inputs now trigger status update
- Same handler used for both
This completes input change tracking.
Why this matters
Without command binding, checkbox changes wouldn't trigger dirty detection. Both inputs must update the status indicator.
This page will later include a demo of checkbox toggle updating status.
⚠ If something breaks here
Common issues at this step:
- Forgetting command parameter
- Using trace_add for Checkbutton
How to recover:
- Add
command=self._on_setting_changed to Checkbutton
- Use command, not trace, for checkbox
What this page does
Introduces the bottom row that contains Save button and status indicator.
Part: Persistence
Section: UI Changes
Depends on: Pages 72-73 (replaces Apply frame)
This step is part of the larger process of building the persistence UI.
Code (this page)
persist_frame = tk.Frame(main_frame)
persist_frame.pack(fill=tk.X)
Explanation
- Replaces the Configuration "Apply" frame
- Contains Save button and status label
fill=tk.X allows horizontal expansion
- Frame provides layout structure
This creates the persistence controls area.
Why this matters
Persistence replaces Apply with Save. The frame structure remains similar, but the purpose changes from "apply to memory" to "save to disk."
This page will later include a comparison of Configuration vs Persistence UI.
⚠ If something breaks here
Common issues at this step:
- Keeping both Apply and Save (stage pollution)
- Wrong parent frame
How to recover:
- Replace Apply frame with persist_frame
- Parent should be main_frame
What this page does
Adds the Save button that writes settings to disk.
Part: Persistence
Section: UI Changes
Depends on: Page 134 (persist_frame exists)
This step is part of the larger process of building the save interface.
Code (this page)
self.save_button = tk.Button(persist_frame, text="Save", width=10, command=self._on_save)
self.save_button.pack(side=tk.LEFT)
Explanation
- "Save" replaces "Apply Settings" from Configuration
command=self._on_save connects to save handler
- Width matches other buttons for consistency
- Left-packed to allow status label beside it
This is the disk commit action.
Why this matters
Save is the primary user action for persistence. Unlike Apply (in-memory), Save writes to disk for permanent storage.
This page will later include a screenshot of the Save button.
⚠ If something breaks here
Common issues at this step:
- Using wrong handler name
- Keeping Apply button alongside Save
How to recover:
- Use
command=self._on_save
- Remove Apply button when adding Save
What this page does
Shows "Saved" or "Unsaved" status next to the Save button.
Part: Persistence
Section: UI Changes
Depends on: Page 135 (Save button exists)
This step is part of the larger process of providing save feedback.
Code (this page)
self.save_status_label = tk.Label(persist_frame, text="Saved", font=("TkDefaultFont", 10))
self.save_status_label.pack(side=tk.LEFT, padx=(15, 0))
Explanation
- Starts as "Saved" because settings were just loaded
- Changes to "Unsaved" when user edits settings
- Left padding separates from Save button
- Instance variable for dynamic updates
This provides save state feedback.
Why this matters
Without status indicator, users wouldn't know if they have unsaved changes. This visual feedback prevents accidental data loss.
This page will later include screenshots of both states.
⚠ If something breaks here
Common issues at this step:
- Wrong initial text (should be "Saved")
- Missing instance variable prefix
How to recover:
- Initial text is "Saved" (settings just loaded)
- Use
self.save_status_label for later updates
What this page does
Provides a single entry point for all dirty tracking updates.
Part: Persistence
Section: Dirty Tracking
Depends on: Pages 132-133 (trace and command callbacks)
This step is part of the larger process of implementing change detection.
Code (this page)
def _on_setting_changed(self, *args):
self._update_save_status()
Explanation
*args accepts trace callback arguments (ignored)
- Delegates to
_update_save_status() for actual logic
- Single handler for both duration and alarm changes
- Keeps change detection centralized
This is the change detection coordinator.
Why this matters
Both trace callbacks and command callbacks need to trigger the same update. A single handler avoids code duplication.
This page will later include a callback flow diagram.
⚠ If something breaks here
Common issues at this step:
- Missing
args (trace passes extra arguments)
- Not delegating to status update
How to recover:
- Use
args to accept any arguments
- Call
self._update_save_status() inside
What this page does
Determines whether current UI values differ from the last saved snapshot.
Part: Persistence
Section: Dirty Tracking
Depends on: Page 129 (saved_settings exists)
This step is part of the larger process of detecting unsaved changes.
Code (this page)
def _is_dirty(self) -> bool:
duration_seconds = self._parse_duration_input()
if duration_seconds is None:
return True
current_alarm = self.alarm_var.get()
return (
duration_seconds != self.saved_settings.default_duration_seconds or
current_alarm != self.saved_settings.alarm_enabled
)
Explanation
- Parses current duration from UI
- Invalid duration counts as "dirty" (needs attention)
- Compares against
saved_settings snapshot (not settings)
- Returns True if any value differs
This implements the Snapshot Dirty Tracking Pattern.
Why this matters
Comparing against `saved_settings` (not `settings`) is crucial. This detects differences from what's on disk, not what was last applied.
This page will later include a comparison diagram.
⚠ If something breaks here
Common issues at this step:
- Comparing against
settings instead of saved_settings
- Not handling invalid duration as dirty
How to recover:
- Always compare against
saved_settings
- Return
True when parsing fails
What this page does
Updates the UI label based on dirty state.
Part: Persistence
Section: Dirty Tracking
Depends on: Page 138 (_is_dirty exists)
This step is part of the larger process of providing visual feedback.
Code (this page)
def _update_save_status(self):
if self._is_dirty():
self.save_status_label.config(text="Unsaved")
else:
self.save_status_label.config(text="Saved")
Explanation
- Calls
_is_dirty() to determine state
- Updates label text accordingly
- Called from change handler and after save
- Centralizes status rendering
This connects dirty detection to UI.
Why this matters
Without this method, dirty detection would have no visible effect. This translates the boolean state into user-visible feedback.
This page will later include screenshots of both status states.
⚠ If something breaks here
Common issues at this step:
- Not calling after save completes
- Wrong label text
How to recover:
- Call
_update_save_status() after successful save
- Use exact strings "Saved" and "Unsaved"
What this page does
Validates inputs, writes to disk, updates snapshot, and optionally syncs timer.
Part: Persistence
Section: Save Workflow
Depends on: Pages 125-128 (PersistenceManager.save), Page 85 (smart sync)
This step is part of the larger process of completing the save action.
Code (this page)
def _on_save(self):
duration_seconds = self._parse_duration_input()
if duration_seconds is None:
messagebox.showerror("Invalid Input", "Duration must be a number between 1 and 999 minutes.")
return
old_default = self.settings.default_duration_seconds
self.settings.default_duration_seconds = duration_seconds
self.settings.alarm_enabled = self.alarm_var.get()
error = PersistenceManager.save(self.settings)
if error:
messagebox.showerror("Save Error", error)
return
self.saved_settings = self.settings.copy()
self._update_save_status()
if (not self.timer_state.is_running and
not self.timer_state.alarm_triggered and
self.timer_state.remaining_seconds == old_default):
self.timer_state.remaining_seconds = self.settings.default_duration_seconds
Explanation
- Validates duration before saving (same as Apply)
- Captures old_default for smart sync
- Updates settings object with new values
- Calls
PersistenceManager.save() to write to disk
- Shows error dialog if save fails
- Updates
saved_settings snapshot on success
- Updates status label to "Saved"
- Smart sync updates timer only if in Ready state
This is the complete save workflow.
Why this matters
Save is the culmination of Persistence. It combines validation, disk write, snapshot update, and smart sync into a single, correct operation.
This page will later include a complete save flow diagram.
⚠ If something breaks here
Common issues at this step:
- Not updating saved_settings after save
- Forgetting to update status label
- Smart sync using wrong comparison
How to recover:
- Always
saved_settings = settings.copy() after successful save
- Always call
_update_save_status() after save
- Smart sync compares against
old_default
What this page does
Locks in what Persistence adds and confirms stage boundaries.
Part: Persistence
Section: Stage Boundary Verification
Depends on: All Persistence pages (111–140)
This step is part of the larger process of verifying milestone completion.
Code (this page)
# No new code on this page.
Explanation
Why this matters
Without explicit verification, features could leak across stages. This checklist confirms Persistence is complete and properly scoped.
What this page does
Defines the "Full Code Visual" policy and marks project completion.
Part: Build It Production Policy
Section: Documentation Standards
Depends on: All pages (1–141)
This step is part of the larger process of finalizing the instructional material.
Code (this page)
# No new code on this page.
Explanation
Why this matters
This page marks the completion of the Build It instructional system for the Academic Study Timer. The project is ready for visual enhancement and final production.