├── .clockking └── db.json ├── .github └── workflows │ └── main.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── screenshots ├── add_clock_entry.png ├── delete_entry.png ├── edit_clock_entry.png ├── granularity.png ├── recording.png ├── start_recording.png ├── stop_recording.png └── task_list.png └── src ├── app_context.rs ├── autosave.rs ├── clock_entries_table.rs ├── clock_entry_form.rs ├── db.rs ├── format.rs ├── granularity_picker.rs ├── input.rs ├── main.rs ├── main_dialog.rs ├── model.rs ├── record.rs ├── stats_view.rs └── time_picker.rs /.clockking/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "clock_entries": [ 3 | { 4 | "from": "09:00:00", 5 | "to": "11:00:00", 6 | "description": "Project Business analysis", 7 | "is_clocked": true, 8 | "granularity": "Detailed" 9 | }, 10 | { 11 | "from": "11:00:00", 12 | "to": "11:30:00", 13 | "description": "Coffee", 14 | "is_clocked": false, 15 | "granularity": "Detailed" 16 | }, 17 | { 18 | "from": "11:30:00", 19 | "to": "13:00:00", 20 | "description": "Implementing requirements", 21 | "is_clocked": false, 22 | "granularity": "Detailed" 23 | }, 24 | { 25 | "from": "13:00:00", 26 | "to": "13:30:00", 27 | "description": "Lunch", 28 | "is_clocked": true, 29 | "granularity": "Detailed" 30 | }, 31 | { 32 | "from": "13:30:00", 33 | "to": "17:00:00", 34 | "description": "Unit tests implementation", 35 | "is_clocked": false, 36 | "granularity": "Detailed" 37 | } 38 | ], 39 | "granularity": "Detailed" 40 | } -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | on: [push, pull_request] 3 | jobs: 4 | check: 5 | name: Check 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout sources 9 | uses: actions/checkout@v2 10 | 11 | - name: Install stable toolchain 12 | uses: actions-rs/toolchain@v1 13 | with: 14 | profile: minimal 15 | toolchain: stable 16 | override: true 17 | 18 | - name: Run cargo check 19 | uses: actions-rs/cargo@v1 20 | with: 21 | command: check 22 | test: 23 | name: Test Suite 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout sources 27 | uses: actions/checkout@v2 28 | 29 | - name: Install stable toolchain 30 | uses: actions-rs/toolchain@v1 31 | with: 32 | profile: minimal 33 | toolchain: stable 34 | override: true 35 | 36 | - name: Run cargo test 37 | uses: actions-rs/cargo@v1 38 | with: 39 | command: test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | .clockking 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.7.6" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" 10 | dependencies = [ 11 | "getrandom", 12 | "once_cell", 13 | "version_check", 14 | ] 15 | 16 | [[package]] 17 | name = "autocfg" 18 | version = "1.1.0" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 21 | 22 | [[package]] 23 | name = "bitflags" 24 | version = "1.3.2" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 27 | 28 | [[package]] 29 | name = "cfg-if" 30 | version = "1.0.0" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 33 | 34 | [[package]] 35 | name = "chrono" 36 | version = "0.4.19" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 39 | dependencies = [ 40 | "libc", 41 | "num-integer", 42 | "num-traits", 43 | "serde", 44 | "time 0.1.44", 45 | "winapi", 46 | ] 47 | 48 | [[package]] 49 | name = "clockking" 50 | version = "0.1.0" 51 | dependencies = [ 52 | "chrono", 53 | "cursive", 54 | "cursive-extras", 55 | "cursive_table_view", 56 | "scheduled-thread-pool", 57 | "serde", 58 | "serde_json", 59 | ] 60 | 61 | [[package]] 62 | name = "crossbeam-channel" 63 | version = "0.5.4" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" 66 | dependencies = [ 67 | "cfg-if", 68 | "crossbeam-utils", 69 | ] 70 | 71 | [[package]] 72 | name = "crossbeam-utils" 73 | version = "0.8.8" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" 76 | dependencies = [ 77 | "cfg-if", 78 | "lazy_static", 79 | ] 80 | 81 | [[package]] 82 | name = "crossterm" 83 | version = "0.22.1" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c" 86 | dependencies = [ 87 | "bitflags", 88 | "crossterm_winapi", 89 | "libc", 90 | "mio", 91 | "parking_lot", 92 | "signal-hook", 93 | "signal-hook-mio", 94 | "winapi", 95 | ] 96 | 97 | [[package]] 98 | name = "crossterm_winapi" 99 | version = "0.9.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" 102 | dependencies = [ 103 | "winapi", 104 | ] 105 | 106 | [[package]] 107 | name = "cursive" 108 | version = "0.17.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "ca536d245342f6c005e7547ab640e444a3dc2fc0319a92124c8c1cbff025e775" 111 | dependencies = [ 112 | "ahash", 113 | "cfg-if", 114 | "crossbeam-channel", 115 | "crossterm", 116 | "cursive_core", 117 | "lazy_static", 118 | "libc", 119 | "log", 120 | "signal-hook", 121 | "unicode-segmentation", 122 | "unicode-width", 123 | ] 124 | 125 | [[package]] 126 | name = "cursive-extras" 127 | version = "0.3.2" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "df7968de69acd496970f763508808507399396905eb72de8223d8798697f272a" 130 | dependencies = [ 131 | "crossbeam-channel", 132 | "cursive_core", 133 | ] 134 | 135 | [[package]] 136 | name = "cursive_core" 137 | version = "0.3.1" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "e27fbda42833e46148ff28db338f6189a4407e4a38ba1f4105e2f623089e66a0" 140 | dependencies = [ 141 | "ahash", 142 | "crossbeam-channel", 143 | "enum-map", 144 | "enumset", 145 | "lazy_static", 146 | "log", 147 | "num", 148 | "owning_ref", 149 | "time 0.3.9", 150 | "unicode-segmentation", 151 | "unicode-width", 152 | "xi-unicode", 153 | ] 154 | 155 | [[package]] 156 | name = "cursive_table_view" 157 | version = "0.14.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "f8935dd87d19c54b7506b245bc988a7b4e65b1058e1d0d64c0ad9b3188e48060" 160 | dependencies = [ 161 | "cursive_core", 162 | ] 163 | 164 | [[package]] 165 | name = "darling" 166 | version = "0.13.4" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" 169 | dependencies = [ 170 | "darling_core", 171 | "darling_macro", 172 | ] 173 | 174 | [[package]] 175 | name = "darling_core" 176 | version = "0.13.4" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" 179 | dependencies = [ 180 | "fnv", 181 | "ident_case", 182 | "proc-macro2", 183 | "quote", 184 | "syn", 185 | ] 186 | 187 | [[package]] 188 | name = "darling_macro" 189 | version = "0.13.4" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" 192 | dependencies = [ 193 | "darling_core", 194 | "quote", 195 | "syn", 196 | ] 197 | 198 | [[package]] 199 | name = "enum-map" 200 | version = "2.1.0" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "0348b2a57c82f98b9dbd8098b1abb2416f221823d3e50cbe24eaebdd16896826" 203 | dependencies = [ 204 | "enum-map-derive", 205 | ] 206 | 207 | [[package]] 208 | name = "enum-map-derive" 209 | version = "0.8.0" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "a63b7a0ddec6f38dcec5e36257750b7a8fcaf4227e12ceb306e341d63634da05" 212 | dependencies = [ 213 | "proc-macro2", 214 | "quote", 215 | "syn", 216 | ] 217 | 218 | [[package]] 219 | name = "enumset" 220 | version = "1.0.10" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "e8b6b2301b38343c1f00b2cc5116d6c28306758344db73e3347f21aa1e60e756" 223 | dependencies = [ 224 | "enumset_derive", 225 | ] 226 | 227 | [[package]] 228 | name = "enumset_derive" 229 | version = "0.5.7" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "b201779d9a5dee6b1478eb1c6b9643b1fbca784ce98ac74e7f33cd1dad620058" 232 | dependencies = [ 233 | "darling", 234 | "proc-macro2", 235 | "quote", 236 | "syn", 237 | ] 238 | 239 | [[package]] 240 | name = "fnv" 241 | version = "1.0.7" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 244 | 245 | [[package]] 246 | name = "getrandom" 247 | version = "0.2.6" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" 250 | dependencies = [ 251 | "cfg-if", 252 | "libc", 253 | "wasi", 254 | ] 255 | 256 | [[package]] 257 | name = "ident_case" 258 | version = "1.0.1" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 261 | 262 | [[package]] 263 | name = "instant" 264 | version = "0.1.12" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 267 | dependencies = [ 268 | "cfg-if", 269 | ] 270 | 271 | [[package]] 272 | name = "itoa" 273 | version = "1.0.1" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" 276 | 277 | [[package]] 278 | name = "lazy_static" 279 | version = "1.4.0" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 282 | 283 | [[package]] 284 | name = "libc" 285 | version = "0.2.122" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "ec647867e2bf0772e28c8bcde4f0d19a9216916e890543b5a03ed8ef27b8f259" 288 | 289 | [[package]] 290 | name = "lock_api" 291 | version = "0.4.7" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" 294 | dependencies = [ 295 | "autocfg", 296 | "scopeguard", 297 | ] 298 | 299 | [[package]] 300 | name = "log" 301 | version = "0.4.16" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" 304 | dependencies = [ 305 | "cfg-if", 306 | ] 307 | 308 | [[package]] 309 | name = "mio" 310 | version = "0.7.14" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" 313 | dependencies = [ 314 | "libc", 315 | "log", 316 | "miow", 317 | "ntapi", 318 | "winapi", 319 | ] 320 | 321 | [[package]] 322 | name = "miow" 323 | version = "0.3.7" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" 326 | dependencies = [ 327 | "winapi", 328 | ] 329 | 330 | [[package]] 331 | name = "ntapi" 332 | version = "0.3.7" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" 335 | dependencies = [ 336 | "winapi", 337 | ] 338 | 339 | [[package]] 340 | name = "num" 341 | version = "0.4.0" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" 344 | dependencies = [ 345 | "num-complex", 346 | "num-integer", 347 | "num-iter", 348 | "num-rational", 349 | "num-traits", 350 | ] 351 | 352 | [[package]] 353 | name = "num-complex" 354 | version = "0.4.0" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" 357 | dependencies = [ 358 | "num-traits", 359 | ] 360 | 361 | [[package]] 362 | name = "num-integer" 363 | version = "0.1.44" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 366 | dependencies = [ 367 | "autocfg", 368 | "num-traits", 369 | ] 370 | 371 | [[package]] 372 | name = "num-iter" 373 | version = "0.1.42" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" 376 | dependencies = [ 377 | "autocfg", 378 | "num-integer", 379 | "num-traits", 380 | ] 381 | 382 | [[package]] 383 | name = "num-rational" 384 | version = "0.4.0" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" 387 | dependencies = [ 388 | "autocfg", 389 | "num-integer", 390 | "num-traits", 391 | ] 392 | 393 | [[package]] 394 | name = "num-traits" 395 | version = "0.2.14" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 398 | dependencies = [ 399 | "autocfg", 400 | ] 401 | 402 | [[package]] 403 | name = "num_threads" 404 | version = "0.1.5" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0" 407 | dependencies = [ 408 | "libc", 409 | ] 410 | 411 | [[package]] 412 | name = "once_cell" 413 | version = "1.10.0" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" 416 | 417 | [[package]] 418 | name = "owning_ref" 419 | version = "0.4.1" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce" 422 | dependencies = [ 423 | "stable_deref_trait", 424 | ] 425 | 426 | [[package]] 427 | name = "parking_lot" 428 | version = "0.11.2" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" 431 | dependencies = [ 432 | "instant", 433 | "lock_api", 434 | "parking_lot_core", 435 | ] 436 | 437 | [[package]] 438 | name = "parking_lot_core" 439 | version = "0.8.5" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" 442 | dependencies = [ 443 | "cfg-if", 444 | "instant", 445 | "libc", 446 | "redox_syscall", 447 | "smallvec", 448 | "winapi", 449 | ] 450 | 451 | [[package]] 452 | name = "proc-macro2" 453 | version = "1.0.37" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" 456 | dependencies = [ 457 | "unicode-xid", 458 | ] 459 | 460 | [[package]] 461 | name = "quote" 462 | version = "1.0.18" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" 465 | dependencies = [ 466 | "proc-macro2", 467 | ] 468 | 469 | [[package]] 470 | name = "redox_syscall" 471 | version = "0.2.13" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" 474 | dependencies = [ 475 | "bitflags", 476 | ] 477 | 478 | [[package]] 479 | name = "ryu" 480 | version = "1.0.9" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" 483 | 484 | [[package]] 485 | name = "scheduled-thread-pool" 486 | version = "0.2.5" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7" 489 | dependencies = [ 490 | "parking_lot", 491 | ] 492 | 493 | [[package]] 494 | name = "scopeguard" 495 | version = "1.1.0" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 498 | 499 | [[package]] 500 | name = "serde" 501 | version = "1.0.136" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" 504 | dependencies = [ 505 | "serde_derive", 506 | ] 507 | 508 | [[package]] 509 | name = "serde_derive" 510 | version = "1.0.136" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" 513 | dependencies = [ 514 | "proc-macro2", 515 | "quote", 516 | "syn", 517 | ] 518 | 519 | [[package]] 520 | name = "serde_json" 521 | version = "1.0.79" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" 524 | dependencies = [ 525 | "itoa", 526 | "ryu", 527 | "serde", 528 | ] 529 | 530 | [[package]] 531 | name = "signal-hook" 532 | version = "0.3.13" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" 535 | dependencies = [ 536 | "libc", 537 | "signal-hook-registry", 538 | ] 539 | 540 | [[package]] 541 | name = "signal-hook-mio" 542 | version = "0.2.3" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" 545 | dependencies = [ 546 | "libc", 547 | "mio", 548 | "signal-hook", 549 | ] 550 | 551 | [[package]] 552 | name = "signal-hook-registry" 553 | version = "1.4.0" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 556 | dependencies = [ 557 | "libc", 558 | ] 559 | 560 | [[package]] 561 | name = "smallvec" 562 | version = "1.8.0" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" 565 | 566 | [[package]] 567 | name = "stable_deref_trait" 568 | version = "1.2.0" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 571 | 572 | [[package]] 573 | name = "syn" 574 | version = "1.0.91" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" 577 | dependencies = [ 578 | "proc-macro2", 579 | "quote", 580 | "unicode-xid", 581 | ] 582 | 583 | [[package]] 584 | name = "time" 585 | version = "0.1.44" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" 588 | dependencies = [ 589 | "libc", 590 | "wasi", 591 | "winapi", 592 | ] 593 | 594 | [[package]] 595 | name = "time" 596 | version = "0.3.9" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" 599 | dependencies = [ 600 | "itoa", 601 | "libc", 602 | "num_threads", 603 | ] 604 | 605 | [[package]] 606 | name = "unicode-segmentation" 607 | version = "1.9.0" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" 610 | 611 | [[package]] 612 | name = "unicode-width" 613 | version = "0.1.9" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 616 | 617 | [[package]] 618 | name = "unicode-xid" 619 | version = "0.2.2" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 622 | 623 | [[package]] 624 | name = "version_check" 625 | version = "0.9.4" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 628 | 629 | [[package]] 630 | name = "wasi" 631 | version = "0.10.0+wasi-snapshot-preview1" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 634 | 635 | [[package]] 636 | name = "winapi" 637 | version = "0.3.9" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 640 | dependencies = [ 641 | "winapi-i686-pc-windows-gnu", 642 | "winapi-x86_64-pc-windows-gnu", 643 | ] 644 | 645 | [[package]] 646 | name = "winapi-i686-pc-windows-gnu" 647 | version = "0.4.0" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 650 | 651 | [[package]] 652 | name = "winapi-x86_64-pc-windows-gnu" 653 | version = "0.4.0" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 656 | 657 | [[package]] 658 | name = "xi-unicode" 659 | version = "0.3.0" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" 662 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clockking" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | cursive = { version = "*", default-features = false, features = ["crossterm-backend"] } 10 | cursive_table_view = "0.14" 11 | cursive-extras = "0.3.0" 12 | chrono = { version = "0.4", features = ["serde"]} 13 | serde = { version = "1.0", features = ["derive"] } 14 | serde_json = "1.0" 15 | scheduled-thread-pool = "0.2.5" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Hrvoje Peradin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clock King 2 | 3 | If you happen to like the app, you can [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/krvoje) 4 | 5 | [![run-tests](https://github.com/krvoje/clockking/actions/workflows/main.yaml/badge.svg?branch=master)](https://github.com/krvoje/clockking/actions/workflows/main.yaml) 6 | 7 | A console app for jotting down hours tracking for the day. 8 | Written to learn Rust with [Cursive](https://github.com/gyscos/cursive). 9 | 10 | Meant as manual task list, where the results can be transferred to a time tracking app at the end of day. 11 | 12 | *Disclaimer:* This is a weekend project that I did for fun, and ended up finding useful at work. 13 | It's by no means an app with guarantees on anything, but more akin to a script with a TUI I hacked for personal use. 14 | I've had no issues with losing data, but that doesn't mean it's impossible in some corner cases. So, please bear that 15 | in mind when using the app, and approach it lightheartedly as I did, as a tool to have fun with, that's also useful. 16 | Issues, comments, and suggestions are welcome. 17 | 18 | # Quickstart 19 | 20 | To start the program, just run: 21 | 22 | `cargo run --release` 23 | 24 | If you do not have the Rust toolchain installed, [check the installation instructions](https://www.rust-lang.org/learn/get-started). 25 | 26 | # Functionality 27 | 28 | ## Clock entries list 29 | 30 | This is the main screen, and contains the list of clock entries that can be checked once they are entered to a time tracking app. 31 | 32 | Keyboard shortcuts are listed on the screen buttons, spacebar toggles whether an item is clocked or not. 33 | 34 | ![Clock entries list](screenshots/task_list.png) 35 | 36 | ## Adding a clock entry 37 | 38 | Pressing the `<(A)dd>` adds a new clock entry after the current selection in the table. Clock King tries to be smart and guess the end time of the task. 39 | 40 | ![Add new entry](screenshots/add_clock_entry.png) 41 | 42 | ## Editing a clock entry 43 | 44 | Pressing `Enter` opens the edit form for the current selection. 45 | 46 | ![Edit entry](screenshots/edit_clock_entry.png) 47 | 48 | ## Deleting a clock entry 49 | 50 | Entries can be deleted from the list, and restored from the undo buffer. 51 | 52 | ![Edit entry](screenshots/delete_entry.png) 53 | 54 | ## Granularity 55 | 56 | There is a time-granularity toggle for how detailed you want to be with your tracking. 57 | - Relaxed (1h) 58 | - Reasonable (30m) 59 | - Detailed (15m) 60 | - Paranoid (5m) 61 | - OCD (1m) 62 | - Scientific (1s) 63 | 64 | Changing the granularity automatically adjusts existing entries. 65 | 66 | ![Granularity](screenshots/granularity.png) 67 | 68 | ## Record a new entry 69 | 70 | By pressing `` you can submit details for a new entry you want to start recording. The start time 71 | of the entry will be set to the current timestamp (rounded to the selected granularity). When you submit the data, 72 | Clock Kings starts the recording. 73 | 74 | ![Start recording](screenshots/start_recording.png) 75 | 76 | You can continue working while Clock King records your entry in the background. 77 | 78 | ![Recording](screenshots/recording.png) 79 | 80 | By pressing `` Clock King stops the recording at the current timestamp, and you can submit the updated 81 | entry details. 82 | 83 | ![End Recording](screenshots/stop_recording.png) 84 | 85 | ## File database 86 | 87 | The executable creates a `./.clockking/db.json` where the current list is stored. There's an autosave thread, and the data 88 | gets saved when you quit the program normally. 89 | 90 | -------------------------------------------------------------------------------- /screenshots/add_clock_entry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krvoje/clockking/0d9ed84189e69f727b655fe1e90a0253f746eb8f/screenshots/add_clock_entry.png -------------------------------------------------------------------------------- /screenshots/delete_entry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krvoje/clockking/0d9ed84189e69f727b655fe1e90a0253f746eb8f/screenshots/delete_entry.png -------------------------------------------------------------------------------- /screenshots/edit_clock_entry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krvoje/clockking/0d9ed84189e69f727b655fe1e90a0253f746eb8f/screenshots/edit_clock_entry.png -------------------------------------------------------------------------------- /screenshots/granularity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krvoje/clockking/0d9ed84189e69f727b655fe1e90a0253f746eb8f/screenshots/granularity.png -------------------------------------------------------------------------------- /screenshots/recording.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krvoje/clockking/0d9ed84189e69f727b655fe1e90a0253f746eb8f/screenshots/recording.png -------------------------------------------------------------------------------- /screenshots/start_recording.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krvoje/clockking/0d9ed84189e69f727b655fe1e90a0253f746eb8f/screenshots/start_recording.png -------------------------------------------------------------------------------- /screenshots/stop_recording.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krvoje/clockking/0d9ed84189e69f727b655fe1e90a0253f746eb8f/screenshots/stop_recording.png -------------------------------------------------------------------------------- /screenshots/task_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krvoje/clockking/0d9ed84189e69f727b655fe1e90a0253f746eb8f/screenshots/task_list.png -------------------------------------------------------------------------------- /src/app_context.rs: -------------------------------------------------------------------------------- 1 | use cursive::Cursive; 2 | use crate::GlobalContext; 3 | 4 | pub fn fetch(s: &mut Cursive) -> &mut GlobalContext { 5 | s.user_data::().expect("Global context should be defined") 6 | } -------------------------------------------------------------------------------- /src/autosave.rs: -------------------------------------------------------------------------------- 1 | use cursive::Cursive; 2 | use scheduled_thread_pool::ScheduledThreadPool; 3 | use crate::db; 4 | 5 | pub fn start_autosave_loop(siv: &Cursive) { 6 | let cb_sink = siv.cb_sink().clone(); 7 | let thread_pool = ScheduledThreadPool::new(1); 8 | thread_pool.execute_at_fixed_rate( 9 | core::time::Duration::from_secs(30), 10 | core::time::Duration::from_secs(30), 11 | move || { cb_sink.send(Box::new(db::save_to_db)).unwrap() } 12 | ); 13 | } -------------------------------------------------------------------------------- /src/clock_entries_table.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::ops::Add; 3 | 4 | use chrono::Duration; 5 | use cursive::align::HAlign; 6 | use cursive::Cursive; 7 | use cursive::traits::{Nameable, Resizable}; 8 | use cursive::views::{NamedView, ResizedView}; 9 | use cursive_table_view::{TableView, TableViewItem}; 10 | 11 | use crate::{app_context, clock_entry_form, ClockEntry, ClockKing, format, granularity_picker, input, stats_view, time_picker}; 12 | 13 | pub const CLOCK_ENTRIES_TABLE: &str = "clock_entries"; 14 | 15 | #[derive(Copy, Clone, PartialEq, Eq, Hash)] 16 | pub enum ClockEntryColumn { 17 | From, 18 | To, 19 | Description, 20 | Duration, 21 | IsClocked, 22 | } 23 | 24 | impl ClockEntryColumn { 25 | pub(crate) fn as_str(&self) -> &str { 26 | match *self { 27 | ClockEntryColumn::From => "From", 28 | ClockEntryColumn::To => "To", 29 | ClockEntryColumn::Description => "Description", 30 | ClockEntryColumn::Duration => "Duration", 31 | ClockEntryColumn::IsClocked => "Clocked", 32 | } 33 | } 34 | } 35 | 36 | impl TableViewItem for ClockEntry { 37 | fn to_column(&self, column: ClockEntryColumn) -> String { 38 | match column { 39 | ClockEntryColumn::From => format::format_naive_time(self.granularity, self.from), 40 | ClockEntryColumn::To => format::format_naive_time(self.granularity, self.to), 41 | ClockEntryColumn::Description => self.description.to_string(), 42 | ClockEntryColumn::Duration => format::format_hms(self.granularity, self.duration().num_seconds()), 43 | ClockEntryColumn::IsClocked => if self.is_clocked { "[x]".to_string() } else { "[ ]".to_string() }, 44 | } 45 | } 46 | 47 | fn cmp(&self, other: &Self, column: ClockEntryColumn) -> Ordering where Self: Sized { 48 | match column { 49 | ClockEntryColumn::From => self.from.cmp(&other.from), 50 | ClockEntryColumn::To => self.to.cmp(&other.to), 51 | ClockEntryColumn::Description => self.description.cmp(&other.description), 52 | ClockEntryColumn::Duration => self.duration().cmp(&other.duration()), 53 | ClockEntryColumn::IsClocked => self.is_clocked.cmp(&other.is_clocked), 54 | } 55 | } 56 | } 57 | 58 | pub fn new(model: ClockKing) -> ResizedView>> { 59 | let mut table: TableView = TableView::::new() 60 | .column(ClockEntryColumn::From, ClockEntryColumn::From.as_str(), |c| {c.width_percent(10).align(HAlign::Center) }) 61 | .column(ClockEntryColumn::To, ClockEntryColumn::To.as_str(), |c| {c.width_percent(10).align(HAlign::Center)}) 62 | .column(ClockEntryColumn::Description, ClockEntryColumn::Description.as_str(), |c| {c.align(HAlign::Center)}) 63 | .column(ClockEntryColumn::Duration, ClockEntryColumn::Duration.as_str(), |c| {c.width_percent(12).align(HAlign::Center)}) 64 | .column(ClockEntryColumn::IsClocked, ClockEntryColumn::IsClocked.as_str(), |c| {c.width_percent(12).align(HAlign::Center)}) 65 | .items(model.clock_entries) 66 | ; 67 | 68 | 69 | table.set_on_submit(move |s: &mut Cursive, _: usize, index: usize| { 70 | edit_entry(s, index); 71 | }); 72 | 73 | table 74 | .with_name(CLOCK_ENTRIES_TABLE) 75 | .min_size((100,20)) 76 | } 77 | 78 | fn edit_entry(s: &mut Cursive, index: usize) { 79 | let granularity = granularity_picker::get_granularity(s); 80 | let form = s.call_on_name(CLOCK_ENTRIES_TABLE, move |t: &mut TableView| { 81 | let current_entry = t.borrow_item(index).cloned(); 82 | clock_entry_form::new( 83 | "Edit Clock Entry ⏰", 84 | current_entry.as_ref(), 85 | granularity, 86 | move |s: &mut Cursive| submit_clock_entry(s, Some(index))) 87 | }).unwrap(); 88 | s.add_layer(form); 89 | } 90 | 91 | pub fn add_new_entry(s: &mut Cursive) { 92 | let granularity = granularity_picker::get_granularity(s); 93 | let template_entry: Option = s.call_on_name(CLOCK_ENTRIES_TABLE, move |t: &mut TableView| { 94 | t.item().and_then(|it| t.borrow_item(it).map(|it| ClockEntry { 95 | from: it.to, 96 | to: it.to.add(Duration::minutes(60)), 97 | description: String::from(""), 98 | is_clocked: false, 99 | granularity, 100 | })) 101 | }).unwrap(); 102 | 103 | s.add_layer( 104 | clock_entry_form::new( 105 | "Add Clock Entry ⏰", 106 | template_entry.as_ref(), 107 | granularity, 108 | move |s: &mut Cursive| submit_clock_entry(s, None) 109 | ) 110 | ); 111 | } 112 | 113 | fn submit_clock_entry(s: &mut Cursive, index: Option) { 114 | let new_entry = ClockEntry { 115 | from: time_picker::time_picker_value(s, ClockEntryColumn::From), 116 | to: time_picker::time_picker_value(s, ClockEntryColumn::To), 117 | description: input::text_area_value(s, ClockEntryColumn::Description), 118 | is_clocked: input::checkbox_value(s, ClockEntryColumn::IsClocked), 119 | granularity: granularity_picker::get_granularity(s) 120 | }; 121 | s.call_on_name(CLOCK_ENTRIES_TABLE, |table: &mut TableView| { 122 | index.map(|i| table.remove_item(i)); 123 | table.insert_item(new_entry); 124 | }).expect("Unable to get clock entries table"); 125 | stats_view::update_stats(s); 126 | s.pop_layer(); 127 | } 128 | 129 | pub fn delete_current_entry(s: &mut Cursive) { 130 | s.add_layer( 131 | cursive_extras::confirm_dialog( 132 | "Delete entry", 133 | "Are you sure?", 134 | |s| { 135 | s.pop_layer(); 136 | let deleted = s.call_on_name(CLOCK_ENTRIES_TABLE, move |t: &mut TableView| { 137 | t.item().and_then(|index| t.remove_item(index)) 138 | }).unwrap(); 139 | app_context::fetch(s).delete(deleted); 140 | stats_view::update_stats(s) 141 | } 142 | )); 143 | } 144 | 145 | pub fn undo_delete(s: &mut Cursive) { 146 | if let Some(deleted) = app_context::fetch(s).undo() { 147 | s.call_on_name(CLOCK_ENTRIES_TABLE, move |t: &mut TableView| { 148 | t.insert_item(deleted); 149 | }); 150 | } 151 | stats_view::update_stats(s); 152 | } 153 | 154 | pub fn mark_current_entry_as_clocked(s: &mut Cursive) { 155 | s.call_on_name(CLOCK_ENTRIES_TABLE, move |t: &mut TableView| { 156 | if let Some(index) = t.item() { 157 | let mut item = t.borrow_item_mut(index).expect("No entry at current index"); 158 | item.is_clocked = !item.is_clocked; 159 | } 160 | }).unwrap(); 161 | stats_view::update_stats(s); 162 | } 163 | 164 | pub fn get_clock_entries(s: &mut Cursive) -> Vec { 165 | s.call_on_name(CLOCK_ENTRIES_TABLE, |table: &mut TableView| { 166 | table.borrow_items().to_vec() 167 | }).expect("Clock entries table not defined") 168 | } -------------------------------------------------------------------------------- /src/clock_entry_form.rs: -------------------------------------------------------------------------------- 1 | use cursive::Cursive; 2 | use cursive::traits::Nameable; 3 | use cursive::views::{Dialog, ListView, NamedView}; 4 | 5 | use crate::{ClockEntry, Granularity, input, time_picker}; 6 | use crate::clock_entries_table::ClockEntryColumn; 7 | 8 | const CLOCK_ENTRY_FORM: &str = "edit_clock_entry"; 9 | 10 | pub fn new( 11 | prompt: &str, 12 | entry: Option<&ClockEntry>, 13 | granularity: Granularity, 14 | on_submit: F, 15 | ) -> NamedView 16 | where 17 | F: 'static + Fn(&mut Cursive) { 18 | Dialog::new() 19 | .title(prompt) 20 | .button("Cancel", |s| { s.pop_layer(); }) 21 | .content( 22 | ListView::new() 23 | .child( 24 | ClockEntryColumn::From.as_str(), 25 | time_picker::time_picker_input(ClockEntryColumn::From, entry.map(|it| it.from), granularity) 26 | ) 27 | .child( 28 | ClockEntryColumn::To.as_str(), 29 | time_picker::time_picker_input(ClockEntryColumn::To, entry.map(|it|it.to), granularity) 30 | ) 31 | .child( 32 | ClockEntryColumn::Description.as_str(), 33 | input::text_area_input(ClockEntryColumn::Description, entry.map(|it| it.description.clone())) 34 | ) 35 | .child( 36 | ClockEntryColumn::IsClocked.as_str(), 37 | input::checkbox_input(ClockEntryColumn::IsClocked, entry.map(|it| it.is_clocked)) 38 | ) 39 | ) 40 | .button("Ok",on_submit).with_name(CLOCK_ENTRY_FORM) 41 | } -------------------------------------------------------------------------------- /src/db.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{create_dir_all, File}; 2 | use std::io::{BufReader, BufWriter}; 3 | 4 | use cursive::Cursive; 5 | 6 | use crate::{app_context, clock_entries_table, ClockEntry, ClockKing, GlobalContext, Granularity, granularity_picker}; 7 | 8 | const DB_LOCATION: &str = "./.clockking/db.json"; 9 | 10 | pub fn init_from_db(s: &mut Cursive) -> ClockKing { 11 | create_dir_all("./.clockking").expect("Unable to create the .clockking directory"); 12 | let file = File::open(DB_LOCATION).or_else(|_| File::create(DB_LOCATION)).expect("Unable to create nor open a .clockking file"); 13 | let reader = BufReader::new(file); 14 | let u: ClockKing = serde_json::from_reader(reader).unwrap_or_else(|_| ClockKing { 15 | clock_entries: Vec::::default(), 16 | granularity: Granularity::Detailed, 17 | }); 18 | s.set_user_data(GlobalContext::new(&u)); 19 | 20 | u 21 | } 22 | 23 | pub fn save_to_db(s: &mut Cursive) { 24 | let clock_entries = clock_entries_table::get_clock_entries(s); 25 | let granularity = granularity_picker::get_granularity(s); 26 | let new_model = ClockKing { 27 | clock_entries, 28 | granularity, 29 | }; 30 | if app_context::fetch(s).model_changed(&new_model) { 31 | save_model_to_db(s, &new_model); 32 | } 33 | } 34 | 35 | fn save_model_to_db(s: &mut Cursive, clock_king: &ClockKing) { 36 | app_context::fetch(s).save(clock_king.clone()); 37 | let file = File::create(DB_LOCATION).expect("Unable to open DB file"); 38 | let writer = BufWriter::new(file); 39 | serde_json::to_writer_pretty(writer, &clock_king).expect("Saving to DB failed"); 40 | } -------------------------------------------------------------------------------- /src/format.rs: -------------------------------------------------------------------------------- 1 | use chrono::{NaiveTime, Timelike}; 2 | 3 | use crate::granularity_picker::Granularity; 4 | 5 | pub fn format_hms_with_prompt(granularity: Granularity, prompt: &str, total_seconds: i64) -> String { 6 | format!("{}:\t{}", prompt, format_hms(granularity, total_seconds)) 7 | } 8 | 9 | pub fn format_hms(granularity: Granularity, total_seconds: i64) -> String { 10 | let hours = total_seconds / 3600; 11 | let minutes = (total_seconds / 60) % 60; 12 | let seconds = total_seconds % 60; 13 | 14 | match granularity { 15 | Granularity::Relaxed => format!("{}h", hours), 16 | Granularity::Reasonable => format!("{}h {:02$}m", hours, minutes / 30 * 30, 2), 17 | Granularity::Detailed => format!("{}h {:02$}m", hours, minutes / 15 * 15, 2), 18 | Granularity::Paranoid => format!("{}h {:02$}m", hours, minutes / 5 * 5, 2), 19 | Granularity::Ocd => format!("{}h {:02$}m", hours, minutes, 2), 20 | Granularity::Scientific => format!("{}h {:03$}m {:03$}s", hours, minutes, seconds, 2), 21 | } 22 | } 23 | 24 | pub fn format_naive_time(granularity: Granularity, it: NaiveTime) -> String { 25 | format_clock(granularity, it.hour(), it.minute(), it.second()) 26 | } 27 | 28 | pub fn format_clock(granularity: Granularity, hours: u32, minutes: u32, seconds: u32) -> String { 29 | match granularity { 30 | Granularity::Relaxed => format!("{:01$}:00", hours, 2), 31 | Granularity::Reasonable => format!("{:02$}:{:02$}", hours, (minutes / 30) * 30, 2), 32 | Granularity::Detailed => format!("{:02$}:{:02$}", hours, (minutes / 15) * 15, 2), 33 | Granularity::Paranoid => format!("{:02$}:{:02$}", hours, (minutes / 5) * 5, 2), 34 | Granularity::Ocd => format!("{:02$}:{:02$}", hours, minutes, 2), 35 | Granularity::Scientific => format!("{:03$}:{:03$}:{:03$}", hours, minutes, seconds, 2), 36 | } 37 | } 38 | 39 | #[cfg(test)] 40 | mod format_hms_test { 41 | use crate::format::format_hms; 42 | use crate::Granularity; 43 | 44 | #[test] 45 | fn format_hms_relaxed() { 46 | (0..24).for_each(|hour| { 47 | (0..60).for_each(|minute| { 48 | (0..60).for_each(|second| { 49 | assert_eq!( 50 | format_hms(Granularity::Relaxed, hour * 3600 + minute * 60 + second), 51 | format!("{}h", hour) 52 | ) 53 | }) 54 | }) 55 | }); 56 | } 57 | 58 | #[test] 59 | fn format_hms_reasonable() { 60 | (0..24).for_each(|hour| { 61 | (0..60).for_each(|minute| { 62 | (0..60).for_each(|second| { 63 | assert_eq!( 64 | format_hms(Granularity::Reasonable, hour * 3600 + minute * 60 + second), 65 | format!("{}h {:02$}m", hour, minute / 30 * 30, 2) 66 | ) 67 | }) 68 | }) 69 | }); 70 | } 71 | 72 | #[test] 73 | fn format_hms_detailed() { 74 | (0..24).for_each(|hour| { 75 | (0..60).for_each(|minute| { 76 | (0..60).for_each(|second| { 77 | assert_eq!( 78 | format_hms(Granularity::Detailed, hour * 3600 + minute * 60 + second), 79 | format!("{}h {:02$}m", hour, minute / 15 * 15, 2) 80 | ) 81 | }) 82 | }) 83 | }); 84 | } 85 | 86 | #[test] 87 | fn format_hms_paranoid() { 88 | (0..24).for_each(|hour| { 89 | (0..60).for_each(|minute| { 90 | (0..60).for_each(|second| { 91 | assert_eq!( 92 | format_hms(Granularity::Paranoid, hour * 3600 + minute * 60 + second), 93 | format!("{}h {:02$}m", hour, minute / 5 * 5, 2) 94 | ) 95 | }) 96 | }) 97 | }); 98 | } 99 | 100 | #[test] 101 | fn format_hms_ocd() { 102 | (0..24).for_each(|hour| { 103 | (0..60).for_each(|minute| { 104 | (0..60).for_each(|second| { 105 | assert_eq!( 106 | format_hms(Granularity::Ocd, hour * 3600 + minute * 60 + second), 107 | format!("{}h {:02$}m", hour, minute, 2) 108 | ) 109 | }) 110 | }) 111 | }); 112 | } 113 | 114 | #[test] 115 | fn format_hms_scientific() { 116 | (0..24).for_each(|hour| { 117 | (0..60).for_each(|minute| { 118 | (0..60).for_each(|second| { 119 | assert_eq!( 120 | format_hms(Granularity::Scientific, hour * 3600 + minute * 60 + second), 121 | format!("{}h {:03$}m {:03$}s", hour, minute, second, 2) 122 | ) 123 | }) 124 | }) 125 | }); 126 | } 127 | 128 | } 129 | 130 | #[cfg(test)] 131 | mod format_clock_test { 132 | use crate::format::format_clock; 133 | use crate::Granularity; 134 | 135 | #[test] 136 | fn format_clock_test() { 137 | (0..24).for_each(|hour| { 138 | (0..60).for_each(|minute| { 139 | (0..60).for_each(|second| { 140 | assert_eq!( 141 | format_clock(Granularity::Relaxed, hour, minute, second), 142 | format!("{:01$}:00", hour, 2) 143 | ); 144 | assert_eq!( 145 | format_clock(Granularity::Reasonable, hour, minute, second), 146 | format!("{:02$}:{:02$}", hour, minute / 30 * 30, 2) 147 | ); 148 | assert_eq!( 149 | format_clock(Granularity::Detailed, hour, minute, second), 150 | format!("{:02$}:{:02$}", hour, minute / 15 * 15, 2) 151 | ); 152 | assert_eq!( 153 | format_clock(Granularity::Paranoid, hour, minute, second), 154 | format!("{:02$}:{:02$}", hour, minute / 5 * 5, 2) 155 | ); 156 | assert_eq!( 157 | format_clock(Granularity::Ocd, hour, minute, second), 158 | format!("{:02$}:{:02$}", hour, minute, 2) 159 | ); 160 | assert_eq!( 161 | format_clock(Granularity::Scientific, hour, minute, second), 162 | format!("{:03$}:{:03$}:{:03$}", hour, minute, second, 2) 163 | ); 164 | }) 165 | }) 166 | }); 167 | } 168 | } -------------------------------------------------------------------------------- /src/granularity_picker.rs: -------------------------------------------------------------------------------- 1 | use chrono::{NaiveTime, Timelike}; 2 | use cursive::{Cursive, traits::Nameable, views::SelectView}; 3 | use cursive::direction::Orientation; 4 | use cursive::traits::Resizable; 5 | use cursive::views::{LinearLayout, NamedView, TextView}; 6 | use cursive_table_view::TableView; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::{app_context, clock_entries_table, model::ClockEntry, stats_view}; 10 | use crate::clock_entries_table::ClockEntryColumn; 11 | 12 | const GRANULARITY: &str = "Granularity"; 13 | 14 | #[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] 15 | pub enum Granularity { 16 | Relaxed, 17 | Reasonable, 18 | Detailed, 19 | Paranoid, 20 | Ocd, 21 | Scientific, 22 | } 23 | 24 | pub fn new(selected_granularity: Granularity) -> LinearLayout { 25 | LinearLayout::new(Orientation::Horizontal) 26 | .child(TextView::new("Time granularity:").min_width(20)) 27 | .child(create_view(selected_granularity)) 28 | } 29 | 30 | fn create_view(selected_granularity: Granularity) -> NamedView> { 31 | let mut view = SelectView::new().popup(); 32 | view.add_item("Relaxed (1h)", Granularity::Relaxed); 33 | view.add_item("Reasonable (30m)", Granularity::Reasonable); 34 | view.add_item("Detailed (15m)", Granularity::Detailed); 35 | view.add_item("Paranoid (5m)", Granularity::Paranoid); 36 | view.add_item("OCD (1m)", Granularity::Ocd); 37 | view.add_item("Scientific (1s)", Granularity::Scientific); 38 | view.set_selection(selected_granularity as usize); 39 | 40 | view.on_submit(move |s, granularity| { 41 | select_granularity(s, *granularity); 42 | }).with_name(GRANULARITY) 43 | } 44 | 45 | fn select_granularity(s: &mut Cursive, granularity: Granularity) { 46 | s.call_on_name(clock_entries_table::CLOCK_ENTRIES_TABLE, |t: &mut TableView| { 47 | for item in t.borrow_items_mut() { 48 | normalize_for_granularity(item, granularity); 49 | }; 50 | }).expect("The Clock entries table should be defined"); 51 | app_context::fetch(s).normalize_recording(granularity); 52 | stats_view::update_stats(s); 53 | } 54 | 55 | pub fn get_granularity(s: &mut Cursive) -> Granularity { 56 | s.call_on_name(GRANULARITY, |view: &mut SelectView|{ 57 | *view.selection().expect("Something should be selected") 58 | }).expect("The Granularity select should be defined") 59 | } 60 | 61 | pub fn normalize_for_granularity(item: &mut ClockEntry, granularity: Granularity) { 62 | item.from = normalize(item.from, granularity); 63 | item.to = normalize(item.to, granularity); 64 | item.granularity = granularity; 65 | } 66 | 67 | fn normalize(it: NaiveTime, granularity: Granularity) -> NaiveTime { 68 | match granularity { 69 | Granularity::Relaxed => { 70 | it.with_minute(0).unwrap() 71 | .with_second(0).unwrap() 72 | .with_nanosecond(0).unwrap() 73 | }, 74 | Granularity::Reasonable => { 75 | it.with_minute((it.minute() / 30) * 30).unwrap() 76 | .with_second(0).unwrap() 77 | .with_nanosecond(0).unwrap() 78 | }, 79 | Granularity::Detailed => { 80 | it.with_minute((it.minute() / 15) * 15).unwrap() 81 | .with_second(0).unwrap() 82 | .with_nanosecond(0).unwrap() 83 | }, 84 | Granularity::Paranoid => { 85 | it.with_minute((it.minute() / 5) * 5).unwrap() 86 | .with_second(0).unwrap() 87 | .with_nanosecond(0).unwrap() 88 | }, 89 | Granularity::Ocd => { 90 | it.with_second(0).unwrap() 91 | .with_nanosecond(0).unwrap() 92 | }, 93 | Granularity::Scientific => { 94 | it.with_nanosecond(0).unwrap() 95 | }, 96 | } 97 | } 98 | 99 | #[cfg(test)] 100 | mod test { 101 | use chrono::NaiveTime; 102 | 103 | use crate::Granularity; 104 | use crate::granularity_picker::normalize; 105 | 106 | #[test] 107 | fn test_normalize() { 108 | (0..24).for_each(|hour| { 109 | (0..60).for_each(|minute| { 110 | (0..60).for_each(|second| { 111 | assert_eq!( 112 | normalize(NaiveTime::from_hms(hour, minute, second), Granularity::Relaxed), 113 | NaiveTime::from_hms(hour, 0, 0) 114 | ); 115 | assert_eq!( 116 | normalize(NaiveTime::from_hms(hour, minute, second), Granularity::Reasonable), 117 | NaiveTime::from_hms(hour, minute / 30 * 30, 0) 118 | ); 119 | assert_eq!( 120 | normalize(NaiveTime::from_hms(hour, minute, second), Granularity::Detailed), 121 | NaiveTime::from_hms(hour, minute / 15 * 15, 0) 122 | ); 123 | assert_eq!( 124 | normalize(NaiveTime::from_hms(hour, minute, second), Granularity::Paranoid), 125 | NaiveTime::from_hms(hour, minute / 5 * 5, 0) 126 | ); 127 | assert_eq!( 128 | normalize(NaiveTime::from_hms(hour, minute, second), Granularity::Ocd), 129 | NaiveTime::from_hms(hour, minute, 0) 130 | ); 131 | assert_eq!( 132 | normalize(NaiveTime::from_hms(hour, minute, second), Granularity::Scientific), 133 | NaiveTime::from_hms(hour, minute, second) 134 | ); 135 | }) 136 | }) 137 | }); 138 | } 139 | } -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | use cursive::Cursive; 2 | use cursive::traits::Nameable; 3 | use cursive::views::{Checkbox, NamedView, TextArea}; 4 | use crate::clock_entries_table::ClockEntryColumn; 5 | 6 | pub fn text_area_input(col: ClockEntryColumn, value:Option) -> NamedView