Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/common.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ jobs:
java-version: 21
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Run unit tests
run: ./gradlew test --scan
- name: Run E2E tests (Linux)
if: ${{ inputs.runs-on == 'ubuntu-latest' }}
run: ./gradlew testE2e --scan
- name: Execute Gradle build
run: ./gradlew ${{ inputs.gradle-tasks }} --scan
- name: Upload dists
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,10 @@ build
/gen-src
# mac stuff
**/.DS_Store

# KI / Agent stuff

.agentbridge
opencode.jsonc
opencode.jsonc.tui-migration.bak
tui.json
105 changes: 105 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# AGENTS.md - SimpleTimeTracking (STT)

## Purpose

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.

## Use Cases

- **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

## Architecture

### Tech Stack

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

### Module / Package Layout

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](doc/ui-architecture.md) for a detailed breakdown of UI components, controllers, data objects, and event bus wiring.

### Key Design Patterns

- **Command pattern**: `Command` (sealed hierarchy) + `CommandHandler` interface (Visitor), parsed via ANTLR grammar
- **Dependency Injection**: Dagger `@Module` / `@Provides` / `@Inject`; components are `Dagger*Application` (e.g., `DaggerCLIApplication`, `DaggerUIApplication`)
- **Event-driven**: `MBassador` event bus for decoupled UI updates (time ticks, shutdown, notifications)
- **Repository**: `ItemReader` / `ItemWriter` interfaces abstract storage; `STTItemReader` / `STTItemWriter` implement the plain-text file format
- **Service lifecycle**: `Service` interface with `start()`/`stop()` for config, backup, and logging services
- **Data stored**: A plain-text file (`.stt/activities`) with one record per line

## Code Style

- **Language**: Kotlin (prefer immutable data classes, `val`, extension functions)
- **Naming**: `camelCase` for methods/variables, `PascalCase` for classes, no underscores
- **Test naming**: `should[Expectation]` — e.g., `shouldCreateItemWithoutEnd`
- **Test structure**: GIVEN / WHEN / THEN comment annotations, `sut` (system under test) variable
- **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(...)`)
- **File format**: one time-tracking record per line, human-readable text

## Build & Test

```bash
./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 application
```

Output fat jar: `build/libs/STT-<version>.jar`

## Commit Convention

Use [Conventional Commits](https://www.conventionalcommits.org/):

```
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`
46 changes: 38 additions & 8 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ plugins {
id("org.sonarqube") version "5.1.0.4882"
id("com.github.ben-manes.versions") version "0.36.0"

// JavaFX distribution and packaging
id("org.openjfx.javafxplugin") version "0.1.0"

// Java module support (required for jlink/jpackage)
id("org.javamodularity.moduleplugin") version "1.8.12"
id("org.beryx.jlink") version "3.0.1"

// Auto-versioning from git tags
id("com.palantir.git-version") version "2.0.0"
}

Expand All @@ -33,7 +36,7 @@ repositories {
}

// -SNAPSHOT is added if the release task is not set
// jpackge for windows needs an 2 digit version, at least
// jpackage for windows needs a 2-digit version, at least
val upcomingVersion = "4.0"
val archivesBaseName = "STT"

Expand All @@ -42,7 +45,7 @@ version = upcomingVersion
application {
mainModule.set("org.stt")
mainClass.set("org.stt.StartWithJFX")
// add-opens, so we can access the file decoration-warning.png within this module
// Required so ControlsFX validation decoration can access its own internal PNG
applicationDefaultJvmArgs =
listOf("--add-opens=org.controlsfx.controls/impl.org.controlsfx.control.validation=org.stt")
}
Expand All @@ -64,7 +67,7 @@ configurations {
val spek_version = "2.0.4"

dependencies {
val daggerVersion = "2.50" // with dagger 2.52 they introduced an incomplete usage ofjakarta.inject
val daggerVersion = "2.50" // with dagger 2.52 they introduced an incomplete usage of jakarta.inject
antlr(group = "org.antlr", name = "antlr4", version = "4.9.1")
implementation(group = "org.antlr", name = "antlr4-runtime", version = "4.9.1")

Expand All @@ -80,11 +83,18 @@ dependencies {
implementation("com.jsoniter:jsoniter:0.9.23")
implementation(kotlin("stdlib-jdk8"))

// ── Test dependencies ──
testImplementation("commons-io:commons-io:2.8.0")
testImplementation("org.mockito:mockito-core:5.12.0")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
testImplementation("org.assertj:assertj-core:3.26.3")
testImplementation("junit:junit-dep:4.11")
// TestFX for JavaFX UI end-to-end tests
testImplementation("org.testfx:testfx-core:4.0.18")
// Monocle headless platform, pinned to JDK 21.0.x to match the javafx plugin version
testRuntimeOnly("org.testfx:openjfx-monocle:21.0.2") {
exclude(group = "org.openjfx")
}
}

javafx {
Expand All @@ -106,7 +116,7 @@ distributions.getByName("main") {
}

tasks.compileJava {
// workaround, to make kapt created classes available to java module source set
// Make kapt-generated sources available to the Java module source set
sourceSets {
main {
java {
Expand All @@ -118,9 +128,30 @@ tasks.compileJava {

tasks.test {
extensions.configure(TestModuleOptions::class) {
// disable java-module path for tests
// Tests run on the classpath (not the module path) to avoid JPMS issues
// with test frameworks and mocking libraries
runOnClasspath = true
}
// E2E tests are excluded from the default `test` task — run them separately
// via the `testE2e` task below.
exclude("**/*E2ETest*")
}

/*
* Custom task for running TestFX end-to-end tests.
* These tests require Monocle (headless JavaFX) and are excluded from
* the regular `test` task because they are slow and need a display-less
* environment.
*
* Usage: ./gradlew testE2e
*/
tasks.register<Test>("testE2e") {
include("**/*E2ETest*")
useJUnit()
testLogging {
events("passed", "failed", "skipped")
showExceptions = true
}
}

tasks.withType<org.jetbrains.kotlin.gradle.internal.KaptGenerateStubsTask> {
Expand Down Expand Up @@ -190,7 +221,7 @@ jlink {
jvmArgs =
application.applicationDefaultJvmArgs.plus(
listOf(
// some classes in the merged-modules (see jlink plugin( need access to javafx modules
// some classes in the merged-modules (see jlink plugin) need access to javafx modules
"--add-reads=simpleTimeTracking.merged.module=javafx.graphics",
"--add-reads=simpleTimeTracking.merged.module=javafx.base",
"--add-reads=simpleTimeTracking.merged.module=javafx.controls"
Expand Down Expand Up @@ -223,5 +254,4 @@ jlink {
icon = "src/main/resources/Logo.icns"
}
}
}

}
Loading
Loading