SimpleTimeTracking (STT) is a cross-platform desktop application for fast, unobtrusive time tracking. It prioritizes the individual user's workflow over management reporting — design goal is to let you start/stop tracking with minimal friction and copy results into other systems.
- Start/stop working on a task with a single command or button click
- Resume the last or any previous activity
- Search across historical activities by comment text
- Report on time spent per activity/group (day, period, search filter)
- Overtime calculation against configured worktime rules
- CSV/Jira export for integration with other systems
- Dual interface: JavaFX GUI for desktop use, CLI for scripting/terminal use
- Automatic backup and item logging
| Layer | Technology |
|---|---|
| Language | Kotlin (JVM), with some Java source files |
| UI | JavaFX 21, ControlsFX, RichTextFX |
| Build | Gradle (Kotlin DSL), JDK 21+ |
| DI | Dagger 2 (annotation-based, kapt) |
| Parsing | ANTLR 4 for command text parsing |
| Config | YAML (SnakeYAML) |
| Event Bus | MBassador (in-process pub/sub) |
| Testing | JUnit 4, AssertJ, Mockito (+ mockito-kotlin), TestFX |
All source under src/main/kotlin/org/stt/:
org.stt/
├── cli/ # CLI entry points (Main, ReportPrinter, FormatConverter)
├── command/ # Command pattern: Commands.kt, CommandHandler.kt, CommandFormatter (ANTLR)
├── config/ # Configuration loading from YAML, config classes
├── connector/jira/ # Jira integration (REST client)
├── csv/ # CSV import/export
├── event/ # Event bus classes (NotifyUser, ShuttingDown, TimePassedEvent)
├── gui/ # JavaFX UI (MainWindow, ActivitiesController, ReportController, Settings)
├── model/ # Domain model (TimeTrackingItem, ReportingItem)
├── persistence/ # ItemReader / ItemWriter interfaces + STT file format implementation
├── query/ # Query/filter logic over time tracking items (Criteria, WorkTimeQueries)
├── reporting/ # Report generation (SummingReportGenerator, OvertimeReportGenerator)
├── text/ # Text completion, categorization, grouping (CommonPrefixGrouper, JiraExpansion)
├── time/ # Date/time utilities, duration rounding
├── update/ # Update check mechanism
├── validation/ # Input validation
See doc/ui-architecture.md for a detailed breakdown of UI components, controllers, data objects, and event bus wiring.
- Command pattern:
Command(sealed hierarchy) +CommandHandlerinterface (Visitor), parsed via ANTLR grammar - Dependency Injection: Dagger
@Module/@Provides/@Inject; components areDagger*Application(e.g.,DaggerCLIApplication,DaggerUIApplication) - Event-driven:
MBassadorevent bus for decoupled UI updates (time ticks, shutdown, notifications) - Repository:
ItemReader/ItemWriterinterfaces abstract storage;STTItemReader/STTItemWriterimplement the plain-text file format - Service lifecycle:
Serviceinterface withstart()/stop()for config, backup, and logging services - Data stored: A plain-text file (
.stt/activities) with one record per line
- Language: Kotlin (prefer immutable data classes,
val, extension functions) - Naming:
camelCasefor methods/variables,PascalCasefor classes, no underscores - Imports: explicit single imports (no wildcard
.*except for standard lib / assertions) - Nullability: explicit nullable types with
?, prefer?:elvis operator - DI: constructor injection via
@Inject, module-provided bindings for platform/third-party types - Logging:
java.util.logging.Logger(Logger.getLogger(...)) - Documentation: every class must have a KDoc comment (
/** ... */) describing its purpose and behavior; test classes additionally summarize the concerns and behaviors they verify (seeJsonSubmitConnectorTestfor the pattern) - File format: one time-tracking record per line, human-readable text
| Tool | Dependency | Purpose |
|---|---|---|
| JUnit 4 | junit:junit-dep:4.11 |
Runner (@Test, @Before, @Theory, @RunWith(Theories::class), @DataPoints, @TestedOn, @Rule) |
| AssertJ | assertj-core:3.26.3 |
Fluent assertions (assertThat(...)) |
| Mockito | mockito-core:5.12.0 |
Mocking (@Mock, MockitoAnnotations.initMocks(), given()/verify()) |
| Mockito Kotlin | mockito-kotlin:5.4.0 |
Kotlin-friendly matchers (any(), anyOrNull()) |
| TestFX | via monocle | Headless JavaFX testing |
| Commons IO | commons-io:2.8.0 |
Temp file I/O in tests (TemporaryFolder) |
Mockito inline mock maker is enabled via
src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker(mock-maker-inline) for mocking final classes.
Tests mirror src/main/kotlin/org/stt/ one-to-one under src/test/kotlin/org/stt/:
src/test/kotlin/org/stt/
├── cli/ # MainTest, ReportPrinterTest
├── command/ # ActivitiesTest, CommandFormatterTest
├── config/ # PasswordSettingTest
├── connector/jira/ # JiraClientTest
├── csv/importer/ # CsvImporterTest
├── gui/jfx/ # ActivitiesControllerTest, TimeTrackingItemCellTest
│ └── binding/ # ReportBindingTest
├── importer/ # STTItemReaderTest, STTItemWriterTest, TiImporterTest
├── model/ # TimeTrackingItemTest
├── persistence/ # BackupCreatorTest, IOUtilTest
│ └── stt/ # STTItemConverterTest
├── query/ # TimeTrackingItemQueriesTest
├── reporting/ # SummingReportGeneratorTest, OvertimeReportGeneratorTest, WorkingtimeItemProviderTest
├── text/ # CommonPrefixGrouperTest, JiraExpansionProviderTest
├── time/ # DateTimesTest, DurationRounderTest, IntervalTest
├── update/ # VersionComparatorTest
├── validation/ # ItemAndDateValidatorTest
├── IntRangeTest.kt # (root-level utility)
├── StatesTest.kt # (root-level utility)
├── StringsTest.kt # (root-level utility)
├── Tests.kt # Mockito matcher helpers (Matchers.argThat, Matchers.any)
└── ItemReaderTestHelper.kt # Stubbing helper (givenReaderReturns)
should[Expectation] → e.g., shouldReturnTrueForSameDate, shouldCreateItemWithoutEnd
Every test follows GIVEN / WHEN / THEN comments. The System Under Test is always named sut:
@Test
fun shouldDoSomething() {
// GIVEN
...
// WHEN
val result = sut.method()
// THEN
assertThat(result).isEqualTo(...)
}@Mock private lateinit var dependency: SomeClass
private lateinit var sut: ClassUnderTest
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
sut = ClassUnderTest(dependency)
}Pure AssertJ chaining — no JUnit assertEquals/assertTrue:
assertThat(result).isEqualTo(expected)
assertThat(result).isTrue()
assertThat(list).hasSize(3).containsExactly(a, b, c)Use JUnit 4 Theories with @DataPoints or @TestedOn for data-driven tests (see CommandFormatterTest, TimeTrackingItemQueriesTest, STTItemWriterTest):
@RunWith(Theories::class)
class SomeTheoryTest {
@Theory
fun shouldHandleCase(@TestedOn(ints = [0, 1, 42]) input: Int) { ... }
}| Area | Coverage | Details |
|---|---|---|
model/ |
Full | TimeTrackingItem (22 tests), ReportingItem tested via reporting |
query/ |
Full | TimeTrackingItemQueries (20+ tests with theories) |
command/ |
Full | Activities (8 tests), CommandFormatter (theory-based) |
reporting/ |
Full | Summing, Overtime, WorkingtimeItemProvider all covered |
time/ |
Good | DurationRounder (5 tests), DateTimes (18 tests), Interval (8 tests) |
text/ |
Partial | CommonPrefixGrouper, JiraExpansionProvider tested; ItemCategorizer via reporting |
persistence/ |
Partial | BackupCreator, STTItemConverter, STTItemReader/Writer tested; ItemPersister untested |
cli/ |
Partial | Main, ReportPrinter tested; CLIApplication, FormatConverter untested |
config/ |
Minimal | Only PasswordSetting tested (2 tests); YamlConfigService etc. untested |
gui/jfx/ |
Minimal | ActivitiesController, TimeTrackingItemCell, ReportBinding tested; most controllers untested |
event/ |
None | ItemLogService untested |
submit/ |
None | Entire submit package untested |
root/ |
Added | Strings, States, IntRange tested; StopWatch, Streams, Service untested |
Tests.kt(org.stt.Matchers) —argThat(lambda)andany<T>()wrappers for Mockito-Kotlin compatibilityItemReaderTestHelper.kt—givenReaderReturns(reader, item1, item2, ...)chains stubs to return items thennullIOUtil.kt(org.stt.importer) —readAll(reader)collects all items from anItemReaderTestFX.kt(org.stt.gui.jfx) —installTK()mocks JavaFXToolkitfor headless UI testing
Every test must run and pass on macOS, Linux, and Windows. Avoid:
- Hard-coding path separators (
/or\) — useFile.separatororFile(path).isAbsolute - Assuming a specific filesystem root (e.g.
/tmporC:\) - Platform-specific shell commands or process execution
- Relying on Linux/macOS-only tools or paths
./gradlew build # compile + test + assemble fat jar
./gradlew test # run all tests (JUnit 4)
./gradlew check # + static analysis (SonarQube if configured)
./gradlew dist # jlink + jpackage for native distribution
./gradlew run # compile and start the GUI applicationOutput fat jar: build/libs/STT-<version>.jar
Use Conventional Commits:
feat: add resume-last-activity CLI command
fix: report crashes on empty activity list
refactor: extract DurationRounder from ReportGenerator
test: add overtime calculation edge cases
docs: update README with new CLI usage
chore: bump Dagger to 2.50
Scopes (optional): cli, gui, persistence, query, reporting, config, time, deps
All new features and significant changes must start with OpenSpec — a spec-driven planning workflow that produces design artifacts before any code is written.
Explore ──→ Propose ──→ Apply ──→ Archive
│ │ │ │
│ v │ v
└─── think design code sync specs
(optional) + spec tasks + archive
| Skill | Command | When to Use |
|---|---|---|
openspec-explore |
/opsx-explore |
Thinking through an idea, investigating codebase, clarifying requirements before committing to a change |
openspec-propose |
/opsx-propose |
Ready to formalize — creates proposal.md, specs/, design.md, tasks.md |
openspec-apply-change |
/opsx-apply |
Implement tasks from a change, one by one |
openspec-update-change |
/opsx-update |
Revise planning artifacts mid-change without editing code |
openspec-sync-specs |
/opsx-sync |
Merge delta specs from a change into main specs |
openspec-archive-change |
/opsx-archive |
Finalize a completed change and archive it |
- Explore — Use
/opsx-exploreto investigate the codebase, clarify the problem, and explore options with the AI as a thinking partner. - Propose — Use
/opsx-propose <change-name>to generate planning artifacts: a proposal, capability specs (delta against main specs), design decisions, and implementation tasks. - Apply — Use
/opsx-apply <change-name>to implement tasks. Tasks are checked off intasks.mdas they're completed. - Archive — Use
/opsx-archive <change-name>when all tasks are done. Delta specs are synced into main specs and the change directory is moved toarchive/.
openspec/
├── config.yaml # Project context, rules, tool config
├── specs/ # Main specs (authoritative requirements)
│ └── <capability>/spec.md
└── changes/
└── <change-name>/
├── .openspec.yaml # Change metadata
├── proposal.md # What & why
├── specs/ # Delta specs for this change
│ └── <capability>/spec.md
├── design.md # How
└── tasks.md # Implementation checklist
- Never skip OpenSpec for features, enhancements, or fixes that touch externally observable behavior
- Minor refactors, dependency bumps, and documentation-only changes may bypass the workflow (use conventional commit directly)
- All OpenSpec artifacts live under
openspec/in the repo root