DEV Community

Cover image for SlothMode: Building a Deliberately "Lazy" Focus Enforcer with Open-Source AI
Johnny Santamaria
Johnny Santamaria

Posted on

SlothMode: Building a Deliberately "Lazy" Focus Enforcer with Open-Source AI

Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady Submission πŸ¦₯

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady

A note on process: I designed the architecture first β€” diagrams, pattern assignments, class relationships β€” and then used AI to accelerate implementation against that blueprint. The design pattern framework for this post comes from Design Patterns: Elements of Reusable Object-Oriented Software (Gamma, Helm, Johnson, Vlissides), referenced at the end.


Section 1: About the Project

What is SlothMode?

I was sleepy and wanted to be congratulated for it, we all do don't we? But an apple fell on my head.

SlothMode is a desktop productivity application that enforces deliberate rest. Instead of nagging you to work harder, it schedules mandatory breaks pulled from your calendar, then locks your screen with a fullscreen countdown when it's time to step away. Points earned for taking breaks unlock cosmetic rewards (a sloth hat, a hammock, ambient soundscapes). It's a focus tool built around the idea that rest is part of the work.

The project lives at github.com/johnny603/SlothMode.

Why is it important?

Most productivity software optimizes for output. SlothMode inverts that: it treats recovery as a first-class metric. The "SlothMode" concept β€” everything is farther away, stairs are steeper, the bus leaves earlier β€” is a gentle joke about aging and exhaustion, but it's also the design philosophy. The app makes slowing down rewarding rather than punishing.

Who is the typical user?

A knowledge worker or student who schedules focus blocks in a calendar (Google Calendar, CalDAV, or a local ICS file) and wants their break time protected. Example scenario: You block 2:00–2:15 PM as "Break" in your calendar. At 2:00, SlothMode reads that event through its hybrid calendar source, transitions to LockoutState, and displays a fullscreen countdown. You can't dismiss it until the timer runs out β€” but when you do, you earn points toward a cosmetic reward.

Screenshot

(Insert annotated screenshot of the running app here β€” I'll describe it for the template: an arrow pointing to the fullscreen CountdownRing overlay, a second arrow to the SlothHammock reward badge in the corner, and a third to the system tray icon showing "Focus" state.)

Users

The project is an early-stage open-source repo (205 files, 15 test suites passing as of the last build), so user count is effectively the contributor base. It's a framework/toolkit more than a deployed product β€” designed to be extended via its SlothPlugin interface.


Section 2: The Issue

What issue did I address?

(Link to your GitHub issue here.) The issue: SlothMode only supported a single hard-coded break source, making it unusable for users who track breaks differently (some use CalDAV, some use local ICS files, some manually add breaks). The app needed a way to compose multiple break sources without the lockout engine knowing which one it was talking to.

Why is this important?

The lockout scheduler is the heart of the app. If adding a new break source requires modifying SessionHub, LockoutState, and BreakRepository every time, the project becomes closed to contribution. Decoupling break sources from the scheduling engine is the difference between a demo and a tool.

Key code locations

File / Folder Purpose
src/main/java/io/slothmode/calendar/ All break-source implementations: CalDAVSource, CalendarSource, HybridSource, ManualSource, BreakSourceFactory
calendar/BreakRepository.java Aggregates breaks from whatever source is configured
calendar/AddBreakCommand.java Command-pattern wrapper for adding a break (undoable)
session/SessionHub.java Orchestrates state transitions (IdleState β†’ FocusState β†’ LockoutState β†’ RewardState)
session/SessionObserver.java, SessionSubject.java Observer wiring so the UI reacts to state changes
lockout/LockoutAbstraction.java, LockoutImplementor.java Bridge pattern separating "how to lock" from "what triggers a lock"

Supporting artifacts

(Insert screenshot of a NullPointerException or empty-break-list log from before the fix β€” e.g., HybridSource returning an empty list when one source is down.)


Section 3: Codebase Overview

Tech stack

Technology Purpose
Java 17 + Maven (pom.xml, mvnw) Core language and build system; the app is a desktop Swing/JavaFX client
JavaFX (ui/MainWindow.java, resources/ui/main.css) UI layer; the CountdownRing and fullscreen lockout render here
Logback (resources/logback.xml) Structured logging for session transitions and lockout events
JUnit + Surefire 15 test classes covering calendar, command, config, lockout, model, plugin, and session packages
Ollama / llama.cpp (model/OllamaModelFactory.java, LlamaCppModelFactory.java, MockModel.java) Local LLM inference for the SessionSummaryAgent β€” summarizes what you did during a focus session, entirely offline
CalDAV / ICS Break ingestion from external calendars

System diagram

[Calendar: CalDAV | ICS | Manual]
        β”‚
        β–Ό
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚ BreakSource     │◄─────│ BreakSourceFactoryβ”‚
 β”‚ (HybridSource)  β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β–Ό
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚ BreakRepository │──► AddBreakCommand (undoable)
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β–Ό
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   Observer    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚   SessionHub    │──────────────►│   MainWindow  β”‚
 │ Idle→Focus→     │               │  (Countdown)  │
 β”‚ Lockoutβ†’Reward  β”‚               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β–Ό
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚LockoutAbstraction│────►│LockoutImplementorβ”‚
 β”‚ (Bridge)         β”‚     β”‚Fullscreen/Nudge  β”‚
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β–Ό
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚  RewardState    │─────►│ RewardDecorator  β”‚
 β”‚                 β”‚      β”‚ SlothHat/Hammock β”‚
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

(Numbered components 1–7 correspond to the workflow below.)

Workflow: the 2:00 PM break

  1. Calendar ingestion β€” HybridSource (the BreakSource implementation configured via BreakSourceFactory) queries CalDAV and the local ICS file, merges results.
  2. Repository β€” BreakRepository receives the merged Break objects.
  3. SessionHub observes the clock; at 2:00 it calls SessionState.transition(LockoutState).
  4. Observer notification β€” SessionSubject.notifyObservers() fires; MainWindow renders the CountdownRing overlay.
  5. Lockout β€” LockoutAbstraction delegates to its LockoutImplementor (FullscreenLockout for aggressive mode, GentleNudgeLockout for a softer prompt).
  6. Reward β€” When the timer completes, RewardState awards points via FixedPointsStrategy / StreakPointsStrategy and decorates the reward bundle with SlothHat.
  7. Summary β€” SessionSummaryAgent (backed by an Ollama model) generates a one-line summary of the session, all local.

Section 4: Challenges

The challenge

Designing a break-source abstraction that composes cleanly. The naΓ―ve approach β€” if (source == CALDAV) ... else if (source == ICS) ... β€” would have leaked into SessionHub and made the lockout logic untestable. I needed HybridSource to be able to wrap any two BreakSource implementations and behave identically to a single source.

Three self-directed attempts

  1. Conditional compilation β€” I tried a single BreakSource class with a SourceType enum switch. It passed tests but violated the project's existing pattern vocabulary and made BreakRepositoryTest brittle.
  2. Subclass explosion β€” I tried CalDAVOnlySource, IcsOnlySource, HybridCalDAVIcsSource. This worked but produced 6+ classes and doubled the test surface.
  3. Adapter + composition β€” I mapped the problem onto the Adapter pattern (from the GoF book): HybridSource implements BreakSource, holds two references typed as BreakSource, and delegates. Existing sources needed no changes.

Who I reached out to

I consulted the project's own docs/dev-setup.md and the lockout-manual-test.md to understand the expected runtime behavior, then used AI (feeding it the architecture diagram and the relevant GoF pattern definitions) to validate the Adapter mapping against the existing BreakSourceFactory contract.

Status

Resolved. The pattern is in place and HybridSourceTest passes alongside CalDAVSourceTest and CalendarSourceTest.


Section 5: Solution

What I did

I implemented HybridSource as a composition of two BreakSource instances, both typed against the abstract interface. It polls both sources, merges their Break lists, and returns a single de-duplicated list. Because HybridSource is a BreakSource, it can be nested arbitrarily (new HybridSource(new HybridSource(a, b), c)) and BreakRepository never needs to know.

Key technologies referenced

  • Java interface polymorphism β€” BreakSource (from the calendar package in the tech stack table) defines the contract; HybridSource, CalDAVSource, ManualSource all implement it.
  • System diagram component 1 β€” the BreakSource / BreakSourceFactory pair; my solution sits exactly there, as a third BreakSource implementation.

How I verified it

  • HybridSourceTest (in src/test/java/io/slothmode/calendar/) β€” asserts that two mock sources produce a merged list, that an empty source doesn't break the merge, and that a failing source degrades gracefully.
  • Full suite: ./mvnw test β†’ 15 test classes, all green (see target/surefire-reports/).
  • Manual: ran the app with a CalDAV source and a local ICS file simultaneously; the 2:00 PM break fired once, not twice.

(Insert screenshot of the Surefire test summary here.)

Code snippet

// HybridSource.java β€” Adapter pattern: composes two BreakSources
public class HybridSource implements BreakSource {
    private final BreakSource primary;
    private final BreakSource secondary;

    public HybridSource(BreakSource primary, BreakSource secondary) {
        this.primary = primary;
        this.secondary = secondary;
    }

    @Override
    public List<Break> fetchBreaks() {
        List<Break> merged = new ArrayList<>(primary.fetchBreaks());
        for (Break b : secondary.fetchBreaks()) {
            if (!merged.contains(b)) merged.add(b); // de-dupe
        }
        return merged;
    }
}
Enter fullscreen mode Exit fullscreen mode

Line by line: the constructor accepts any two BreakSource implementations (polymorphism β€” this is what makes the factory viable). fetchBreaks() delegates to primary, then appends de-duplicated secondary breaks. No instanceof, no enum, no subclass explosion.

Pull request

(Link to your PR here.)


How I Built It

I used open-source AI as an implementation accelerator, not as an architect:

  • Design first. I produced the system diagram above and explicitly mapped each package to a GoF pattern before writing code. I referenced the Design Patterns textbook for the Adapter, Bridge, Command, Memento, Observer, State, Strategy, and Visitor mappings already present in the codebase (LockoutAbstraction = Bridge, AddBreakCommand = Command, SaveWorkMemento = Memento, SessionObserver = Observer, SessionState = State, BreakStrategy = Strategy, EventVisitor = Visitor).
  • AI as implementer. I fed the architecture diagram and pattern definitions to an AI assistant, asking for implementations that respected the existing interfaces. The pre-supplied structure prevented the model from inventing new abstractions or confusing the Bridge with the Adapter.
  • Open-weight models at runtime. SlothMode's own SessionSummaryAgent runs against Ollama or llama.cpp via OllamaModelFactory / LlamaCppModelFactory, with MockModel for tests. Session summaries never leave the machine.

Why Does Open Innovation Matter?

SlothMode's core value proposition β€” rest without surveillance β€” is incompatible with a closed API. If the session summarizer phoned home to a hosted LLM, the app would be logging exactly when you're not working. Local inference via open-weight models is a product requirement, not a nice-to-have. The SlothModel interface lets users swap Ollama, llama.cpp, or a mock without touching the session logic β€” the same open-interface principle as BreakSource. Open source made it possible to build a rest tool that doesn't itself need to watch you.

My Agent Session

(Embed your DevRelay agent_session here, or link it.)

The Lazy Payoff

The entire point of SlothMode is measured laziness: the CountdownRing enforces breaks you'd otherwise skip, StreakPointsStrategy rewards consistency, and the SlothHammock decorator makes rest tangible. In testing, the hybrid calendar source removed the "I forgot to log my break" failure mode entirely β€” breaks arrive from the calendar you already maintain. (Add a personal anecdote here if you used it this week.)

Prize Categories

(List partner categories you're entering, or remove this section.)


References

  • Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. β€” Source for the Adapter, Bridge, Command, Memento, Observer, State, Strategy, and Visitor patterns used throughout SlothMode's architecture.
  • SlothMode repository: github.com/johnny603/SlothMode
  • Hacktoberfest Open-Source AI Challenge Week 3

Top comments (0)