Complete Guide

Study Timer

📄 142 Pages 🎯 Stage: complete 📦 Output: complete.py

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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
# 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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
# 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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
# 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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
# 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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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").

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
self.root.bell()

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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
# 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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
# 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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
# 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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
# 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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
root = tk.Tk()

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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
root.mainloop()

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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
# 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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
@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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
# 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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
# 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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
return minutes * 60

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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
# 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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
# 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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
@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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
@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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

⚠ 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)

python
# 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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

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)

python
# 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.

✓ Checkpoint

After this page, you should be able to:

If this is not true, stop and review before continuing.

Contents