140 Commits

Author SHA1 Message Date
d1bf54f31d fix(deps): update all dependencies
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2025-07-15 03:28:26 +00:00
10f832c1d1 fix(deps): update rust crate serde to v1.0.219
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-03 03:27:44 +00:00
c57b1bc434 chore(deps): update rust crate axum to v0.8.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-03 00:28:23 +00:00
6b2cc8925a chore(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-04 01:47:13 +00:00
e9b810837e fix(deps): update rust crate uuid to v1.15.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-02-27 01:45:00 +00:00
12fd200185 chore(deps): update all dependencies
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-02-25 01:42:42 +00:00
bb4e6ba6ab fix(deps): update rust crate uuid to v1.14.0
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2025-02-21 01:44:48 +00:00
66099d5bb7 fix(deps): update rust crate serde to v1.0.218
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-20 06:54:17 +01:00
817d1db963 chore(deps): update all dependencies
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2025-02-20 05:43:17 +00:00
4e5ed0c27f chore(deps): update all dependencies
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-02-18 01:43:20 +00:00
245aa67f09 chore(deps): update rust crate tempfile to v3.17.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-02-17 01:38:27 +00:00
43593c5852 fix(deps): update rust crate prost to v0.13.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-02-13 01:39:33 +00:00
657b11c3f7 chore(deps): update rust crate clap to v4.5.29
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-02-12 01:45:19 +00:00
8ce42aac25 chore(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-02-06 01:42:13 +00:00
ada480ea02 chore(deps): update rust crate clap to v4.5.28
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-02-04 01:43:06 +00:00
8a995b191c fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-29 01:45:42 +00:00
4897e56d7d chore(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-22 01:40:28 +00:00
c69ebb0fe4 fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-20 01:39:19 +00:00
0b71fd889e fix(deps): update rust crate serde_json to v1.0.136
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-19 01:40:37 +00:00
68f6ce63e5 fix(deps): update rust crate uuid to v1.12.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-15 01:45:30 +00:00
e7e169352d fix(deps): update all dependencies to v6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-13 01:38:50 +00:00
fc392ac43b fix(deps): update rust crate uuid to v1.11.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-10 05:34:27 +00:00
25f854f2fe chore(deps): update rust crate clap to v4.5.26
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-10 01:35:24 +00:00
eaaa63e0b6 chore(deps): update rust crate tokio to v1.43.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-09 01:37:46 +00:00
000f96f965 chore(deps): update rust crate clap to v4.5.24
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-08 01:33:50 +00:00
8870b89378 fix(deps): update rust crate serde_json to v1.0.135
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-07 01:36:16 +00:00
2704b0b0c3 fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-04 05:32:25 +00:00
39b05501f1 chore(deps): update rust crate axum to 0.8
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-02 01:33:14 +00:00
2d4ae16de8 chore(deps): update rust crate itertools to 0.14.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-01 01:34:55 +00:00
f036db202d fix(deps): update rust crate serde to v1.0.217
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-12-28 01:38:06 +00:00
384ee78652 chore(deps): update rust crate anyhow to v1.0.95
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-12-23 01:39:54 +00:00
f5ca5970c1 fix(deps): update rust crate serde_json to v1.0.134
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-12-22 01:33:09 +00:00
6a179f0881 chore(release): v0.5.0 (#62)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
chore(release): 0.5.0

Co-authored-by: cuddle-please <bot@cuddle.sh>
Reviewed-on: #62
2024-12-15 11:44:44 +01:00
09546907e5 feat: allow taking a local path
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-12-15 11:41:41 +01:00
83f9816cce fix(deps): update rust crate serde to v1.0.216
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-12-11 05:33:05 +00:00
c261d6cb65 fix(deps): update rust crate prost to v0.13.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-12-07 01:39:32 +00:00
fb01406738 chore(deps): update rust crate clap to v4.5.23
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-12-06 01:43:44 +00:00
352fd86145 chore(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-12-04 01:45:10 +00:00
bea5258e8f chore(deps): update rust crate tracing-subscriber to v0.3.19
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-30 02:15:38 +00:00
a0a256ac7f chore(deps): update rust crate tracing to v0.1.41
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-28 01:40:06 +00:00
9cd12f8636 chore(release): v0.4.0 (#15)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
chore(release): 0.4.0

Co-authored-by: cuddle-please <bot@cuddle.sh>
Reviewed-on: #15
2024-11-23 14:30:38 +01:00
34fba9754c feat: update hyperlog
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-11-23 14:26:11 +01:00
ee0680194b fix(deps): update rust crate tower-http to v0.6.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-19 01:28:57 +00:00
38f8db78cd chore(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-17 05:32:40 +00:00
02b8b8cd59 chore(deps): update rust crate axum to v0.7.8
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-16 01:38:26 +00:00
09fb11f9d9 chore(deps): update rust crate clap to v4.5.21
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-14 01:33:16 +00:00
102b35e083 fix(deps): update rust crate serde to v1.0.215
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-12 01:30:48 +00:00
986d261bd7 chore(deps): update rust crate tempfile to v3.14.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-08 05:30:54 +00:00
0f70cf9f7b chore(deps): update rust crate tokio to v1.41.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-08 01:32:34 +00:00
eb30858d9d chore(deps): update rust crate anyhow to v1.0.93
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-07 01:32:05 +00:00
22900ba92a chore(deps): update rust crate anyhow to v1.0.92
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-02 01:36:39 +00:00
44ef8a708c fix(deps): update rust crate serde to v1.0.214
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-29 01:39:13 +00:00
acc7e0cd6d fix(deps): update rust crate serde to v1.0.213
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-23 02:58:53 +02:00
e8d222f4ba chore(deps): update all dependencies to v1.0.91
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2024-10-23 00:40:51 +00:00
3c5fb25fa3 fix(deps): update rust crate ratatui to 0.29.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-22 00:32:43 +00:00
b3b170c057 fix(deps): update rust crate serde_json to v1.0.132
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-20 00:38:17 +00:00
e64fc61926 feat: add hyperlog
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-10-19 11:49:32 +02:00
4eb1a8224a chore(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-19 00:36:20 +00:00
6dfe2bf0d6 fix(deps): update rust crate serde_json to v1.0.129
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-18 00:36:43 +00:00
0e38e23942 fix(deps): update rust crate uuid to v1.11.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-17 00:37:43 +00:00
c35aad3cf8 fix(deps): update rust crate human-panic to v2.0.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-11 00:33:58 +00:00
10c2282c78 chore(deps): update rust crate clap to v4.5.20
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-09 00:35:54 +00:00
35c4fae36a chore(deps): update rust crate futures to v0.3.31
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-06 00:32:26 +00:00
b11d72ca05 chore(deps): update rust crate clap to v4.5.19
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-02 00:35:30 +00:00
a97ef32ffb chore(deps): update rust crate tempfile to v3.13.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-29 00:33:52 +00:00
2873ff3d7e chore(deps): update rust crate axum to v0.7.7
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-28 00:38:27 +00:00
4096790f2a chore(deps): update tonic monorepo to v0.12.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-27 00:31:35 +00:00
0ce0e691ee fix(deps): update rust crate tower-http to v0.6.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-24 00:36:53 +00:00
40dfbcd031 fix(deps): update rust crate prost to v0.13.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-22 00:40:54 +00:00
4ff3261dbc chore(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-21 00:58:02 +00:00
53c2cdf018 fix(deps): update rust crate tower-http to 0.6.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-20 00:42:29 +00:00
c96eebf6f9 chore(deps): update rust crate anyhow to v1.0.89
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-15 04:31:24 +00:00
9d9d6be3b7 chore(deps): update rust crate anyhow to v1.0.88
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-12 00:33:33 +00:00
f106929cca chore(deps): update rust crate anyhow to v1.0.87
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-07 04:29:21 +00:00
d876891242 fix(deps): update rust crate serde to v1.0.210
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-07 00:31:58 +00:00
6961987a77 chore(deps): update rust crate similar-asserts to v1.6.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-06 00:30:16 +00:00
d3695eba50 chore(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-05 00:32:33 +00:00
f175c4ebcf fix(deps): update rust crate sqlx to v0.8.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-04 00:27:32 +00:00
58df153c6e fix(deps): update rust crate prost to v0.13.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-31 00:28:05 +00:00
2b7a05bc4e chore(deps): update rust crate tokio to v1.40.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-30 08:27:22 +00:00
fcb0ea7393 chore(deps): update tonic monorepo to v0.12.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-26 20:42:40 +00:00
3a1741d7dc fix(deps): update rust crate ratatui to v0.28.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-25 09:51:50 +00:00
8886af4a8f fix(deps): update rust crate sqlx to v0.8.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-24 08:07:53 +00:00
08a6f77146 fix(deps): update rust crate serde to v1.0.209
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-24 03:54:43 +00:00
c15c7f0ae2 fix(deps): update rust crate serde_json to v1.0.127
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-23 21:19:27 +00:00
04c6e97f30 fix(deps): update rust crate serde_json to v1.0.126
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-23 20:40:42 +00:00
29e0c37599 fix(deps): update rust crate prost to 0.13.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-21 23:30:14 +00:00
e655c57f21 chore(deps): update tonic monorepo to 0.12.0
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2024-08-21 22:31:50 +00:00
28fb99e6f9 fix(deps): update rust crate serde to v1.0.208
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-21 23:22:49 +02:00
3a231cea96 chore(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-21 21:02:46 +00:00
b3c1784cae Merge pull request 'chore(release): v0.3.0' (#12) from cuddle-please/release into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #12
2024-07-05 23:10:26 +02:00
cuddle-please
a790e7f039 chore(release): 0.3.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-30 15:34:55 +00:00
20190ac784 feat: add markdown editing mode
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-06-30 17:31:25 +02:00
2df2412d39 Merge pull request 'chore(release): v0.2.0' (#11) from cuddle-please/release into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #11
2024-06-01 14:51:25 +02:00
615484d1cd chore(test): test commit
Some checks failed
continuous-integration/drone/push Build is failing
2024-06-01 14:50:35 +02:00
cuddle-please
530314c0f8 chore(release): 0.2.0
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2024-06-01 12:47:07 +00:00
67d4fe31b3 chore(test): test commit
Some checks failed
continuous-integration/drone/push Build is failing
2024-06-01 14:36:48 +02:00
d0d5efc3e3 chore(release): v0.2.0 (#4)
Some checks failed
continuous-integration/drone/push Build is failing
### Added
- enable creating items on the same level
- add command for quickly creating an item
- remove removal of spaces in title
- with toggle item
- with backend
- can get actual available roots
- can add items
- server can actually create root and sections
- abstract commander
- with async commands instead of inline mutations phew.
- add command pattern
- allow async function in command
- move core to tui and begin grpc work
- add protos
- update deps
- with basic server

### Fixed
- *(deps)* update rust crate serde to v1.0.203
- *(deps)* update rust crate prost to 0.12.6
- *(deps)* update rust crate prost to 0.12.5
- *(deps)* update rust crate serde to 1.0.202

### Other
- *(deps)* update all dependencies
- *(deps)* update rust crate itertools to 0.13.0
- move unused imports into cfg
- remove unused functions and fix warnings
- remove unused variables
- fix formatting
- refactor out graph created event
- let state use either local or backend
- remove warnings
- remove extra logs

Co-authored-by: cuddle-please <bot@cuddle.sh>
Reviewed-on: #4
2024-06-01 13:37:14 +02:00
9587c60e72 feat: add archive sub command
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-06-01 13:13:18 +02:00
710fb431f7 chore: change default to green
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-06-01 12:28:42 +02:00
be28b4ff80 feat: add colors
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-30 23:05:00 +02:00
73c6ba25b1 feat: add actual remote
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-30 22:24:01 +02:00
3f22148bfa feat: add grpc as well
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-29 21:37:43 +02:00
d773eff6fa chore(test): test commit
Some checks failed
continuous-integration/drone/push Build is failing
2024-05-29 20:18:07 +02:00
4859c2767c feat: add grpc
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-29 20:00:30 +02:00
e3bb6b0f60 feat: add more urls
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-29 19:58:55 +02:00
9159ce4eee feat: add hyperlog trace
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-29 19:55:57 +02:00
b6c3dd1a80 chore(test): test commit
Some checks failed
continuous-integration/drone/push Build is failing
2024-05-29 19:46:56 +02:00
119731e2a0 feat: make into service instead
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-29 19:43:12 +02:00
98feed2d71 feat: remove demo cast
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-29 19:37:44 +02:00
a70f1cd4b4 chore(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-25 19:15:42 +00:00
0927d36505 fix(deps): update rust crate serde to v1.0.203
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-25 18:41:46 +00:00
bc2b47a6c5 fix(deps): update rust crate prost to 0.12.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-17 16:23:23 +00:00
44d9ed2790 chore(deps): update rust crate itertools to 0.13.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-16 16:29:20 +00:00
118aeb3898 fix(deps): update rust crate prost to 0.12.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-16 14:31:21 +00:00
65c2466f97 feat: enable creating items on the same level
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-16 10:10:11 +02:00
832587b51d feat: add command for quickly creating an item
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-15 15:25:55 +02:00
4ad8120cb5 feat: remove removal of spaces in title
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-15 15:17:24 +02:00
1f0f526e38 chore: move unused imports into cfg
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-15 15:12:10 +02:00
91ad8eda01 chore: remove unused functions and fix warnings
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-15 15:10:35 +02:00
7496d0e964 chore: remove unused variables
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-15 15:07:49 +02:00
208b14583e chore: fix formatting
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-15 15:07:20 +02:00
4a91a564bf feat: with toggle item
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-15 15:06:16 +02:00
364f3992bb fix(deps): update rust crate serde to 1.0.202
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-15 08:07:52 +00:00
837caee5db feat: with backend
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-14 23:30:20 +02:00
816869e6f9 feat: can get actual available roots
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-14 20:58:38 +02:00
7bdf8393b1 feat: can add items
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-13 23:33:37 +02:00
699bac7159 feat: server can actually create root and sections
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-13 22:57:20 +02:00
76f1c87663 feat: abstract commander
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-12 22:24:37 +02:00
64d59e069f chore: refactor out graph created event
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-12 21:11:08 +02:00
9bb5bc9e87 feat: with async commands instead of inline mutations phew.
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-12 21:07:21 +02:00
2d63d3ad4c refactor: let state use either local or backend
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-12 15:54:03 +02:00
9cb3296cec chore: remove warnings
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-12 14:38:03 +02:00
874045dca8 chore: remove extra logs
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-12 14:35:35 +02:00
5548d8e36e feat: add command pattern
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-12 14:29:14 +02:00
cf26422673 feat: allow async function in command
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-12 12:58:54 +02:00
4a0fcd1bbb feat: move core to tui and begin grpc work
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-11 23:23:00 +02:00
86cba91b16 feat: add protos
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-11 16:23:52 +02:00
113c646334 feat: update deps
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-11 15:51:22 +02:00
6a147ba0d2 feat: with basic server
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-11 15:51:01 +02:00
75 changed files with 5223 additions and 1187 deletions

View File

@@ -1,2 +1,2 @@
kind: template
load: cuddle-rust-cli-plan.yaml
load: cuddle-rust-service-plan.yaml

View File

@@ -6,6 +6,121 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.5.0] - 2024-12-15
### Added
- allow taking a local path
### Fixed
- *(deps)* update rust crate serde to v1.0.216
- *(deps)* update rust crate prost to v0.13.4
### Other
- *(deps)* update rust crate clap to v4.5.23
- *(deps)* update all dependencies
- *(deps)* update rust crate tracing-subscriber to v0.3.19
- *(deps)* update rust crate tracing to v0.1.41
## [0.4.0] - 2024-11-23
### Added
- update hyperlog
- add hyperlog
### Fixed
- *(deps)* update rust crate tower-http to v0.6.2
- *(deps)* update rust crate serde to v1.0.215
- *(deps)* update rust crate serde to v1.0.214
- *(deps)* update rust crate serde to v1.0.213
- *(deps)* update rust crate ratatui to 0.29.0
- *(deps)* update rust crate serde_json to v1.0.132
- *(deps)* update rust crate serde_json to v1.0.129
- *(deps)* update rust crate uuid to v1.11.0
- *(deps)* update rust crate human-panic to v2.0.2
- *(deps)* update rust crate tower-http to v0.6.1
- *(deps)* update rust crate prost to v0.13.3
- *(deps)* update rust crate tower-http to 0.6.0
- *(deps)* update rust crate serde to v1.0.210
- *(deps)* update rust crate sqlx to v0.8.2
- *(deps)* update rust crate prost to v0.13.2
- *(deps)* update rust crate ratatui to v0.28.1
- *(deps)* update rust crate sqlx to v0.8.1
- *(deps)* update rust crate serde to v1.0.209
- *(deps)* update rust crate serde_json to v1.0.127
- *(deps)* update rust crate serde_json to v1.0.126
- *(deps)* update rust crate prost to 0.13.0
- *(deps)* update rust crate serde to v1.0.208
### Other
- *(deps)* update all dependencies
- *(deps)* update rust crate axum to v0.7.8
- *(deps)* update rust crate clap to v4.5.21
- *(deps)* update rust crate tempfile to v3.14.0
- *(deps)* update rust crate tokio to v1.41.1
- *(deps)* update rust crate anyhow to v1.0.93
- *(deps)* update rust crate anyhow to v1.0.92
- *(deps)* update all dependencies to v1.0.91
- *(deps)* update all dependencies
- *(deps)* update rust crate clap to v4.5.20
- *(deps)* update rust crate futures to v0.3.31
- *(deps)* update rust crate clap to v4.5.19
- *(deps)* update rust crate tempfile to v3.13.0
- *(deps)* update rust crate axum to v0.7.7
- *(deps)* update tonic monorepo to v0.12.3
- *(deps)* update all dependencies
- *(deps)* update rust crate anyhow to v1.0.89
- *(deps)* update rust crate anyhow to v1.0.88
- *(deps)* update rust crate anyhow to v1.0.87
- *(deps)* update rust crate similar-asserts to v1.6.0
- *(deps)* update all dependencies
- *(deps)* update rust crate tokio to v1.40.0
- *(deps)* update tonic monorepo to v0.12.2
- *(deps)* update tonic monorepo to 0.12.0
- *(deps)* update all dependencies
## [0.3.0] - 2024-06-30
### Added
- add markdown editing mode
## [0.2.0] - 2024-05-25
### Added
- enable creating items on the same level
- add command for quickly creating an item
- remove removal of spaces in title
- with toggle item
- with backend
- can get actual available roots
- can add items
- server can actually create root and sections
- abstract commander
- with async commands instead of inline mutations phew.
- add command pattern
- allow async function in command
- move core to tui and begin grpc work
- add protos
- update deps
- with basic server
### Fixed
- *(deps)* update rust crate serde to v1.0.203
- *(deps)* update rust crate prost to 0.12.6
- *(deps)* update rust crate prost to 0.12.5
- *(deps)* update rust crate serde to 1.0.202
### Other
- *(deps)* update all dependencies
- *(deps)* update rust crate itertools to 0.13.0
- move unused imports into cfg
- remove unused functions and fix warnings
- remove unused variables
- fix formatting
- refactor out graph created event
- let state use either local or backend
- remove warnings
- remove extra logs
## [0.1.0] - 2024-05-11
### Added

1684
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,8 @@ resolver = "2"
[workspace.dependencies]
hyperlog-core = { path = "crates/hyperlog-core" }
hyperlog-tui = { path = "crates/hyperlog-tui" }
hyperlog-server = { path = "crates/hyperlog-server" }
hyperlog-protos = { path = "crates/hyperlog-protos" }
anyhow = { version = "1" }
tokio = { version = "1", features = ["full"] }
@@ -12,9 +14,16 @@ tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
clap = { version = "4", features = ["derive", "env"] }
dotenv = { version = "0.15" }
axum = { version = "0.7" }
axum = { version = "0.8" }
serde = { version = "1.0.202", features = ["derive"] }
serde_json = "1.0.117"
itertools = "0.12.1"
itertools = "0.14.0"
uuid = { version = "1.8.0", features = ["v4"] }
tonic = { version = "0.12.0", features = ["tls", "tls-roots"] }
futures = { version = "0.3.30" }
sha2 = { version = "0.10.8" }
hex = { version = "0.4.3" }
toml = { version = "0.8.14" }
[workspace.package]
version = "0.1.0"
version = "0.5.0"

4
buf.yaml Normal file
View File

@@ -0,0 +1,4 @@
version: v2
modules:
- path: crates/hyperlog-protos/proto
name: buf.build/noschemaplz/hyperlog

View File

@@ -12,19 +12,9 @@ clap.workspace = true
dotenv.workspace = true
axum.workspace = true
serde = { version = "1.0.201", features = ["derive"] }
sqlx = { version = "0.7.4", features = [
"runtime-tokio",
"tls-rustls",
"postgres",
"uuid",
"time",
] }
serde = { version = "1.0.202", features = ["derive"] }
uuid = { version = "1.8.0", features = ["v4"] }
tower-http = { version = "0.5.2", features = ["cors", "trace"] }
serde_json = "1.0.117"
bus = "2.4.1"
dirs = "5.0.1"
[dev-dependencies]
similar-asserts = "1.5.0"

View File

@@ -1,11 +1 @@
#![feature(map_try_insert)]
pub mod commander;
pub mod querier;
pub mod engine;
pub mod events;
pub mod log;
pub mod shared_engine;
pub mod state;
pub mod storage;

View File

@@ -1,32 +0,0 @@
use crate::{
commander::Commander, events::Events, querier::Querier, shared_engine::SharedEngine,
storage::Storage,
};
#[allow(dead_code)]
pub struct State {
engine: SharedEngine,
pub storage: Storage,
events: Events,
pub commander: Commander,
pub querier: Querier,
}
impl State {
pub fn new() -> anyhow::Result<Self> {
let storage = Storage::new();
let engine = storage.load()?;
let events = Events::default();
let engine = SharedEngine::from(engine);
Ok(Self {
engine: engine.clone(),
storage: storage.clone(),
events: events.clone(),
commander: Commander::new(engine.clone(), storage, events)?,
querier: Querier::new(engine),
})
}
}

View File

@@ -0,0 +1,12 @@
[package]
name = "hyperlog-protos"
edition = "2021"
version.workspace = true
[dependencies]
tonic.workspace = true
prost = "0.13.0"
[build-dependencies]
tonic-build = "0.12.0"

View File

@@ -0,0 +1,3 @@
fn main() {
tonic_build::compile_protos("proto/hyperlog.proto").unwrap();
}

View File

@@ -0,0 +1,99 @@
syntax = "proto3";
package hyperlog;
message UserGraphItem {
map<string, GraphItem> items = 1;
}
message SectionGraphItem {
map<string, GraphItem> items = 1;
}
message ItemStateNotDone {}
message ItemStateDone {}
message ItemGraphItem {
string title = 1;
string description = 2;
oneof item_state {
ItemStateNotDone not_done = 3;
ItemStateDone done = 4;
}
}
message GraphItem {
oneof contents {
UserGraphItem user = 1;
SectionGraphItem section = 2;
ItemGraphItem item = 3;
}
}
service Graph {
// Commands
rpc CreateSection(CreateSectionRequest) returns (CreateSectionResponse);
rpc CreateRoot(CreateRootRequest) returns (CreateRootResponse);
rpc CreateItem(CreateItemRequest) returns (CreateItemResponse);
rpc UpdateItem(UpdateItemRequest) returns (UpdateItemResponse);
rpc ToggleItem(ToggleItemRequest) returns (ToggleItemResponse);
rpc Archive(ArchiveRequest) returns (ArchiveResponse);
// Queriers
rpc GetAvailableRoots(GetAvailableRootsRequest) returns (GetAvailableRootsResponse);
rpc Get(GetRequest) returns (GetReply);
}
// Commands
message CreateSectionRequest {
string root = 1;
repeated string path = 2;
}
message CreateSectionResponse {}
message CreateRootRequest {
string root = 1;
}
message CreateRootResponse {}
message CreateItemRequest {
string root = 1;
repeated string path = 2;
ItemGraphItem item = 3;
}
message CreateItemResponse {}
message UpdateItemRequest {
string root = 1;
repeated string path = 2;
ItemGraphItem item = 3;
}
message UpdateItemResponse {}
message ToggleItemRequest {
string root = 1;
repeated string path = 2;
}
message ToggleItemResponse {}
message ArchiveRequest {
string root = 1;
repeated string path = 2;
}
message ArchiveResponse {}
// Queries
message GetAvailableRootsRequest {}
message GetAvailableRootsResponse {
repeated string roots = 1;
}
message GetRequest {
string root = 1;
repeated string paths = 2;
}
message GetReply {
GraphItem item = 1;
}

View File

@@ -0,0 +1,3 @@
pub mod hyperlog {
tonic::include_proto!("hyperlog"); // Specify the same package name as in your .proto file
}

View File

@@ -0,0 +1,30 @@
[package]
name = "hyperlog-server"
version = "0.1.0"
edition = "2021"
[dependencies]
hyperlog-core.workspace = true
hyperlog-protos.workspace = true
anyhow.workspace = true
tokio.workspace = true
tracing.workspace = true
axum.workspace = true
serde.workspace = true
serde_json.workspace = true
uuid.workspace = true
tonic.workspace = true
tower-http = { version = "0.6.0", features = ["cors", "trace"] }
sqlx = { version = "0.8.0", features = [
"runtime-tokio",
"tls-rustls",
"postgres",
"uuid",
"time",
] }
[dev-dependencies]
similar-asserts = "1.5.0"
tempfile = "3.10.1"

View File

@@ -0,0 +1,17 @@
-- Add migration script here
CREATE TABLE roots (
id UUID NOT NULL PRIMARY KEY,
root_name VARCHAR(255) UNIQUE NOT NULL
);
CREATE TABLE nodes (
id UUID NOT NULL PRIMARY KEY,
root_id UUID NOT NULL,
path VARCHAR NOT NULL,
item_type VARCHAR NOT NULL,
item_content JSONB
);
CREATE UNIQUE INDEX idx_unique_root_path ON nodes(root_id, path);

View File

@@ -0,0 +1,3 @@
-- Add migration script here
ALTER TABLE nodes ADD COLUMN status VARCHAR(20) DEFAULT 'active' NOT NULL;

View File

@@ -0,0 +1,170 @@
use hyperlog_core::log::ItemState;
use crate::{
services::{
archive::{self, Archive, ArchiveExt},
create_item::{self, CreateItem, CreateItemExt},
create_root::{self, CreateRoot, CreateRootExt},
create_section::{self, CreateSection, CreateSectionExt},
toggle_item::{self, ToggleItem, ToggleItemExt},
update_item::{self, UpdateItem, UpdateItemExt},
},
state::SharedState,
};
#[allow(dead_code)]
pub enum Command {
CreateRoot {
root: String,
},
CreateSection {
root: String,
path: Vec<String>,
},
CreateItem {
root: String,
path: Vec<String>,
title: String,
description: String,
state: ItemState,
},
UpdateItem {
root: String,
path: Vec<String>,
title: String,
description: String,
state: ItemState,
},
ToggleItem {
root: String,
path: Vec<String>,
},
Move {
root: String,
src: Vec<String>,
dest: Vec<String>,
},
Archive {
root: String,
path: Vec<String>,
},
}
#[allow(dead_code)]
pub struct Commander {
create_root: CreateRoot,
create_section: CreateSection,
create_item: CreateItem,
update_item: UpdateItem,
toggle_item: ToggleItem,
archive: Archive,
}
impl Commander {
pub fn new(
create_root: CreateRoot,
create_section: CreateSection,
create_item: CreateItem,
update_item: UpdateItem,
toggle_item: ToggleItem,
archive: Archive,
) -> Self {
Self {
create_root,
create_section,
create_item,
update_item,
toggle_item,
archive,
}
}
pub async fn execute(&self, cmd: Command) -> anyhow::Result<()> {
match cmd {
Command::CreateRoot { root } => {
self.create_root
.execute(create_root::Request { root })
.await?;
Ok(())
}
Command::CreateSection { root, path } => {
self.create_section
.execute(create_section::Request { root, path })
.await?;
Ok(())
}
Command::CreateItem {
root,
path,
title,
description,
state,
} => {
self.create_item
.execute(create_item::Request {
root,
path,
title,
description,
state,
})
.await?;
Ok(())
}
Command::UpdateItem {
root,
path,
title,
description,
state,
} => {
self.update_item
.execute(update_item::Request {
root,
path,
title,
description,
state,
})
.await?;
Ok(())
}
Command::ToggleItem { root, path } => {
self.toggle_item
.execute(toggle_item::Request { root, path })
.await?;
Ok(())
}
Command::Move { .. } => todo!(),
Command::Archive { root, path } => {
self.archive
.execute(archive::Request { root, path })
.await?;
Ok(())
}
}
}
}
pub trait CommanderExt {
fn commander(&self) -> Commander;
}
impl CommanderExt for SharedState {
fn commander(&self) -> Commander {
Commander::new(
self.create_root_service(),
self.create_section_service(),
self.create_item_service(),
self.update_item_service(),
self.toggle_item_service(),
self.archive_service(),
)
}
}

View File

@@ -0,0 +1,486 @@
use hyperlog_protos::hyperlog::{
graph_server::{Graph, GraphServer},
*,
};
use std::{collections::HashMap, net::SocketAddr};
use tonic::{transport, Response};
use crate::{
commands::{Command, Commander, CommanderExt},
querier::{Querier, QuerierExt},
state::SharedState,
};
#[allow(dead_code)]
pub struct Server {
querier: Querier,
commander: Commander,
}
impl Server {
pub fn new(querier: Querier, commander: Commander) -> Self {
Self { querier, commander }
}
}
#[tonic::async_trait]
impl Graph for Server {
async fn create_item(
&self,
request: tonic::Request<CreateItemRequest>,
) -> std::result::Result<tonic::Response<CreateItemResponse>, tonic::Status> {
let req = request.into_inner();
tracing::trace!("create item: req({:?})", req);
if req.root.is_empty() {
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"root cannot be empty".to_string(),
));
}
if req.path.is_empty() {
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"path cannot be empty".to_string(),
));
}
if req
.path
.iter()
.filter(|item| item.is_empty())
.collect::<Vec<_>>()
.first()
.is_some()
{
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"path cannot contain empty paths".to_string(),
));
}
if req
.path
.iter()
.filter(|item| item.contains("."))
.collect::<Vec<_>>()
.first()
.is_some()
{
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"path cannot contain `.`".to_string(),
));
}
let item = match req.item {
Some(i) => i,
None => {
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"item cannot contain empty or null".to_string(),
));
}
};
self.commander
.execute(Command::CreateItem {
root: req.root,
path: req.path,
title: item.title,
description: item.description,
state: match item.item_state {
Some(item_graph_item::ItemState::Done(_)) => {
hyperlog_core::log::ItemState::Done
}
Some(item_graph_item::ItemState::NotDone(_)) => {
hyperlog_core::log::ItemState::NotDone
}
None => hyperlog_core::log::ItemState::default(),
},
})
.await
.map_err(to_tonic_err)?;
Ok(Response::new(CreateItemResponse {}))
}
async fn create_root(
&self,
request: tonic::Request<CreateRootRequest>,
) -> std::result::Result<tonic::Response<CreateRootResponse>, tonic::Status> {
let req = request.into_inner();
tracing::trace!("create root: req({:?})", req);
if req.root.is_empty() {
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"root cannot be empty".to_string(),
));
}
self.commander
.execute(Command::CreateRoot { root: req.root })
.await
.map_err(to_tonic_err)?;
Ok(Response::new(CreateRootResponse {}))
}
async fn create_section(
&self,
request: tonic::Request<CreateSectionRequest>,
) -> std::result::Result<tonic::Response<CreateSectionResponse>, tonic::Status> {
let req = request.into_inner();
tracing::trace!("create section: req({:?})", req);
if req.root.is_empty() {
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"root cannot be empty".to_string(),
));
}
if req.path.is_empty() {
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"path cannot be empty".to_string(),
));
}
if req
.path
.iter()
.filter(|item| item.is_empty())
.collect::<Vec<_>>()
.first()
.is_some()
{
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"path cannot contain empty paths".to_string(),
));
}
if req
.path
.iter()
.filter(|item| item.contains("."))
.collect::<Vec<_>>()
.first()
.is_some()
{
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"path cannot contain `.`".to_string(),
));
}
self.commander
.execute(Command::CreateSection {
root: req.root,
path: req.path,
})
.await
.map_err(to_tonic_err)?;
Ok(Response::new(CreateSectionResponse {}))
}
async fn get(
&self,
request: tonic::Request<GetRequest>,
) -> std::result::Result<tonic::Response<GetReply>, tonic::Status> {
let msg = request.get_ref();
tracing::trace!("get: req({:?})", msg);
let res = self
.querier
.get(&msg.root, msg.paths.clone())
.await
.map_err(to_tonic_err)?;
match res {
Some(item) => Ok(Response::new(GetReply {
item: Some(to_native(&item).map_err(to_tonic_err)?),
})),
None => {
return Err(tonic::Status::new(
tonic::Code::NotFound,
"failed to find any valid roots",
))
}
}
}
async fn get_available_roots(
&self,
request: tonic::Request<GetAvailableRootsRequest>,
) -> std::result::Result<tonic::Response<GetAvailableRootsResponse>, tonic::Status> {
let req = request.into_inner();
tracing::trace!("get available roots: req({:?})", req);
let roots = match self
.querier
.get_available_roots()
.await
.map_err(to_tonic_err)?
{
Some(roots) => roots,
None => {
return Err(tonic::Status::new(
tonic::Code::NotFound,
"failed to find any valid roots",
))
}
};
Ok(Response::new(GetAvailableRootsResponse { roots }))
}
async fn update_item(
&self,
request: tonic::Request<UpdateItemRequest>,
) -> std::result::Result<tonic::Response<UpdateItemResponse>, tonic::Status> {
let req = request.into_inner();
tracing::trace!("update item: req({:?})", req);
if req.root.is_empty() {
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"root cannot be empty".to_string(),
));
}
if req.path.is_empty() {
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"path cannot be empty".to_string(),
));
}
if req
.path
.iter()
.filter(|item| item.is_empty())
.collect::<Vec<_>>()
.first()
.is_some()
{
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"path cannot contain empty paths".to_string(),
));
}
if req
.path
.iter()
.filter(|item| item.contains("."))
.collect::<Vec<_>>()
.first()
.is_some()
{
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"path cannot contain `.`".to_string(),
));
}
let item = match req.item {
Some(i) => i,
None => {
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"item cannot contain empty or null".to_string(),
));
}
};
self.commander
.execute(Command::UpdateItem {
root: req.root,
path: req.path,
title: item.title,
description: item.description,
state: match item.item_state {
Some(item_graph_item::ItemState::Done(_)) => {
hyperlog_core::log::ItemState::Done
}
Some(item_graph_item::ItemState::NotDone(_)) => {
hyperlog_core::log::ItemState::NotDone
}
None => hyperlog_core::log::ItemState::default(),
},
})
.await
.map_err(to_tonic_err)?;
Ok(Response::new(UpdateItemResponse {}))
}
async fn toggle_item(
&self,
request: tonic::Request<ToggleItemRequest>,
) -> std::result::Result<tonic::Response<ToggleItemResponse>, tonic::Status> {
let req = request.into_inner();
tracing::trace!("update item: req({:?})", req);
if req.root.is_empty() {
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"root cannot be empty".to_string(),
));
}
if req.path.is_empty() {
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"path cannot be empty".to_string(),
));
}
if req
.path
.iter()
.filter(|item| item.is_empty())
.collect::<Vec<_>>()
.first()
.is_some()
{
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"path cannot contain empty paths".to_string(),
));
}
if req
.path
.iter()
.filter(|item| item.contains("."))
.collect::<Vec<_>>()
.first()
.is_some()
{
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"path cannot contain `.`".to_string(),
));
}
self.commander
.execute(Command::ToggleItem {
root: req.root,
path: req.path,
})
.await
.map_err(to_tonic_err)?;
Ok(Response::new(ToggleItemResponse {}))
}
async fn archive(
&self,
request: tonic::Request<ArchiveRequest>,
) -> std::result::Result<tonic::Response<ArchiveResponse>, tonic::Status> {
let req = request.into_inner();
tracing::trace!("update item: req({:?})", req);
if req.root.is_empty() {
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"root cannot be empty".to_string(),
));
}
if req.path.is_empty() {
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"path cannot be empty".to_string(),
));
}
self.commander
.execute(Command::Archive {
root: req.root,
path: req.path,
})
.await
.map_err(to_tonic_err)?;
Ok(Response::new(ArchiveResponse {}))
}
}
fn to_native(from: &hyperlog_core::log::GraphItem) -> anyhow::Result<GraphItem> {
match from {
hyperlog_core::log::GraphItem::User(section)
| hyperlog_core::log::GraphItem::Section(section) => {
let mut root = HashMap::new();
for (key, value) in section.iter() {
root.insert(key.to_string(), to_native(value)?);
}
match from {
hyperlog_core::log::GraphItem::User(_) => Ok(GraphItem {
contents: Some(graph_item::Contents::User(UserGraphItem { items: root })),
}),
hyperlog_core::log::GraphItem::Section(_) => Ok(GraphItem {
contents: Some(graph_item::Contents::Section(SectionGraphItem {
items: root,
})),
}),
_ => {
todo!()
}
}
}
hyperlog_core::log::GraphItem::Item {
title,
description,
state,
} => Ok(GraphItem {
contents: Some(graph_item::Contents::Item(ItemGraphItem {
title: title.to_owned(),
description: description.to_owned(),
item_state: Some(match state {
hyperlog_core::log::ItemState::NotDone => {
item_graph_item::ItemState::NotDone(ItemStateNotDone {})
}
hyperlog_core::log::ItemState::Done => {
item_graph_item::ItemState::Done(ItemStateDone {})
}
}),
})),
}),
}
}
// TODO: create more defined protobuf categories for errors
fn to_tonic_err(err: anyhow::Error) -> tonic::Status {
tonic::Status::new(tonic::Code::Unknown, err.to_string())
}
pub trait ServerExt {
fn grpc_server(&self) -> Server;
}
impl ServerExt for SharedState {
fn grpc_server(&self) -> Server {
Server::new(self.querier(), self.commander())
}
}
pub async fn serve(state: &SharedState, host: SocketAddr) -> anyhow::Result<()> {
tracing::info!("listening on {}", host);
let graph_server = state.grpc_server();
transport::Server::builder()
.add_service(GraphServer::new(graph_server))
.serve(host)
.await?;
Ok(())
}

View File

@@ -0,0 +1,40 @@
use std::net::SocketAddr;
use axum::{extract::MatchedPath, http::Request, routing::get, Router};
use tower_http::trace::TraceLayer;
use crate::state::SharedState;
async fn root() -> &'static str {
"Hello, hyperlog!"
}
pub async fn serve(state: &SharedState, host: &SocketAddr) -> anyhow::Result<()> {
let app = Router::new()
.route("/", get(root))
.with_state(state.clone())
.layer(
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
// Log the matched route's path (with placeholders not filled in).
// Use request.uri() or OriginalUri if you want the real path.
let matched_path = request
.extensions()
.get::<MatchedPath>()
.map(MatchedPath::as_str);
tracing::info_span!(
"http_request",
method = ?request.method(),
matched_path,
some_other_field = tracing::field::Empty,
)
}), // ...
);
tracing::info!("listening on {}", host);
let listener = tokio::net::TcpListener::bind(host).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
Ok(())
}

View File

@@ -0,0 +1,40 @@
use std::net::SocketAddr;
use axum::{extract::MatchedPath, http::Request, routing::get, Router};
use tower_http::trace::TraceLayer;
use crate::state::SharedState;
async fn root() -> &'static str {
"Hello, hyperlog!"
}
pub async fn serve(state: &SharedState, host: &SocketAddr) -> anyhow::Result<()> {
let app = Router::new()
.route("/", get(root))
.with_state(state.clone())
.layer(
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
// Log the matched route's path (with placeholders not filled in).
// Use request.uri() or OriginalUri if you want the real path.
let matched_path = request
.extensions()
.get::<MatchedPath>()
.map(MatchedPath::as_str);
tracing::info_span!(
"http_request",
method = ?request.method(),
matched_path,
some_other_field = tracing::field::Empty,
)
}), // ...
);
tracing::info!("listening on {}", host);
let listener = tokio::net::TcpListener::bind(host).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
Ok(())
}

View File

@@ -0,0 +1,50 @@
#![feature(map_try_insert)]
use std::{net::SocketAddr, sync::Arc};
use crate::state::{SharedState, State};
mod external_grpc;
mod external_http;
mod internal_http;
mod commands;
mod querier;
mod state;
mod services;
#[derive(Clone)]
pub struct ServeOptions {
pub external_http: SocketAddr,
pub internal_http: SocketAddr,
pub external_grpc: SocketAddr,
}
pub async fn serve(opts: ServeOptions) -> anyhow::Result<()> {
let ctrl_c = async {
tokio::signal::ctrl_c().await.unwrap();
tracing::info!("kill signal received, shutting down");
};
tracing::debug!("setting up dependencies");
let state = SharedState(Arc::new(State::new().await?));
tracing::debug!("serve starting");
tokio::select!(
res = external_http::serve(&state, &opts.external_http) => {
res?
},
res = internal_http::serve(&state, &opts.internal_http) => {
res?
},
res = external_grpc::serve(&state, opts.external_grpc) => {
res?
}
() = ctrl_c => {}
);
tracing::debug!("serve finalized");
Ok(())
}

View File

@@ -0,0 +1,62 @@
use hyperlog_core::log::GraphItem;
use crate::{
services::{
get_available_roots::{self, GetAvailableRoots, GetAvailableRootsExt},
get_graph::{GetGraph, GetGraphExt},
},
state::SharedState,
};
pub struct Querier {
get_available_roots: GetAvailableRoots,
get_graph: GetGraph,
}
impl Querier {
pub fn new(get_available_roots: GetAvailableRoots, get_graph: GetGraph) -> Self {
Self {
get_available_roots,
get_graph,
}
}
pub async fn get_available_roots(&self) -> anyhow::Result<Option<Vec<String>>> {
let res = self
.get_available_roots
.execute(get_available_roots::Request {})
.await?;
if res.roots.is_empty() {
return Ok(None);
}
Ok(Some(res.roots))
}
pub async fn get(
&self,
root: &str,
path: impl IntoIterator<Item = impl Into<String>>,
) -> anyhow::Result<Option<GraphItem>> {
let graph = self
.get_graph
.execute(crate::services::get_graph::Request {
root: root.into(),
path: path.into_iter().map(|s| s.into()).collect(),
})
.await?;
Ok(Some(graph.item))
}
}
pub trait QuerierExt {
fn querier(&self) -> Querier;
}
impl QuerierExt for SharedState {
fn querier(&self) -> Querier {
Querier::new(self.get_available_roots_service(), self.get_graph_service())
}
}

View File

@@ -0,0 +1,9 @@
pub mod archive;
pub mod create_item;
pub mod create_root;
pub mod create_section;
pub mod toggle_item;
pub mod update_item;
pub mod get_available_roots;
pub mod get_graph;

View File

@@ -0,0 +1,70 @@
use crate::state::SharedState;
#[derive(Clone)]
pub struct Archive {
db: sqlx::PgPool,
}
pub struct Request {
pub root: String,
pub path: Vec<String>,
}
pub struct Response {}
#[derive(sqlx::FromRow)]
struct Root {
id: uuid::Uuid,
}
impl Archive {
pub fn new(db: sqlx::PgPool) -> Self {
Self { db }
}
pub async fn execute(&self, req: Request) -> anyhow::Result<Response> {
let Root { id: root_id, .. } =
sqlx::query_as(r#"SELECT * FROM roots WHERE root_name = $1"#)
.bind(req.root)
.fetch_one(&self.db)
.await?;
sqlx::query(
r#"
UPDATE nodes
SET status = 'archive'
WHERE
root_id = $1
AND path = $2;
"#,
)
.bind(root_id)
.bind(req.path.join("."))
.execute(&self.db)
.await?;
sqlx::query(
r#"
UPDATE nodes
SET status = 'archive'
WHERE root_id = $1
AND path LIKE $2;
"#,
)
.bind(root_id)
.bind(format!("{}.%", req.path.join(".")))
.execute(&self.db)
.await?;
Ok(Response {})
}
}
pub trait ArchiveExt {
fn archive_service(&self) -> Archive;
}
impl ArchiveExt for SharedState {
fn archive_service(&self) -> Archive {
Archive::new(self.db.clone())
}
}

View File

@@ -0,0 +1,104 @@
use hyperlog_core::log::ItemState;
use sqlx::types::Json;
use crate::state::SharedState;
#[derive(Clone)]
pub struct CreateItem {
db: sqlx::PgPool,
}
pub struct Request {
pub root: String,
pub path: Vec<String>,
pub title: String,
pub description: String,
pub state: ItemState,
}
pub struct Response {}
#[derive(serde::Serialize)]
struct ItemContent {
pub title: String,
pub description: String,
pub state: ItemState,
}
#[derive(sqlx::FromRow)]
struct Root {
id: uuid::Uuid,
}
#[derive(sqlx::FromRow)]
struct Section {}
impl CreateItem {
pub fn new(db: sqlx::PgPool) -> Self {
Self { db }
}
pub async fn execute(&self, req: Request) -> anyhow::Result<Response> {
let Root { id: root_id, .. } =
sqlx::query_as(r#"SELECT * FROM roots WHERE root_name = $1"#)
.bind(req.root)
.fetch_one(&self.db)
.await?;
match req.path.split_last() {
Some((_, section_path)) => {
if !section_path.is_empty() {
let Section { .. } = sqlx::query_as(
r#"
SELECT
*
FROM
nodes
WHERE
root_id = $1 AND
path = $2 AND
item_type = 'SECTION'
"#,
)
.bind(root_id)
.bind(section_path.join("."))
.fetch_one(&self.db)
.await?;
}
let node_id = uuid::Uuid::new_v4();
sqlx::query(
r#"
INSERT INTO nodes
(id, root_id, path, item_type, item_content)
VALUES
($1, $2, $3, $4, $5)"#,
)
.bind(node_id)
.bind(root_id)
.bind(req.path.join("."))
.bind("ITEM".to_string())
.bind(Json(ItemContent {
title: req.title,
description: req.description,
state: req.state,
}))
.execute(&self.db)
.await?;
}
None => anyhow::bail!("path most contain at least one item"),
}
Ok(Response {})
}
}
pub trait CreateItemExt {
fn create_item_service(&self) -> CreateItem;
}
impl CreateItemExt for SharedState {
fn create_item_service(&self) -> CreateItem {
CreateItem::new(self.db.clone())
}
}

View File

@@ -0,0 +1,38 @@
use crate::state::SharedState;
#[derive(Clone)]
pub struct CreateRoot {
db: sqlx::PgPool,
}
pub struct Request {
pub root: String,
}
pub struct Response {}
impl CreateRoot {
pub fn new(db: sqlx::PgPool) -> Self {
Self { db }
}
pub async fn execute(&self, req: Request) -> anyhow::Result<Response> {
let root_id = uuid::Uuid::new_v4();
sqlx::query(r#"INSERT INTO roots (id, root_name) VALUES ($1, $2)"#)
.bind(root_id)
.bind(req.root)
.execute(&self.db)
.await?;
Ok(Response {})
}
}
pub trait CreateRootExt {
fn create_root_service(&self) -> CreateRoot;
}
impl CreateRootExt for SharedState {
fn create_root_service(&self) -> CreateRoot {
CreateRoot::new(self.db.clone())
}
}

View File

@@ -0,0 +1,61 @@
use crate::state::SharedState;
#[derive(Clone)]
pub struct CreateSection {
db: sqlx::PgPool,
}
pub struct Request {
pub root: String,
pub path: Vec<String>,
}
pub struct Response {}
#[derive(sqlx::FromRow)]
struct Root {
id: uuid::Uuid,
}
impl CreateSection {
pub fn new(db: sqlx::PgPool) -> Self {
Self { db }
}
pub async fn execute(&self, req: Request) -> anyhow::Result<Response> {
let Root { id: root_id, .. } =
sqlx::query_as(r#"SELECT * FROM roots WHERE root_name = $1"#)
.bind(req.root)
.fetch_one(&self.db)
.await?;
// FIXME: implement consistency check on path
let node_id = uuid::Uuid::new_v4();
sqlx::query(
r#"
INSERT INTO nodes
(id, root_id, path, item_type, item_content)
VALUES
($1, $2, $3, $4, $5)"#,
)
.bind(node_id)
.bind(root_id)
.bind(req.path.join("."))
.bind("SECTION".to_string())
.bind(None::<serde_json::Value>)
.execute(&self.db)
.await?;
Ok(Response {})
}
}
pub trait CreateSectionExt {
fn create_section_service(&self) -> CreateSection;
}
impl CreateSectionExt for SharedState {
fn create_section_service(&self) -> CreateSection {
CreateSection::new(self.db.clone())
}
}

View File

@@ -0,0 +1,51 @@
use crate::state::SharedState;
#[derive(Clone)]
pub struct GetAvailableRoots {
db: sqlx::PgPool,
}
pub struct Request {}
pub struct Response {
pub roots: Vec<String>,
}
#[derive(sqlx::FromRow)]
pub struct Root {
root_name: String,
}
impl GetAvailableRoots {
pub fn new(db: sqlx::PgPool) -> Self {
Self { db }
}
pub async fn execute(&self, _req: Request) -> anyhow::Result<Response> {
let roots: Vec<Root> = sqlx::query_as(
r#"
SELECT
*
FROM
roots
LIMIT
100
"#,
)
.fetch_all(&self.db)
.await?;
Ok(Response {
roots: roots.into_iter().map(|i| i.root_name).collect(),
})
}
}
pub trait GetAvailableRootsExt {
fn get_available_roots_service(&self) -> GetAvailableRoots;
}
impl GetAvailableRootsExt for SharedState {
fn get_available_roots_service(&self) -> GetAvailableRoots {
GetAvailableRoots::new(self.db.clone())
}
}

View File

@@ -0,0 +1,369 @@
use std::collections::BTreeMap;
use hyperlog_core::log::{GraphItem, ItemState};
use serde::Deserialize;
use sqlx::types::Json;
use crate::state::SharedState;
use self::engine::Engine;
#[derive(Clone)]
pub struct GetGraph {
db: sqlx::PgPool,
}
pub struct Request {
pub root: String,
pub path: Vec<String>,
}
pub struct Response {
pub item: GraphItem,
}
#[derive(sqlx::FromRow)]
struct Root {
id: uuid::Uuid,
}
#[derive(Deserialize)]
struct Item {
title: String,
description: String,
state: ItemState,
}
#[derive(sqlx::FromRow, Debug)]
struct Node {
path: String,
item_type: String,
item_content: Option<Json<serde_json::Value>>,
}
impl GetGraph {
pub fn new(db: sqlx::PgPool) -> Self {
Self { db }
}
pub async fn execute(&self, req: Request) -> anyhow::Result<Response> {
let Root { id: root_id, .. } =
sqlx::query_as(r#"SELECT * FROM roots WHERE root_name = $1"#)
.bind(&req.root)
.fetch_one(&self.db)
.await?;
let nodes: Vec<Node> = sqlx::query_as(
r#"
SELECT
*
FROM
nodes
WHERE
root_id = $1
AND status = 'active'
LIMIT
1000
"#,
)
.bind(root_id)
.fetch_all(&self.db)
.await?;
let item = self.build_graph(req.root, req.path, nodes)?;
Ok(Response { item })
}
fn build_graph(
&self,
root: String,
path: Vec<String>,
mut nodes: Vec<Node>,
) -> anyhow::Result<GraphItem> {
nodes.sort_by(|a, b| a.path.cmp(&b.path));
let mut engine = Engine::default();
engine.create_root(&root)?;
self.get_graph_items(&root, &mut engine, &nodes)?;
engine
.get(&root, &path.iter().map(|s| s.as_str()).collect::<Vec<_>>())
.ok_or(anyhow::anyhow!("failed to find a valid graph"))
.cloned()
}
fn get_graph_items(
&self,
root: &str,
engine: &mut Engine,
nodes: &Vec<Node>,
) -> anyhow::Result<()> {
for node in nodes {
if let Some(item) = self.get_graph_item(node) {
let path = node.path.split('.').collect::<Vec<_>>();
engine.create(root, &path, item)?;
}
}
Ok(())
}
fn get_graph_item(&self, node: &Node) -> Option<GraphItem> {
match node.item_type.as_str() {
"SECTION" => Some(GraphItem::Section(BTreeMap::default())),
"ITEM" => {
if let Some(content) = &node.item_content {
let item: Item = serde_json::from_value(content.0.clone()).ok()?;
Some(GraphItem::Item {
title: item.title,
description: item.description,
state: item.state,
})
} else {
None
}
}
_ => None,
}
}
}
pub trait GetGraphExt {
fn get_graph_service(&self) -> GetGraph;
}
impl GetGraphExt for SharedState {
fn get_graph_service(&self) -> GetGraph {
GetGraph::new(self.db.clone())
}
}
mod engine {
use std::{collections::BTreeMap, fmt::Display};
use anyhow::{anyhow, Context};
use hyperlog_core::log::{Graph, GraphItem, ItemState};
#[derive(Default)]
pub struct Engine {
graph: Graph,
}
impl Engine {
#[allow(dead_code)]
pub fn engine_from_str(input: &str) -> anyhow::Result<Self> {
let graph: Graph = serde_json::from_str(input)?;
Ok(Self { graph })
}
#[allow(dead_code)]
pub fn to_str(&self) -> anyhow::Result<String> {
serde_json::to_string_pretty(&self.graph).context("failed to serialize graph")
}
pub fn create_root(&mut self, root: &str) -> anyhow::Result<()> {
self.graph
.try_insert(root.to_string(), GraphItem::User(BTreeMap::default()))
.map_err(|_| anyhow!("entry was already found, aborting"))?;
Ok(())
}
pub fn create(&mut self, root: &str, path: &[&str], item: GraphItem) -> anyhow::Result<()> {
let graph = &mut self.graph;
let (last, items) = path.split_last().ok_or(anyhow!(
"path cannot be empty, must contain at least one item"
))?;
let root = graph
.get_mut(root)
.ok_or(anyhow!("root was missing a user, aborting"))?;
let mut current_item = root;
for section in items {
match current_item {
GraphItem::User(u) => match u.get_mut(section.to_owned()) {
Some(graph_item) => {
current_item = graph_item;
}
None => anyhow::bail!("path: {} section was not found", section),
},
GraphItem::Item { .. } => anyhow::bail!("path: {} was already found", section),
GraphItem::Section(s) => match s.get_mut(section.to_owned()) {
Some(graph_item) => {
current_item = graph_item;
}
None => anyhow::bail!("path: {} section was not found", section),
},
}
}
match current_item {
GraphItem::User(u) => {
u.insert(last.to_string(), item);
}
GraphItem::Section(s) => {
s.insert(last.to_string(), item);
}
GraphItem::Item { .. } => anyhow::bail!("cannot insert an item into an item"),
}
Ok(())
}
pub fn get(&self, root: &str, path: &[&str]) -> Option<&GraphItem> {
let root = self.graph.get(root)?;
root.get(path)
}
#[allow(dead_code)]
pub fn get_mut(&mut self, root: &str, path: &[&str]) -> Option<&mut GraphItem> {
let root = self.graph.get_mut(root)?;
root.get_mut(path)
}
#[allow(dead_code)]
pub fn take(&mut self, root: &str, path: &[&str]) -> Option<GraphItem> {
let root = self.graph.get_mut(root)?;
root.take(path)
}
#[allow(dead_code)]
pub fn section_move(
&mut self,
root: &str,
src_path: &[&str],
dest_path: &[&str],
) -> anyhow::Result<()> {
let src = self
.take(root, src_path)
.ok_or(anyhow!("failed to find source path"))?;
let dest = self
.get_mut(root, dest_path)
.ok_or(anyhow!("failed to find destination"))?;
let src_item = src_path
.last()
.ok_or(anyhow!("src path must have at least one item"))?;
match dest {
GraphItem::User(u) => {
u.try_insert(src_item.to_string(), src)
.map_err(|_e| anyhow!("key was already found, aborting: {}", src_item))?;
}
GraphItem::Section(s) => {
s.try_insert(src_item.to_string(), src)
.map_err(|_e| anyhow!("key was already found, aborting: {}", src_item))?;
}
GraphItem::Item { .. } => {
anyhow::bail!(
"failed to insert src at item, item doesn't support arbitrary items"
)
}
}
Ok(())
}
#[allow(dead_code)]
pub fn delete(&mut self, root: &str, path: &[&str]) -> anyhow::Result<()> {
self.take(root, path)
.map(|_| ())
.ok_or(anyhow!("item was not found"))
}
#[allow(dead_code)]
pub fn toggle_item(&mut self, root: &str, path: &[&str]) -> anyhow::Result<()> {
if let Some(item) = self.get_mut(root, path) {
match item {
GraphItem::Item { state, .. } => match state {
ItemState::NotDone => *state = ItemState::Done,
ItemState::Done => *state = ItemState::NotDone,
},
_ => {
anyhow::bail!("{}.{:?} is not an item", root, path)
}
}
}
Ok(())
}
#[allow(dead_code)]
pub fn update_item(
&mut self,
root: &str,
path: &[&str],
item: &GraphItem,
) -> anyhow::Result<()> {
if let Some((name, dest_last)) = path.split_last() {
if let Some(parent) = self.get_mut(root, dest_last) {
match parent {
GraphItem::User(s) | GraphItem::Section(s) => {
if let Some(mut existing) = s.remove(*name) {
match (&mut existing, item) {
(
GraphItem::Item {
title: ex_title,
description: ex_desc,
state: ex_state,
},
GraphItem::Item {
title,
description,
state,
},
) => {
ex_title.clone_from(title);
ex_desc.clone_from(description);
ex_state.clone_from(state);
let title = title.replace(".", "-");
s.insert(title, existing.clone());
}
_ => {
anyhow::bail!(
"path: {}.{} found is not an item",
root,
path.join(".")
)
}
}
}
}
GraphItem::Item { .. } => {
anyhow::bail!("cannot rename when item is placed in an item")
}
}
}
}
Ok(())
}
#[allow(dead_code)]
pub fn get_roots(&self) -> Option<Vec<String>> {
let items = self.graph.keys().cloned().collect::<Vec<_>>();
if items.is_empty() {
None
} else {
Some(items)
}
}
}
impl Display for Engine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let output = serde_json::to_string_pretty(&self.graph).unwrap();
f.write_str(&output)
}
}
}

View File

@@ -0,0 +1,105 @@
use hyperlog_core::log::ItemState;
use sqlx::types::Json;
use crate::state::SharedState;
#[derive(Clone)]
pub struct ToggleItem {
db: sqlx::PgPool,
}
pub struct Request {
pub root: String,
pub path: Vec<String>,
}
pub struct Response {}
#[derive(serde::Serialize, serde::Deserialize)]
struct ItemContent {
pub title: String,
pub description: String,
pub state: ItemState,
}
#[derive(sqlx::FromRow)]
struct Root {
id: uuid::Uuid,
}
#[derive(sqlx::FromRow)]
struct Node {
id: uuid::Uuid,
item_content: Option<Json<ItemContent>>,
}
impl ToggleItem {
pub fn new(db: sqlx::PgPool) -> Self {
Self { db }
}
pub async fn execute(&self, req: Request) -> anyhow::Result<Response> {
let Root { id: root_id, .. } =
sqlx::query_as(r#"SELECT * FROM roots WHERE root_name = $1"#)
.bind(req.root)
.fetch_one(&self.db)
.await?;
let Node {
id: node_id,
mut item_content,
} = sqlx::query_as(
r#"
SELECT
*
FROM
nodes
WHERE
root_id = $1
AND path = $2
AND item_type = $3
"#,
)
.bind(root_id)
.bind(req.path.join("."))
.bind("ITEM")
.fetch_one(&self.db)
.await?;
if let Some(ref mut content) = item_content {
content.state = match content.state {
ItemState::NotDone => ItemState::Done,
ItemState::Done => ItemState::NotDone,
}
}
let res = sqlx::query(
r#"
UPDATE
nodes
SET
item_content = $1
WHERE
id = $2
"#,
)
.bind(item_content)
.bind(node_id)
.execute(&self.db)
.await?;
if res.rows_affected() != 1 {
anyhow::bail!("failed to update item");
}
Ok(Response {})
}
}
pub trait ToggleItemExt {
fn toggle_item_service(&self) -> ToggleItem;
}
impl ToggleItemExt for SharedState {
fn toggle_item_service(&self) -> ToggleItem {
ToggleItem::new(self.db.clone())
}
}

View File

@@ -0,0 +1,107 @@
use hyperlog_core::log::ItemState;
use sqlx::types::Json;
use crate::state::SharedState;
#[derive(Clone)]
pub struct UpdateItem {
db: sqlx::PgPool,
}
pub struct Request {
pub root: String,
pub path: Vec<String>,
pub title: String,
pub description: String,
pub state: ItemState,
}
pub struct Response {}
#[derive(serde::Serialize)]
struct ItemContent {
pub title: String,
pub description: String,
pub state: ItemState,
}
#[derive(sqlx::FromRow)]
struct Root {
id: uuid::Uuid,
}
impl UpdateItem {
pub fn new(db: sqlx::PgPool) -> Self {
Self { db }
}
pub async fn execute(&self, req: Request) -> anyhow::Result<Response> {
let Root { id: root_id, .. } =
sqlx::query_as(r#"SELECT * FROM roots WHERE root_name = $1"#)
.bind(req.root)
.fetch_one(&self.db)
.await?;
let Root { id: node_id } = sqlx::query_as(
r#"
SELECT
id
FROM
nodes
WHERE
root_id = $1
AND path = $2
AND item_type = $3
"#,
)
.bind(root_id)
.bind(req.path.join("."))
.bind("ITEM")
.fetch_one(&self.db)
.await?;
let (_, rest) = req
.path
.split_last()
.ok_or(anyhow::anyhow!("expected path to have at least one item"))?;
let mut rest = rest.to_vec();
rest.push(req.title.replace(".", "-"));
let res = sqlx::query(
r#"
UPDATE
nodes
SET
item_content = $1,
path = $2
WHERE
id = $3
"#,
)
.bind(Json(ItemContent {
title: req.title,
description: req.description,
state: req.state,
}))
.bind(rest.join("."))
.bind(node_id)
.execute(&self.db)
.await?;
if res.rows_affected() != 1 {
anyhow::bail!("failed to update item");
}
Ok(Response {})
}
}
pub trait UpdateItemExt {
fn update_item_service(&self) -> UpdateItem;
}
impl UpdateItemExt for SharedState {
fn update_item_service(&self) -> UpdateItem {
UpdateItem::new(self.db.clone())
}
}

View File

@@ -0,0 +1,37 @@
use std::{ops::Deref, sync::Arc};
use anyhow::Context;
use sqlx::{Pool, Postgres};
#[derive(Clone)]
pub struct SharedState(pub Arc<State>);
impl Deref for SharedState {
type Target = Arc<State>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub struct State {
pub db: Pool<Postgres>,
}
impl State {
pub async fn new() -> anyhow::Result<Self> {
let db = sqlx::PgPool::connect(
&std::env::var("DATABASE_URL").context("DATABASE_URL is not set")?,
)
.await?;
sqlx::migrate!("migrations/crdb")
.set_locking(false)
.run(&db)
.await?;
let _ = sqlx::query("SELECT 1;").fetch_one(&db).await?;
Ok(Self { db })
}
}

View File

@@ -6,19 +6,30 @@ repository = "https://git.front.kjuulh.io/kjuulh/hyperlog"
[dependencies]
hyperlog-core.workspace = true
hyperlog-protos.workspace = true
anyhow.workspace = true
tokio.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
serde.workspace = true
serde_json.workspace = true
itertools.workspace = true
tonic.workspace = true
futures.workspace = true
sha2.workspace = true
uuid.workspace = true
hex.workspace = true
toml.workspace = true
ratatui = "0.26.2"
crossterm = { version = "0.27.0", features = ["event-stream"] }
directories = "5.0.1"
ratatui = "0.29.0"
crossterm = { version = "0.29.0", features = ["event-stream"] }
directories = "6.0.0"
human-panic = "2.0.0"
ropey = "1.6.1"
bus = "2.4.1"
dirs = "6.0.0"
[dev-dependencies]
similar-asserts = "1.5.0"
tempfile = "3.10.1"

View File

@@ -1,12 +1,18 @@
use hyperlog_core::log::GraphItem;
use itertools::Itertools;
use ratatui::{
prelude::*,
widgets::{Block, Borders, Padding, Paragraph},
};
use crate::{
command_parser::CommandParser, commands::IntoCommand,
components::graph_explorer::GraphExplorer, state::SharedState, Msg,
command_parser::CommandParser,
commands::{batch::BatchCommand, update_item::UpdateItemCommandExt, Command, IntoCommand},
components::graph_explorer::GraphExplorer,
editor,
models::IOEvent,
state::SharedState,
Msg,
};
use self::{
@@ -26,10 +32,10 @@ pub enum Dialog {
}
impl Dialog {
pub fn get_command(&self) -> Option<hyperlog_core::commander::Command> {
pub fn get_command(&self) -> Option<impl IntoCommand> {
match self {
Dialog::CreateItem { state } => state.get_command(),
Dialog::EditItem { state } => state.get_command(),
Dialog::CreateItem { state } => state.get_command().map(|c| c.into_command()),
Dialog::EditItem { state } => state.get_command().map(|c| c.into_command()),
}
}
}
@@ -82,12 +88,27 @@ impl<'a> App<'a> {
pub fn update(&mut self, msg: Msg) -> anyhow::Result<impl IntoCommand> {
tracing::trace!("handling msg: {:?}", msg);
let mut batch = BatchCommand::default();
match &msg {
Msg::ItemCreated(IOEvent::Success(()))
| Msg::ItemUpdated(IOEvent::Success(()))
| Msg::SectionCreated(IOEvent::Success(()))
| Msg::ItemToggled(IOEvent::Success(()))
| Msg::Archive(IOEvent::Success(())) => {
batch.with(self.graph_explorer.new_update_graph());
}
Msg::MoveRight => self.graph_explorer.move_right()?,
Msg::MoveLeft => self.graph_explorer.move_left()?,
Msg::MoveDown => self.graph_explorer.move_down()?,
Msg::MoveUp => self.graph_explorer.move_up()?,
Msg::OpenCreateItemDialog => self.open_dialog(),
Msg::OpenCreateItemDialogBelow => self.open_dialog_below(),
Msg::OpenEditor { item } => {
if let Some(cmd) = self.open_editor(item) {
batch.with(cmd);
}
}
Msg::OpenEditItemDialog { item } => self.open_edit_item_dialog(item),
Msg::EnterInsertMode => self.mode = Mode::Insert,
Msg::EnterViewMode => self.mode = Mode::View,
@@ -97,7 +118,10 @@ impl<'a> App<'a> {
}
Msg::Interact => match self.focus {
AppFocus::Dialog => {}
AppFocus::Graph => self.graph_explorer.interact()?,
AppFocus::Graph => {
let cmd = self.graph_explorer.interact()?;
batch.with(cmd);
}
},
Msg::SubmitCommand { command } => {
tracing::info!("submitting command");
@@ -108,11 +132,9 @@ impl<'a> App<'a> {
if command.is_write() {
if let Some(dialog) = &self.dialog {
if let Some(output) = dialog.get_command() {
self.state.commander.execute(output)?;
batch.with(output.into_command());
}
}
self.graph_explorer.update_graph()?;
}
if command.is_quit() {
@@ -121,26 +143,31 @@ impl<'a> App<'a> {
}
}
AppFocus::Graph => {
if let Some(msg) = self.graph_explorer.execute_command(&command)? {
if let Some(cmd) = self.graph_explorer.execute_command(&command)? {
self.command = None;
return Ok(msg.into_command());
batch.with(cmd);
}
if command.is_quit() {
return Ok(Msg::QuitApp.into_command());
batch.with(Msg::QuitApp.into_command());
}
}
}
}
self.command = None;
return Ok(Msg::EnterViewMode.into_command());
batch.with(Msg::EnterViewMode.into_command());
}
_ => {}
}
let cmd = self.graph_explorer.inner.update(&msg);
if let Some(cmd) = cmd {
batch.with(cmd);
}
if let Some(command) = &mut self.command {
let cmd = command.update(&msg)?;
return Ok(cmd.into_command());
batch.with(cmd);
} else if let Some(dialog) = &mut self.dialog {
match dialog {
Dialog::CreateItem { state } => state.update(&msg)?,
@@ -148,7 +175,7 @@ impl<'a> App<'a> {
}
}
Ok(().into_command())
Ok(batch.into_command())
}
fn open_dialog(&mut self) {
@@ -158,24 +185,77 @@ impl<'a> App<'a> {
self.focus = AppFocus::Dialog;
self.dialog = Some(Dialog::CreateItem {
state: CreateItemState::new(root, path),
state: CreateItemState::new(&self.state, root, path),
});
}
}
fn open_dialog_below(&mut self) {
if self.dialog.is_none() {
let root = self.root.clone();
let path = self.graph_explorer.get_current_path();
if let Some((_, rest)) = path.split_last() {
let path = rest.to_vec();
self.focus = AppFocus::Dialog;
self.dialog = Some(Dialog::CreateItem {
state: CreateItemState::new(&self.state, root, path),
});
}
}
}
fn open_edit_item_dialog(&mut self, item: &GraphItem) {
if self.dialog.is_none() {
let root = self.root.clone();
let path = self.graph_explorer.get_current_path();
self.dialog = Some(Dialog::EditItem {
state: EditItemState::new(root, path, item),
state: EditItemState::new(&self.state, root, path, item),
});
self.command = None;
self.focus = AppFocus::Dialog;
self.mode = Mode::Insert;
}
}
fn open_editor(&self, item: &GraphItem) -> Option<Command> {
tracing::info!("entering editor for session");
match editor::EditorSession::new(item).execute() {
Ok(None) => {
tracing::info!("editor returned without changes, skipping");
}
Ok(Some(item)) => {
if let GraphItem::Item {
title,
description,
state,
} = item
{
return Some(
self.state.update_item_command().command(
&self.root,
&self
.graph_explorer
.get_current_path()
.iter()
.map(|s| s.as_str())
.collect_vec(),
&title,
&description,
state,
),
);
}
}
Err(e) => {
tracing::error!("failed to run editor with: {}", e);
}
}
None
}
}
impl<'a> Widget for &mut App<'a> {

View File

@@ -1,7 +1,12 @@
use hyperlog_core::log::ItemState;
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use crate::models::Msg;
use crate::{
commands::{create_item::CreateItemCommandExt, IntoCommand},
models::Msg,
state::SharedState,
};
use super::{InputBuffer, InputField};
@@ -23,10 +28,16 @@ pub struct CreateItemState {
description: InputBuffer,
focused: CreateItemFocused,
state: SharedState,
}
impl CreateItemState {
pub fn new(root: impl Into<String>, path: impl IntoIterator<Item = impl Into<String>>) -> Self {
pub fn new(
state: &SharedState,
root: impl Into<String>,
path: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
let root = root.into();
let path = path.into_iter().map(|p| p.into()).collect_vec();
@@ -37,6 +48,8 @@ impl CreateItemState {
title: Default::default(),
description: Default::default(),
focused: Default::default(),
state: state.clone(),
}
}
@@ -61,21 +74,29 @@ impl CreateItemState {
Ok(())
}
pub fn get_command(&self) -> Option<hyperlog_core::commander::Command> {
pub fn get_command(&self) -> Option<impl IntoCommand> {
let title = self.title.string();
let description = self.description.string();
if !title.is_empty() {
let mut path = self.path.clone();
path.push(title.replace([' ', '.'], "-"));
path.push(title.replace(['.'], "-"));
Some(hyperlog_core::commander::Command::CreateItem {
root: self.root.clone(),
path,
title: title.trim().into(),
description: description.trim().into(),
state: hyperlog_core::log::ItemState::NotDone,
})
Some(self.state.create_item_command().command(
&self.root,
&path.iter().map(|i| i.as_str()).collect_vec(),
title.trim(),
description.trim(),
&ItemState::NotDone,
))
// Some(commander::Command::CreateItem {
// root: self.root.clone(),
// path,
// title: title.trim().into(),
// description: description.trim().into(),
// state: hyperlog_core::log::ItemState::NotDone,
// })
} else {
None
}

View File

@@ -2,7 +2,11 @@ use hyperlog_core::log::GraphItem;
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use crate::models::Msg;
use crate::{
commands::{update_item::UpdateItemCommandExt, IntoCommand},
models::Msg,
state::SharedState,
};
use super::{InputBuffer, InputField};
@@ -26,10 +30,13 @@ pub struct EditItemState {
item: GraphItem,
focused: EditItemFocused,
state: SharedState,
}
impl EditItemState {
pub fn new(
state: &SharedState,
root: impl Into<String>,
path: impl IntoIterator<Item = impl Into<String>>,
item: &GraphItem,
@@ -47,6 +54,8 @@ impl EditItemState {
title.set_position(title_len);
Self {
state: state.clone(),
root,
path,
@@ -82,24 +91,36 @@ impl EditItemState {
Ok(())
}
pub fn get_command(&self) -> Option<hyperlog_core::commander::Command> {
pub fn get_command(&self) -> Option<impl IntoCommand> {
let title = self.title.string();
let description = self.description.string();
if !title.is_empty() {
let path = self.path.clone();
Some(hyperlog_core::commander::Command::UpdateItem {
root: self.root.clone(),
path,
title: title.trim().into(),
description: description.trim().into(),
state: match &self.item {
Some(self.state.update_item_command().command(
&self.root,
&path.iter().map(|s| s.as_str()).collect_vec(),
title.trim(),
description.trim(),
match &self.item {
GraphItem::User(_) => Default::default(),
GraphItem::Section(_) => Default::default(),
GraphItem::Item { state, .. } => state.clone(),
},
})
))
// Some(commander::Command::UpdateItem {
// root: self.root.clone(),
// path,
// title: title.trim().into(),
// description: description.trim().into(),
// state: match &self.item {
// GraphItem::User(_) => Default::default(),
// GraphItem::Section(_) => Default::default(),
// GraphItem::Item { state, .. } => state.clone(),
// },
// })
} else {
None
}

View File

@@ -6,10 +6,14 @@ pub enum Commands {
WriteQuit,
Archive,
CreateSection { name: String },
CreateItem { name: String },
CreateBelow { name: String },
Edit,
Open,
ShowAll,
HideDone,
Test,
}
impl Commands {
@@ -39,9 +43,17 @@ impl CommandParser {
"cs" | "create-section" => rest.first().map(|name| Commands::CreateSection {
name: name.to_string(),
}),
"ci" | "create-item" => Some(Commands::CreateItem {
name: rest.join(" ").to_string(),
}),
"cb" | "create-below" => Some(Commands::CreateBelow {
name: rest.join(" ").to_string(),
}),
"e" | "edit" => Some(Commands::Edit),
"show-all" => Some(Commands::ShowAll),
"hide-done" => Some(Commands::HideDone),
"test" => Some(Commands::Test),
"o" | "open" => Some(Commands::Open),
_ => None,
},
None => None,

View File

@@ -0,0 +1,78 @@
use hyperlog_core::log::ItemState;
use serde::Serialize;
use tonic::transport::Channel;
use crate::{events::Events, shared_engine::SharedEngine, storage::Storage};
mod local;
mod remote;
#[derive(Serialize, PartialEq, Eq, Debug, Clone)]
pub enum Command {
CreateRoot {
root: String,
},
CreateSection {
root: String,
path: Vec<String>,
},
CreateItem {
root: String,
path: Vec<String>,
title: String,
description: String,
state: ItemState,
},
UpdateItem {
root: String,
path: Vec<String>,
title: String,
description: String,
state: ItemState,
},
ToggleItem {
root: String,
path: Vec<String>,
},
Move {
root: String,
src: Vec<String>,
dest: Vec<String>,
},
Archive {
root: String,
path: Vec<String>,
},
}
#[derive(Clone)]
enum CommanderVariant {
Local(local::Commander),
Remote(remote::Commander),
}
#[derive(Clone)]
pub struct Commander {
variant: CommanderVariant,
}
impl Commander {
pub fn local(engine: SharedEngine, storage: Storage, events: Events) -> anyhow::Result<Self> {
Ok(Self {
variant: CommanderVariant::Local(local::Commander::new(engine, storage, events)?),
})
}
pub fn remote(channel: Channel) -> anyhow::Result<Self> {
Ok(Self {
variant: CommanderVariant::Remote(remote::Commander::new(channel)?),
})
}
pub async fn execute(&self, cmd: Command) -> anyhow::Result<()> {
match &self.variant {
CommanderVariant::Local(commander) => commander.execute(cmd),
CommanderVariant::Remote(commander) => commander.execute(cmd).await,
}
}
}

View File

@@ -1,48 +1,12 @@
use std::collections::BTreeMap;
use serde::Serialize;
use hyperlog_core::log::GraphItem;
use crate::{
events::Events,
log::{GraphItem, ItemState},
shared_engine::SharedEngine,
storage::Storage,
};
use crate::{events::Events, shared_engine::SharedEngine, storage::Storage};
#[derive(Serialize, PartialEq, Eq, Debug, Clone)]
pub enum Command {
CreateRoot {
root: String,
},
CreateSection {
root: String,
path: Vec<String>,
},
CreateItem {
root: String,
path: Vec<String>,
title: String,
description: String,
state: ItemState,
},
UpdateItem {
root: String,
path: Vec<String>,
title: String,
description: String,
state: ItemState,
},
ToggleItem {
root: String,
path: Vec<String>,
},
Move {
root: String,
src: Vec<String>,
dest: Vec<String>,
},
}
use super::Command;
#[derive(Clone)]
pub struct Commander {
engine: SharedEngine,
storage: Storage,
@@ -110,6 +74,9 @@ impl Commander {
state,
},
)?,
Command::Archive { root, path } => self
.engine
.archive(&root, &path.iter().map(|p| p.as_str()).collect::<Vec<_>>())?,
}
self.storage.store(&self.engine)?;

View File

@@ -0,0 +1,125 @@
use hyperlog_protos::hyperlog::{graph_client::GraphClient, *};
use tonic::transport::Channel;
use super::Command;
#[allow(dead_code, unused_variables)]
#[derive(Clone)]
pub struct Commander {
channel: Channel,
}
#[allow(dead_code, unused_variables)]
impl Commander {
pub fn new(channel: Channel) -> anyhow::Result<Self> {
Ok(Self { channel })
}
pub async fn execute(&self, cmd: Command) -> anyhow::Result<()> {
tracing::debug!("executing event: {}", serde_json::to_string(&cmd)?);
match cmd.clone() {
Command::CreateRoot { root } => {
let channel = self.channel.clone();
let mut client = GraphClient::new(channel);
let request = tonic::Request::new(CreateRootRequest { root });
let response = client.create_root(request).await?;
let res = response.into_inner();
}
Command::CreateSection { root, path } => {
let channel = self.channel.clone();
let mut client = GraphClient::new(channel);
let request = tonic::Request::new(CreateSectionRequest { root, path });
let response = client.create_section(request).await?;
let res = response.into_inner();
}
Command::CreateItem {
root,
path,
title,
description,
state,
} => {
let channel = self.channel.clone();
let mut client = GraphClient::new(channel);
let request = tonic::Request::new(CreateItemRequest {
root,
path,
item: Some(ItemGraphItem {
title,
description,
item_state: Some(match state {
hyperlog_core::log::ItemState::NotDone => {
item_graph_item::ItemState::NotDone(ItemStateNotDone {})
}
hyperlog_core::log::ItemState::Done => {
item_graph_item::ItemState::Done(ItemStateDone {})
}
}),
}),
});
let response = client.create_item(request).await?;
let res = response.into_inner();
}
Command::Move { root, src, dest } => {
todo!()
}
Command::ToggleItem { root, path } => {
let channel = self.channel.clone();
let mut client = GraphClient::new(channel);
let request = tonic::Request::new(ToggleItemRequest { root, path });
let response = client.toggle_item(request).await?;
let res = response.into_inner();
}
Command::UpdateItem {
root,
path,
title,
description,
state,
} => {
let channel = self.channel.clone();
let mut client = GraphClient::new(channel);
let request = tonic::Request::new(UpdateItemRequest {
root,
path,
item: Some(ItemGraphItem {
title,
description,
item_state: Some(match state {
hyperlog_core::log::ItemState::NotDone => {
item_graph_item::ItemState::NotDone(ItemStateNotDone {})
}
hyperlog_core::log::ItemState::Done => {
item_graph_item::ItemState::Done(ItemStateDone {})
}
}),
}),
});
let response = client.update_item(request).await?;
let res = response.into_inner();
}
Command::Archive { root, path } => {
let channel = self.channel.clone();
let mut client = GraphClient::new(channel);
let request = tonic::Request::new(ArchiveRequest { root, path });
let response = client.archive(request).await?;
let res = response.into_inner();
}
}
Ok(())
}
}

View File

@@ -1,3 +1,16 @@
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
pub mod batch;
pub mod archive;
pub mod create_item;
pub mod create_section;
pub mod open_item;
pub mod open_update_item_dialog;
pub mod toggle_item;
pub mod update_graph;
pub mod update_item;
use crate::models::Msg;
pub trait IntoCommand {
@@ -6,7 +19,7 @@ pub trait IntoCommand {
impl IntoCommand for () {
fn into_command(self) -> Command {
Command::new(|| None)
Command::new(|_| None)
}
}
@@ -16,16 +29,47 @@ impl IntoCommand for Command {
}
}
type CommandFunc = dyn FnOnce(Dispatch) -> Option<Msg>;
pub struct Command {
func: Box<dyn FnOnce() -> Option<Msg>>,
func: Box<CommandFunc>,
}
impl Command {
pub fn new<T: FnOnce() -> Option<Msg> + 'static>(f: T) -> Self {
pub fn new<T: FnOnce(Dispatch) -> Option<Msg> + 'static>(f: T) -> Self {
Self { func: Box::new(f) }
}
pub fn execute(self) -> Option<Msg> {
self.func.call_once(())
pub fn execute(self, dispatch: Dispatch) -> Option<Msg> {
self.func.call_once((dispatch,))
}
}
pub fn create_dispatch() -> (Dispatch, Receiver) {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
(Dispatch { sender: tx }, Receiver { receiver: rx })
}
#[derive(Clone)]
pub struct Dispatch {
sender: UnboundedSender<Msg>,
}
impl Dispatch {
pub fn send(&self, msg: Msg) {
if let Err(e) = self.sender.send(msg) {
tracing::warn!("failed to send event: {}", e);
}
}
}
pub struct Receiver {
receiver: UnboundedReceiver<Msg>,
}
impl Receiver {
pub async fn next(&mut self) -> Option<Msg> {
self.receiver.recv().await
}
}

View File

@@ -0,0 +1,57 @@
use itertools::Itertools;
use crate::{
commander::{self, Commander},
models::{IOEvent, Msg},
state::SharedState,
};
pub struct ArchiveCommand {
commander: Commander,
}
impl ArchiveCommand {
pub fn new(commander: Commander) -> Self {
Self { commander }
}
pub fn command(self, root: &str, path: &[&str]) -> super::Command {
let root = root.to_owned();
let path = path.iter().map(|s| s.to_string()).collect_vec();
super::Command::new(|dispatch| {
tokio::spawn(async move {
dispatch.send(Msg::Archive(IOEvent::Initialized));
match self
.commander
.execute(commander::Command::Archive { root, path })
.await
{
Ok(()) => {
#[cfg(debug_assertions)]
{
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
dispatch.send(Msg::Archive(IOEvent::Success(())));
}
Err(e) => {
dispatch.send(Msg::Archive(IOEvent::Failure(e.to_string())));
}
}
});
None
})
}
}
pub trait ArchiveCommandExt {
fn archive_command(&self) -> ArchiveCommand;
}
impl ArchiveCommandExt for SharedState {
fn archive_command(&self) -> ArchiveCommand {
ArchiveCommand::new(self.commander.clone())
}
}

View File

@@ -0,0 +1,41 @@
use super::IntoCommand;
#[derive(Default)]
pub struct BatchCommand {
commands: Vec<super::Command>,
}
impl BatchCommand {
pub fn with(&mut self, cmd: impl IntoCommand) -> &mut Self {
self.commands.push(cmd.into_command());
self
}
}
impl IntoCommand for Vec<super::Command> {
fn into_command(self) -> super::Command {
BatchCommand::from(self).into_command()
}
}
impl From<Vec<super::Command>> for BatchCommand {
fn from(value: Vec<super::Command>) -> Self {
BatchCommand { commands: value }
}
}
impl IntoCommand for BatchCommand {
fn into_command(self) -> super::Command {
super::Command::new(|dispatch| {
for command in self.commands {
let msg = command.execute(dispatch.clone());
if let Some(msg) = msg {
dispatch.send(msg);
}
}
None
})
}
}

View File

@@ -0,0 +1,73 @@
use hyperlog_core::log::ItemState;
use itertools::Itertools;
use crate::{
commander::{self, Commander},
models::{IOEvent, Msg},
state::SharedState,
};
pub struct CreateItemCommand {
commander: Commander,
}
impl CreateItemCommand {
pub fn new(commander: Commander) -> Self {
Self { commander }
}
pub fn command(
self,
root: &str,
path: &[&str],
title: &str,
description: &str,
state: &ItemState,
) -> super::Command {
let root = root.to_owned();
let path = path.iter().map(|s| s.to_string()).collect_vec();
let title = title.to_string();
let description = description.to_string();
let state = state.clone();
super::Command::new(|dispatch| {
tokio::spawn(async move {
dispatch.send(Msg::ItemCreated(IOEvent::Initialized));
match self
.commander
.execute(commander::Command::CreateItem {
root,
path,
title,
description,
state,
})
.await
{
Ok(()) => {
#[cfg(debug_assertions)]
{
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
dispatch.send(Msg::ItemCreated(IOEvent::Success(())));
}
Err(e) => {
dispatch.send(Msg::ItemCreated(IOEvent::Failure(e.to_string())));
}
}
});
None
})
}
}
pub trait CreateItemCommandExt {
fn create_item_command(&self) -> CreateItemCommand;
}
impl CreateItemCommandExt for SharedState {
fn create_item_command(&self) -> CreateItemCommand {
CreateItemCommand::new(self.commander.clone())
}
}

View File

@@ -0,0 +1,59 @@
use itertools::Itertools;
use crate::{
commander::{self, Commander},
models::IOEvent,
state::SharedState,
};
pub struct CreateSectionCommand {
commander: Commander,
}
impl CreateSectionCommand {
pub fn new(commander: Commander) -> Self {
Self { commander }
}
pub fn command(self, root: &str, path: &[&str]) -> super::Command {
let root = root.to_owned();
let path = path.iter().map(|s| s.to_string()).collect_vec();
super::Command::new(|dispatch| {
tokio::spawn(async move {
dispatch.send(crate::models::Msg::SectionCreated(IOEvent::Initialized));
match self
.commander
.execute(commander::Command::CreateSection { root, path })
.await
{
Ok(()) => {
#[cfg(debug_assertions)]
{
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
dispatch.send(crate::models::Msg::SectionCreated(IOEvent::Success(())));
}
Err(e) => {
dispatch.send(crate::models::Msg::SectionCreated(IOEvent::Failure(
e.to_string(),
)));
}
}
});
None
})
}
}
pub trait CreateSectionCommandExt {
fn create_section_command(&self) -> CreateSectionCommand;
}
impl CreateSectionCommandExt for SharedState {
fn create_section_command(&self) -> CreateSectionCommand {
CreateSectionCommand::new(self.commander.clone())
}
}

View File

@@ -0,0 +1,59 @@
use crate::{
models::{IOEvent, Msg},
querier::Querier,
state::SharedState,
};
pub struct OpenItemCommand {
querier: Querier,
}
impl OpenItemCommand {
pub fn new(querier: Querier) -> Self {
Self { querier }
}
pub fn command(self, root: &str, path: Vec<String>) -> super::Command {
let root = root.to_string();
super::Command::new(|dispatch| {
tokio::spawn(async move {
dispatch.send(Msg::OpenItem(IOEvent::Initialized));
let item = match self.querier.get_async(&root, path).await {
Ok(item) => match item {
Some(item) => {
dispatch.send(Msg::OpenItem(IOEvent::Success(())));
item
}
None => {
dispatch.send(Msg::OpenItem(IOEvent::Failure(
"failed to find a valid item for path".into(),
)));
return;
}
},
Err(e) => {
dispatch.send(Msg::OpenItem(IOEvent::Failure(e.to_string())));
return;
}
};
dispatch.send(Msg::OpenEditor { item });
});
None
})
}
}
pub trait OpenItemCommandExt {
fn open_item_command(&self) -> OpenItemCommand;
}
impl OpenItemCommandExt for SharedState {
fn open_item_command(&self) -> OpenItemCommand {
OpenItemCommand::new(self.querier.clone())
}
}

View File

@@ -0,0 +1,59 @@
use crate::{
models::{IOEvent, Msg},
querier::Querier,
state::SharedState,
};
pub struct OpenUpdateItemDialogCommand {
querier: Querier,
}
impl OpenUpdateItemDialogCommand {
pub fn new(querier: Querier) -> Self {
Self { querier }
}
pub fn command(self, root: &str, path: Vec<String>) -> super::Command {
let root = root.to_string();
super::Command::new(|dispatch| {
tokio::spawn(async move {
dispatch.send(Msg::OpenUpdateItemDialog(IOEvent::Initialized));
let item = match self.querier.get_async(&root, path).await {
Ok(item) => match item {
Some(item) => {
dispatch.send(Msg::OpenUpdateItemDialog(IOEvent::Success(())));
item
}
None => {
dispatch.send(Msg::OpenUpdateItemDialog(IOEvent::Failure(
"failed to find a valid item for path".into(),
)));
return;
}
},
Err(e) => {
dispatch.send(Msg::OpenUpdateItemDialog(IOEvent::Failure(e.to_string())));
return;
}
};
dispatch.send(Msg::OpenEditItemDialog { item });
});
None
})
}
}
pub trait OpenUpdateItemDialogCommandExt {
fn open_update_item_dialog_command(&self) -> OpenUpdateItemDialogCommand;
}
impl OpenUpdateItemDialogCommandExt for SharedState {
fn open_update_item_dialog_command(&self) -> OpenUpdateItemDialogCommand {
OpenUpdateItemDialogCommand::new(self.querier.clone())
}
}

View File

@@ -0,0 +1,59 @@
use itertools::Itertools;
use crate::{
commander::{self, Commander},
models::IOEvent,
state::SharedState,
};
pub struct ToggleItemCommand {
commander: Commander,
}
impl ToggleItemCommand {
pub fn new(commander: Commander) -> Self {
Self { commander }
}
pub fn command(self, root: &str, path: &[&str]) -> super::Command {
let root = root.to_owned();
let path = path.iter().map(|s| s.to_string()).collect_vec();
super::Command::new(|dispatch| {
tokio::spawn(async move {
dispatch.send(crate::models::Msg::ItemToggled(IOEvent::Initialized));
match self
.commander
.execute(commander::Command::ToggleItem { root, path })
.await
{
Ok(()) => {
#[cfg(debug_assertions)]
{
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
dispatch.send(crate::models::Msg::ItemToggled(IOEvent::Success(())));
}
Err(e) => {
dispatch.send(crate::models::Msg::ItemToggled(IOEvent::Failure(
e.to_string(),
)));
}
}
});
None
})
}
}
pub trait ToggleItemCommandExt {
fn toggle_item_command(&self) -> ToggleItemCommand;
}
impl ToggleItemCommandExt for SharedState {
fn toggle_item_command(&self) -> ToggleItemCommand {
ToggleItemCommand::new(self.commander.clone())
}
}

View File

@@ -0,0 +1,61 @@
use itertools::Itertools;
use crate::{
models::{IOEvent, Msg},
querier::Querier,
state::SharedState,
};
pub struct UpdateGraphCommand {
querier: Querier,
}
impl UpdateGraphCommand {
pub fn new(querier: Querier) -> Self {
Self { querier }
}
pub fn command(self, root: &str, path: &[&str]) -> super::Command {
let root = root.to_owned();
let path = path.iter().map(|i| i.to_string()).collect_vec();
super::Command::new(|dispatch| {
tokio::spawn(async move {
let now = std::time::SystemTime::now();
dispatch.send(Msg::GraphUpdated(IOEvent::Initialized));
match self.querier.get_async(&root, path).await {
Ok(Some(graph)) => {
dispatch.send(Msg::GraphUpdated(IOEvent::Optimistic(graph.clone())));
#[cfg(debug_assertions)]
{
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
dispatch.send(Msg::GraphUpdated(IOEvent::Success(graph)))
}
Ok(None) => dispatch.send(Msg::GraphUpdated(IOEvent::Failure(
"graph was not found user root".into(),
))),
Err(e) => dispatch.send(Msg::GraphUpdated(IOEvent::Failure(format!("{e}")))),
}
let elapsed = now.elapsed().expect("to be able to get time");
tracing::trace!("UpdateGraphCommand took: {}nanos", elapsed.as_nanos());
});
None
})
}
}
pub trait UpdateGraphCommandExt {
fn update_graph_command(&self) -> UpdateGraphCommand;
}
impl UpdateGraphCommandExt for SharedState {
fn update_graph_command(&self) -> UpdateGraphCommand {
UpdateGraphCommand::new(self.querier.clone())
}
}

View File

@@ -0,0 +1,76 @@
use hyperlog_core::log::ItemState;
use itertools::Itertools;
use crate::{
commander::{self, Commander},
models::IOEvent,
state::SharedState,
};
pub struct UpdateItemCommand {
commander: Commander,
}
impl UpdateItemCommand {
pub fn new(commander: Commander) -> Self {
Self { commander }
}
pub fn command(
self,
root: &str,
path: &[&str],
title: &str,
description: &str,
state: ItemState,
) -> super::Command {
let root = root.to_owned();
let path = path.iter().map(|s| s.to_string()).collect_vec();
let title = title.to_string();
let description = description.to_string();
let state = state.clone();
super::Command::new(|dispatch| {
tokio::spawn(async move {
dispatch.send(crate::models::Msg::ItemUpdated(IOEvent::Initialized));
match self
.commander
.execute(commander::Command::UpdateItem {
root,
path,
title,
description,
state,
})
.await
{
Ok(()) => {
#[cfg(debug_assertions)]
{
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
dispatch.send(crate::models::Msg::ItemUpdated(IOEvent::Success(())));
}
Err(e) => {
dispatch.send(crate::models::Msg::ItemUpdated(IOEvent::Failure(
e.to_string(),
)));
}
}
});
None
})
}
}
pub trait UpdateItemCommandExt {
fn update_item_command(&self) -> UpdateItemCommand;
}
impl UpdateItemCommandExt for SharedState {
fn update_item_command(&self) -> UpdateItemCommand {
UpdateItemCommand::new(self.commander.clone())
}
}

View File

@@ -1,9 +1,18 @@
use anyhow::Result;
use hyperlog_core::log::GraphItem;
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use crate::{
command_parser::Commands, components::movement_graph::GraphItemType, models::Msg,
command_parser::Commands,
commands::{
archive::ArchiveCommandExt, batch::BatchCommand, create_item::CreateItemCommandExt,
create_section::CreateSectionCommandExt, open_item::OpenItemCommandExt,
open_update_item_dialog::OpenUpdateItemDialogCommandExt, toggle_item::ToggleItemCommandExt,
update_graph::UpdateGraphCommandExt, Command, IntoCommand,
},
components::movement_graph::GraphItemType,
models::{IOEvent, Msg},
state::SharedState,
};
@@ -46,6 +55,31 @@ pub struct GraphExplorerState<'a> {
graph: Option<GraphItem>,
}
impl<'a> GraphExplorerState<'a> {
pub fn update(&mut self, msg: &Msg) -> Option<Command> {
if let Msg::GraphUpdated(graph_update) = msg {
match graph_update {
IOEvent::Initialized => {
tracing::trace!("initialized graph");
}
IOEvent::Success(graph) => {
tracing::trace!("graph updated successfully");
self.graph = Some(graph.clone());
}
IOEvent::Failure(e) => {
tracing::error!("graph update failed: {}", e);
}
IOEvent::Optimistic(graph) => {
tracing::trace!("graph updated optimistically");
self.graph = Some(graph.clone());
}
}
}
None
}
}
impl<'a> GraphExplorer<'a> {
pub fn new(root: String, state: SharedState) -> Self {
Self {
@@ -60,19 +94,31 @@ impl<'a> GraphExplorer<'a> {
}
}
pub fn update_graph(&mut self) -> Result<&mut Self> {
pub fn new_update_graph(&self) -> Command {
self.state.update_graph_command().command(
&self.inner.root,
&self
.inner
.current_path
.map(|p| p.split(".").collect_vec())
.unwrap_or_default(),
)
}
pub async fn update_graph(&mut self) -> Result<&mut Self> {
let now = std::time::SystemTime::now();
let graph = self
.state
.querier
.get(
.get_async(
&self.inner.root,
self.inner
.current_path
.map(|p| p.split('.').collect::<Vec<_>>())
.unwrap_or_default(),
)
.await?
.ok_or(anyhow::anyhow!("graph should've had an item"))?;
self.inner.graph = Some(graph);
@@ -98,7 +144,7 @@ impl<'a> GraphExplorer<'a> {
/// Choses: 0.1.0.0 else nothing
pub(crate) fn move_right(&mut self) -> Result<()> {
if let Some(graph) = self.linearize_graph() {
tracing::debug!("graph: {:?}", graph);
tracing::trace!("graph: {:?}", graph);
let position_items = &self.inner.current_position;
if let Some(next_item) = graph.next_right(position_items) {
@@ -184,24 +230,72 @@ impl<'a> GraphExplorer<'a> {
}
}
pub fn execute_command(&mut self, command: &Commands) -> anyhow::Result<Option<Msg>> {
pub fn execute_command(&mut self, command: &Commands) -> anyhow::Result<Option<Command>> {
let mut batch = BatchCommand::default();
match command {
Commands::Archive => {
if !self.get_current_path().is_empty() {
tracing::debug!("archiving path: {:?}", self.get_current_path())
batch.with(
self.state
.archive_command()
.command(
&self.inner.root,
&self
.get_current_path()
.iter()
.map(|i| i.as_str())
.collect_vec(),
)
.into_command(),
);
}
}
Commands::CreateSection { name } => {
if !name.is_empty() {
let mut path = self.get_current_path();
path.push(name.replace(" ", "-").replace(".", "-"));
path.push(name.replace(".", "-"));
self.state.commander.execute(
hyperlog_core::commander::Command::CreateSection {
root: self.inner.root.clone(),
path,
},
)?;
let cmd = self.state.create_section_command().command(
&self.inner.root,
&path.iter().map(|i| i.as_str()).collect_vec(),
);
batch.with(cmd.into_command());
}
}
Commands::CreateItem { name } => {
if !name.is_empty() {
let mut path = self.get_current_path();
path.push(name.replace(".", " "));
let cmd = self.state.create_item_command().command(
&self.inner.root,
&path.iter().map(|i| i.as_str()).collect_vec(),
name,
"",
&hyperlog_core::log::ItemState::default(),
);
batch.with(cmd.into_command());
}
}
Commands::CreateBelow { name } => {
if !name.is_empty() {
let path = self.get_current_path();
if let Some((_, path)) = path.split_last() {
let mut path = path.to_vec();
path.push(name.replace(".", " "));
let cmd = self.state.create_item_command().command(
&self.inner.root,
&path.iter().map(|i| i.as_str()).collect_vec(),
name,
"",
&hyperlog_core::log::ItemState::default(),
);
batch.with(cmd.into_command());
}
}
}
Commands::Edit => {
@@ -218,11 +312,11 @@ impl<'a> GraphExplorer<'a> {
todo!("cannot edit section at the moment")
}
GraphItemType::Item { .. } => {
if let Some(item) = self.state.querier.get(&self.inner.root, path) {
if let GraphItem::Item { .. } = item {
return Ok(Some(Msg::OpenEditItemDialog { item }));
}
}
batch.with(
self.state
.open_update_item_dialog_command()
.command(&self.inner.root, path),
);
}
}
}
@@ -233,29 +327,63 @@ impl<'a> GraphExplorer<'a> {
Commands::HideDone => {
self.inner.display_options.filter_by = FilterBy::NotDone;
}
Commands::Test => {
return Ok(Some(Command::new(|dispatch| {
tokio::spawn(async move {
dispatch.send(Msg::MoveDown);
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
dispatch.send(Msg::EnterViewMode);
});
None
})));
}
Commands::Open => {
if self.get_current_item().is_some() {
batch.with(
self.state
.open_item_command()
.command(&self.inner.root, self.get_current_path()),
);
}
}
_ => (),
}
self.update_graph()?;
//self.update_graph()?;
Ok(None)
Ok(Some(batch.into_command()))
}
pub(crate) fn interact(&mut self) -> anyhow::Result<()> {
pub(crate) fn interact(&mut self) -> anyhow::Result<Command> {
let mut batch = BatchCommand::default();
if !self.get_current_path().is_empty() {
tracing::info!("toggling state of items");
self.state
.commander
.execute(hyperlog_core::commander::Command::ToggleItem {
root: self.inner.root.to_string(),
path: self.get_current_path(),
})?;
// self.state
// .commander
// .execute(commander::Command::ToggleItem {
// root: self.inner.root.to_string(),
// path: self.get_current_path(),
// })?;
let cmd = self.state.toggle_item_command().command(
&self.inner.root,
&self
.get_current_path()
.iter()
.map(|i| i.as_str())
.collect_vec(),
);
batch.with(cmd.into_command());
}
self.update_graph()?;
//self.update_graph()?;
Ok(())
Ok(batch.into_command())
}
}
@@ -264,7 +392,6 @@ impl<'a> StatefulWidget for GraphExplorer<'a> {
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let Rect { height, .. } = area;
let _height = height as usize;
if let Some(graph) = &state.graph {
let movement_graph: MovementGraph =

View File

@@ -21,20 +21,20 @@ impl Summarize for MovementGraphItem {
vec![
name,
Span::from(" ~ "),
Span::from(format!("(items: {})", items)),
Span::from(" ~ ").fg(GREEN),
Span::from(format!("(items: {})", items)).fg(Color::DarkGray),
]
}
GraphItemType::Item { done } => {
if done {
vec![
Span::from("["),
Span::from("[").fg(Color::DarkGray),
Span::from("x").fg(GREEN),
Span::from("] "),
Span::from("] ").fg(Color::DarkGray),
name,
]
} else {
vec![Span::from("[ ] "), name]
vec![Span::from("[ ] ").fg(Color::DarkGray), name]
}
}
}

View File

@@ -0,0 +1,81 @@
use std::path::PathBuf;
use tonic::transport::{Channel, ClientTlsConfig};
use crate::{
commander::Commander, events::Events, querier::Querier, shared_engine::SharedEngine,
storage::Storage,
};
#[allow(dead_code)]
pub struct State {
pub commander: Commander,
pub querier: Querier,
backend: Backend,
}
pub enum Backend {
Local { path_override: Option<PathBuf> },
Remote { url: String },
}
impl State {
pub async fn new(backend: Backend) -> anyhow::Result<Self> {
let (querier, commander) = match &backend {
Backend::Local { path_override } => {
let mut storage = Storage::new();
if let Some(path_override) = path_override {
storage.with_base(path_override);
}
let engine = storage.load()?;
let events = Events::default();
let engine = SharedEngine::from(engine);
(
Querier::local(&engine),
Commander::local(engine.clone(), storage.clone(), events.clone())?,
)
}
Backend::Remote { url } => {
let tls = ClientTlsConfig::new();
let channel = Channel::from_shared(url.clone())?
.tls_config(tls.with_native_roots())?
.connect()
.await?;
(
Querier::remote(channel.clone()).await?,
Commander::remote(channel)?,
)
}
};
Ok(Self {
commander,
querier,
backend,
})
}
pub fn unlock(&self) {
if let Backend::Local { path_override } = &self.backend {
let mut storage = Storage::new();
if let Some(path_override) = path_override {
storage.with_base(path_override);
}
storage.clear_lock_file();
}
}
pub fn info(&self) -> Option<anyhow::Result<String>> {
if let Backend::Local { path_override } = &self.backend {
let mut storage = Storage::new();
if let Some(path_override) = path_override {
storage.with_base(path_override);
}
return Some(storage.info());
}
None
}
}

View File

@@ -0,0 +1,289 @@
use std::{
io::{Read, Write},
path::{Path, PathBuf},
time::SystemTime,
};
use anyhow::{anyhow, Context};
use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode},
ExecutableCommand,
};
use hyperlog_core::log::{GraphItem, ItemState};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use sha2::Digest;
use crate::project_dirs::get_project_dir;
pub struct EditorSession<'a> {
item: &'a GraphItem,
}
struct EditorFile {
title: String,
metadata: Metadata,
body: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct Metadata {
state: ItemState,
}
impl EditorFile {
pub fn serialize(&self) -> anyhow::Result<String> {
let metadata =
toml::to_string_pretty(&self.metadata).context("failed to serialize metadata")?;
let frontmatter = format!("+++\n{}+++\n", metadata);
Ok(format!(
"{}\n# {}\n\n{}",
frontmatter, self.title, self.body
))
}
}
impl TryFrom<&GraphItem> for EditorFile {
type Error = anyhow::Error;
fn try_from(value: &GraphItem) -> Result<Self, Self::Error> {
if let GraphItem::Item {
title,
description,
state,
} = value.clone()
{
Ok(Self {
title,
metadata: Metadata { state },
body: description,
})
} else {
anyhow::bail!("can only generate a file based on items")
}
}
}
impl TryFrom<&str> for EditorFile {
type Error = anyhow::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let value = value.to_string();
let frontmatter_parts = value.split("+++").filter(|p| !p.is_empty()).collect_vec();
let frontmatter_content = frontmatter_parts
.first()
.ok_or(anyhow::anyhow!("no front matter parts were found"))?;
tracing::trace!("parsing frontmatter content: {}", frontmatter_content);
let metadata: Metadata = toml::from_str(frontmatter_content)?;
let line_parts = value.split("\n");
let title = line_parts
.clone()
.find(|p| p.starts_with("# "))
.map(|t| t.trim_start_matches("# "))
.ok_or(anyhow!("an editor file requires a title with heading 1"))?;
let body = line_parts
.skip_while(|p| !p.starts_with("# "))
.skip(1)
.skip_while(|p| p.is_empty())
.collect_vec()
.join("\n");
Ok(Self {
title: title.to_string(),
metadata,
body,
})
}
}
impl From<EditorFile> for GraphItem {
fn from(value: EditorFile) -> Self {
Self::Item {
title: value.title,
description: value.body,
state: value.metadata.state,
}
}
}
struct SessionFile {
path: PathBuf,
loaded: SystemTime,
}
impl SessionFile {
pub fn get_path(&self) -> &Path {
self.path.as_path()
}
pub fn is_changed(&self) -> anyhow::Result<bool> {
let modified = self.path.metadata()?.modified()?;
Ok(self.loaded < modified)
}
}
impl Drop for SessionFile {
fn drop(&mut self) {
// std::io::stdout()
// .execute(crossterm::terminal::EnterAlternateScreen)
// .expect("to be able to restore alternative mode");
// enable_raw_mode().expect("to be able to restore raw mode");
if self.path.exists() {
tracing::debug!("cleaning up file: {}", self.path.display());
if let Err(e) = std::fs::remove_file(&self.path) {
tracing::error!(
"failed to cleanup file: {}, error: {}",
self.path.display(),
e
);
}
}
}
}
impl<'a> EditorSession<'a> {
pub fn new(item: &'a GraphItem) -> Self {
Self { item }
}
fn get_file_path(&mut self) -> anyhow::Result<PathBuf> {
let name = self
.item
.get_digest()
.ok_or(anyhow::anyhow!("item doesn't have a title"))?;
let file_path = get_project_dir()
.data_dir()
.join("edit")
.join(format!("{name}.md"));
Ok(file_path)
}
fn prepare_file(&mut self) -> anyhow::Result<SessionFile> {
let file_path = self.get_file_path()?;
if let Some(parent) = file_path.parent() {
tracing::debug!("creating parent dir: {}", parent.display());
std::fs::create_dir_all(parent).context("failed to create dir for edit file")?;
}
let mut file =
std::fs::File::create(&file_path).context("failed to create file for edit file")?;
tracing::debug!("writing contents to file: {}", file_path.display());
let editor_file = EditorFile::try_from(self.item)?;
file.write_all(
editor_file
.serialize()
.context("failed to serialize item to file")?
.as_bytes(),
)
.context("failed to write to file")?;
file.flush().context("failed to flush to disk")?;
let modified_time = file.metadata()?.modified()?;
Ok(SessionFile {
path: file_path,
loaded: modified_time,
})
}
fn get_item_from_file(&self, session_file: SessionFile) -> anyhow::Result<GraphItem> {
let mut file = std::fs::File::open(&session_file.path)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
let editor_file = EditorFile::try_from(content.as_str())?;
Ok(editor_file.into())
}
pub fn execute(&mut self) -> anyhow::Result<Option<GraphItem>> {
let editor = std::env::var("EDITOR").context("no editor was found for EDITOR env var")?;
let session_file = self.prepare_file()?;
tracing::debug!(
"opening editor: {} at path: {}",
editor,
session_file.get_path().display()
);
std::io::stdout().flush()?;
// disable_raw_mode()?;
// std::io::stdout().execute(crossterm::terminal::LeaveAlternateScreen)?;
let path = session_file.get_path();
if let Some(parent) = path.parent() {
if let Err(e) = std::process::Command::new(editor)
.arg(
path.file_name()
.ok_or(anyhow::anyhow!("failed to find file in the given path"))?,
)
.current_dir(parent)
.status()
{
tracing::error!("failed command with: {}", e);
return Ok(None);
}
} else if let Err(e) = std::process::Command::new(editor)
.arg(session_file.get_path())
.status()
{
tracing::error!("failed command with: {}", e);
return Ok(None);
}
tracing::debug!(
"returning from editor, checking file: {}",
session_file.get_path().display()
);
if session_file.is_changed()? {
tracing::debug!(
"file: {} changed, updating item",
session_file.get_path().display()
);
Ok(Some(self.get_item_from_file(session_file)?))
} else {
Ok(None)
}
}
}
trait ItemExt {
fn get_digest(&self) -> Option<String>;
}
impl ItemExt for &GraphItem {
fn get_digest(&self) -> Option<String> {
if let GraphItem::Item { title, .. } = self {
let digest = sha2::Sha256::digest(title.as_bytes());
let digest_hex = hex::encode(digest);
Some(format!(
"{}_{}",
title
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.take(10)
.collect::<String>(),
digest_hex.chars().take(10).collect::<String>()
))
} else {
None
}
}
}

View File

@@ -1,8 +1,7 @@
use std::{collections::BTreeMap, fmt::Display};
use anyhow::{anyhow, Context};
use crate::log::{Graph, GraphItem, ItemState};
use hyperlog_core::log::{Graph, GraphItem, ItemState};
#[derive(Default)]
pub struct Engine {
@@ -205,6 +204,12 @@ impl Engine {
Some(items)
}
}
pub fn archive(&mut self, root: &str, path: &[&str]) -> anyhow::Result<()> {
self.delete(root, path)?;
Ok(())
}
}
impl Display for Engine {
@@ -218,10 +223,9 @@ impl Display for Engine {
mod test {
use std::collections::BTreeMap;
use hyperlog_core::log::{GraphItem, ItemState};
use similar_asserts::assert_eq;
use crate::log::{GraphItem, ItemState};
use super::Engine;
#[test]
@@ -249,7 +253,7 @@ mod test {
.create(
"kjuulh",
&["some-section"],
crate::log::GraphItem::Section(BTreeMap::default()),
GraphItem::Section(BTreeMap::default()),
)
.unwrap();
@@ -275,7 +279,7 @@ mod test {
.create(
"kjuulh",
&["some-section"],
crate::log::GraphItem::Section(BTreeMap::default()),
GraphItem::Section(BTreeMap::default()),
)
.unwrap();
@@ -283,7 +287,7 @@ mod test {
.create(
"kjuulh",
&["some-section", "some-sub-section"],
crate::log::GraphItem::Section(BTreeMap::default()),
GraphItem::Section(BTreeMap::default()),
)
.unwrap();

View File

@@ -1,13 +1,16 @@
#![feature(map_try_insert)]
#![feature(fn_traits)]
#![feature(let_chains)]
use std::{io::Stdout, time::Duration};
use std::io::Stdout;
use anyhow::{Context, Result};
use app::{render_app, App};
use commands::IntoCommand;
use commands::{Dispatch, IntoCommand, Receiver};
use components::graph_explorer::GraphExplorer;
use crossterm::event::{self, Event, KeyCode};
use hyperlog_core::state::State;
use core_state::State;
use crossterm::event::{Event, KeyCode, KeyEventKind};
use futures::{FutureExt, StreamExt};
use models::{EditMsg, Msg};
use ratatui::{backend::CrosstermBackend, Terminal};
@@ -19,9 +22,20 @@ pub(crate) mod app;
pub(crate) mod command_parser;
pub(crate) mod commands;
pub(crate) mod components;
pub(crate) mod state;
pub mod commander;
pub mod core_state;
pub mod shared_engine;
pub mod state;
mod engine;
mod events;
mod querier;
pub mod storage;
mod editor;
mod logging;
mod project_dirs;
mod terminal;
pub async fn execute(state: State) -> Result<()> {
@@ -33,13 +47,13 @@ pub async fn execute(state: State) -> Result<()> {
let state = SharedState::from(state);
let mut terminal = TerminalInstance::new()?;
run(&mut terminal, state).context("app loop failed")?;
run(&mut terminal, state).await.context("app loop failed")?;
Ok(())
}
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>, state: SharedState) -> Result<()> {
let root = match state.querier.get_available_roots() {
async fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>, state: SharedState) -> Result<()> {
let root = match state.querier.get_available_roots_async().await? {
// TODO: maybe present choose root screen
Some(roots) => roots.first().cloned().unwrap(),
None => {
@@ -49,14 +63,25 @@ fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>, state: SharedState) ->
};
let mut graph_explorer = GraphExplorer::new(root.clone(), state.clone());
graph_explorer.update_graph()?;
graph_explorer.update_graph().await?;
let mut app = App::new(&root, state.clone(), graph_explorer);
let (dispatch, mut receiver) = commands::create_dispatch();
let mut event_stream = crossterm::event::EventStream::new();
loop {
terminal.draw(|f| render_app(f, &mut app))?;
if update(terminal, &mut app)?.should_quit() {
if update(
terminal,
&mut app,
&dispatch,
&mut receiver,
&mut event_stream,
)
.await?
.should_quit()
{
break;
}
}
@@ -75,53 +100,107 @@ impl UpdateConclusion {
}
}
fn update(
async fn update<'a>(
_terminal: &mut Terminal<CrosstermBackend<Stdout>>,
app: &mut App,
app: &mut App<'a>,
dispatch: &Dispatch,
receiver: &mut Receiver,
event_stream: &mut crossterm::event::EventStream,
) -> Result<UpdateConclusion> {
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
if let Event::Key(key) = event::read().context("event read failed")? {
let mut cmd = match &app.mode {
app::Mode::View => match key.code {
KeyCode::Enter => app.update(Msg::Interact)?,
KeyCode::Char('l') => app.update(Msg::MoveRight)?,
KeyCode::Char('h') => app.update(Msg::MoveLeft)?,
KeyCode::Char('j') => app.update(Msg::MoveDown)?,
KeyCode::Char('k') => app.update(Msg::MoveUp)?,
KeyCode::Char('a') => {
// TODO: batch commands
app.update(Msg::OpenCreateItemDialog)?;
app.update(Msg::EnterInsertMode)?
}
KeyCode::Char('i') => app.update(Msg::EnterInsertMode)?,
KeyCode::Char(':') => app.update(Msg::EnterCommandMode)?,
_ => return Ok(UpdateConclusion(false)),
},
let cross_event = event_stream.next().fuse();
app::Mode::Command | app::Mode::Insert => match key.code {
KeyCode::Backspace => app.update(Msg::Edit(EditMsg::Delete))?,
KeyCode::Enter => app.update(Msg::Edit(EditMsg::InsertNewLine))?,
KeyCode::Tab => app.update(Msg::Edit(EditMsg::InsertTab))?,
KeyCode::Delete => app.update(Msg::Edit(EditMsg::DeleteNext))?,
KeyCode::Char(c) => app.update(Msg::Edit(EditMsg::InsertChar(c)))?,
KeyCode::Left => app.update(Msg::Edit(EditMsg::MoveLeft))?,
KeyCode::Right => app.update(Msg::Edit(EditMsg::MoveRight))?,
KeyCode::Esc => app.update(Msg::EnterViewMode)?,
_ => return Ok(UpdateConclusion(false)),
},
};
let mut handle_key_event = |maybe_event| -> anyhow::Result<UpdateConclusion> {
match maybe_event {
Some(Ok(e)) => {
if let Event::Key(key) = e
&& key.kind == KeyEventKind::Press
{
let mut cmd = match &app.mode {
app::Mode::View => match key.code {
KeyCode::Enter => app.update(Msg::Interact)?,
KeyCode::Char('l') => app.update(Msg::MoveRight)?,
KeyCode::Char('h') => app.update(Msg::MoveLeft)?,
KeyCode::Char('j') => app.update(Msg::MoveDown)?,
KeyCode::Char('k') => app.update(Msg::MoveUp)?,
KeyCode::Char('a') => {
// TODO: batch commands
app.update(Msg::OpenCreateItemDialog)?;
app.update(Msg::EnterInsertMode)?
}
KeyCode::Char('o') => {
// TODO: batch commands
app.update(Msg::OpenCreateItemDialogBelow)?;
app.update(Msg::EnterInsertMode)?
}
KeyCode::Char('i') => app.update(Msg::EnterInsertMode)?,
KeyCode::Char(':') => app.update(Msg::EnterCommandMode)?,
_ => return Ok(UpdateConclusion(false)),
},
loop {
let msg = cmd.into_command().execute();
match msg {
Some(msg) => {
if let Msg::QuitApp = msg {
return Ok(UpdateConclusion(true));
app::Mode::Command | app::Mode::Insert => match key.code {
KeyCode::Backspace => app.update(Msg::Edit(EditMsg::Delete))?,
KeyCode::Enter => app.update(Msg::Edit(EditMsg::InsertNewLine))?,
KeyCode::Tab => app.update(Msg::Edit(EditMsg::InsertTab))?,
KeyCode::Delete => app.update(Msg::Edit(EditMsg::DeleteNext))?,
KeyCode::Char(c) => app.update(Msg::Edit(EditMsg::InsertChar(c)))?,
KeyCode::Left => app.update(Msg::Edit(EditMsg::MoveLeft))?,
KeyCode::Right => app.update(Msg::Edit(EditMsg::MoveRight))?,
KeyCode::Esc => app.update(Msg::EnterViewMode)?,
_ => return Ok(UpdateConclusion(false)),
},
};
loop {
let msg = cmd.into_command().execute(dispatch.clone());
match msg {
Some(msg) => {
if let Msg::QuitApp = msg {
return Ok(UpdateConclusion(true));
}
cmd = app.update(msg)?;
}
None => break,
}
cmd = app.update(msg)?;
}
None => break,
}
}
Some(Err(e)) => {
tracing::warn!("failed to send event: {}", e);
}
None => {}
}
Ok(UpdateConclusion(false))
};
tokio::select! {
maybe_event = cross_event => {
let conclusion = handle_key_event(maybe_event)?;
return Ok(conclusion)
},
msg = receiver.next() => {
if let Some(msg) = msg {
if let Msg::QuitApp = msg {
return Ok(UpdateConclusion(true));
}
let mut cmd = app.update(msg)?;
loop {
let msg = cmd.into_command().execute(dispatch.clone());
match msg {
Some(msg) => {
if let Msg::QuitApp = msg {
return Ok(UpdateConclusion(true));
}
cmd = app.update(msg)?;
}
None => break,
}
}
}
}

View File

@@ -10,7 +10,9 @@ pub enum Msg {
MoveUp,
QuitApp,
OpenCreateItemDialog,
OpenCreateItemDialogBelow,
OpenEditItemDialog { item: GraphItem },
OpenEditor { item: GraphItem },
Interact,
EnterInsertMode,
@@ -20,11 +22,30 @@ pub enum Msg {
SubmitCommand { command: String },
Edit(EditMsg),
GraphUpdated(IOEvent<GraphItem>),
ItemCreated(IOEvent<()>),
ItemUpdated(IOEvent<()>),
SectionCreated(IOEvent<()>),
ItemToggled(IOEvent<()>),
Archive(IOEvent<()>),
OpenUpdateItemDialog(IOEvent<()>),
OpenItem(IOEvent<()>),
}
#[derive(Debug)]
pub enum IOEvent<T> {
Initialized,
Optimistic(T),
Success(T),
Failure(String),
}
impl IntoCommand for Msg {
fn into_command(self) -> crate::commands::Command {
Command::new(|| Some(self))
Command::new(|_| Some(self))
}
}

View File

@@ -0,0 +1,5 @@
use directories::ProjectDirs;
pub fn get_project_dir() -> ProjectDirs {
ProjectDirs::from("io", "kjuulh", "hyperlog").expect("to be able to get project dirs")
}

View File

@@ -0,0 +1,68 @@
use hyperlog_core::log::GraphItem;
use tonic::transport::Channel;
use crate::shared_engine::SharedEngine;
mod local;
mod remote;
#[derive(Clone)]
enum QuerierVariant {
Local(local::Querier),
Remote(remote::Querier),
}
#[derive(Clone)]
pub struct Querier {
variant: QuerierVariant,
}
impl Querier {
pub fn local(engine: &SharedEngine) -> Self {
Self {
variant: QuerierVariant::Local(local::Querier::new(engine)),
}
}
pub async fn remote(channel: Channel) -> anyhow::Result<Self> {
Ok(Self {
variant: QuerierVariant::Remote(remote::Querier::new(channel).await?),
})
}
pub fn get(
&self,
root: &str,
path: impl IntoIterator<Item = impl Into<String>>,
) -> Option<GraphItem> {
match &self.variant {
QuerierVariant::Local(querier) => querier.get(root, path),
QuerierVariant::Remote(_) => todo!(),
}
}
pub async fn get_async(
&self,
root: &str,
path: impl IntoIterator<Item = impl Into<String>>,
) -> anyhow::Result<Option<GraphItem>> {
match &self.variant {
QuerierVariant::Local(querier) => Ok(querier.get(root, path)),
QuerierVariant::Remote(querier) => querier.get(root, path).await,
}
}
pub fn get_available_roots(&self) -> Option<Vec<String>> {
match &self.variant {
QuerierVariant::Local(querier) => querier.get_available_roots(),
QuerierVariant::Remote(_) => todo!(),
}
}
pub async fn get_available_roots_async(&self) -> anyhow::Result<Option<Vec<String>>> {
match &self.variant {
QuerierVariant::Local(querier) => Ok(querier.get_available_roots()),
QuerierVariant::Remote(querier) => querier.get_available_roots().await,
}
}
}

View File

@@ -1,12 +1,17 @@
use crate::{log::GraphItem, shared_engine::SharedEngine};
use hyperlog_core::log::GraphItem;
use crate::shared_engine::SharedEngine;
#[derive(Clone)]
pub struct Querier {
engine: SharedEngine,
}
impl Querier {
pub fn new(engine: SharedEngine) -> Self {
Self { engine }
pub fn new(engine: &SharedEngine) -> Self {
Self {
engine: engine.clone(),
}
}
pub fn get_available_roots(&self) -> Option<Vec<String>> {
@@ -31,7 +36,10 @@ impl Querier {
path.len()
);
self.engine
.get(root, &path.iter().map(|i| i.as_str()).collect::<Vec<_>>())
let item = self
.engine
.get(root, &path.iter().map(|i| i.as_str()).collect::<Vec<_>>());
item
}
}

View File

@@ -0,0 +1,118 @@
use std::collections::BTreeMap;
use hyperlog_core::log::GraphItem;
use hyperlog_protos::hyperlog::{
graph_client::GraphClient, graph_item::Contents, GetAvailableRootsRequest, GetRequest,
};
use itertools::Itertools;
use tonic::transport::Channel;
#[allow(dead_code)]
#[derive(Clone)]
pub struct Querier {
channel: Channel,
}
#[allow(dead_code, unused_variables)]
impl Querier {
pub async fn new(channel: Channel) -> anyhow::Result<Self> {
Ok(Self { channel })
}
pub async fn get_available_roots(&self) -> anyhow::Result<Option<Vec<String>>> {
let channel = self.channel.clone();
let mut client = GraphClient::new(channel);
let request = tonic::Request::new(GetAvailableRootsRequest {});
let response = client.get_available_roots(request).await?;
let roots = response.into_inner();
if roots.roots.is_empty() {
Ok(None)
} else {
Ok(Some(roots.roots))
}
}
pub async fn get(
&self,
root: &str,
path: impl IntoIterator<Item = impl Into<String>>,
) -> anyhow::Result<Option<GraphItem>> {
let paths = path.into_iter().map(|i| i.into()).collect_vec();
tracing::debug!(
"quering: root:({}), path:({}), len: ({}))",
root,
paths.join("."),
paths.len()
);
let channel = self.channel.clone();
let mut client = GraphClient::new(channel);
let request = tonic::Request::new(GetRequest {
root: root.into(),
paths,
});
let response = client.get(request).await?;
let graph_item = response.into_inner();
if let Some(item) = graph_item.item {
let local_graph = transform_proto_to_local(&item);
Ok(local_graph)
} else {
Ok(None)
}
}
}
fn transform_proto_to_local(input: &hyperlog_protos::hyperlog::GraphItem) -> Option<GraphItem> {
match &input.contents {
Some(item) => match item {
Contents::User(user) => {
let mut items = BTreeMap::new();
for (key, value) in &user.items {
if let Some(item) = transform_proto_to_local(value) {
items.insert(key.clone(), item);
}
}
Some(GraphItem::User(items))
}
Contents::Section(section) => {
let mut items = BTreeMap::new();
for (key, value) in &section.items {
if let Some(item) = transform_proto_to_local(value) {
items.insert(key.clone(), item);
}
}
Some(GraphItem::Section(items))
}
Contents::Item(item) => Some(GraphItem::Item {
title: item.title.clone(),
description: item.description.clone(),
state: match &item.item_state {
Some(state) => match state {
hyperlog_protos::hyperlog::item_graph_item::ItemState::NotDone(_) => {
hyperlog_core::log::ItemState::NotDone
}
hyperlog_protos::hyperlog::item_graph_item::ItemState::Done(_) => {
hyperlog_core::log::ItemState::Done
}
},
None => hyperlog_core::log::ItemState::NotDone,
},
}),
},
None => None,
}
}

View File

@@ -1,6 +1,8 @@
use std::sync::{Arc, RwLock};
use crate::{engine::Engine, log::GraphItem};
use hyperlog_core::log::GraphItem;
use crate::engine::Engine;
#[derive(Clone)]
pub struct SharedEngine {
@@ -67,4 +69,8 @@ impl SharedEngine {
pub(crate) fn get_roots(&self) -> Option<Vec<String>> {
self.inner.read().unwrap().get_roots()
}
pub fn archive(&self, root: &str, path: &[&str]) -> anyhow::Result<()> {
self.inner.write().unwrap().archive(root, path)
}
}

View File

@@ -1,6 +1,6 @@
use std::{ops::Deref, sync::Arc};
use hyperlog_core::state::State;
use crate::core_state::State;
#[derive(Clone)]
pub struct SharedState {

View File

@@ -80,6 +80,13 @@ impl Storage {
pub fn clear_lock_file(self) {
let mut lock_file = self.lock_file.lock().unwrap();
if let Ok(lock) = self.state_lock() {
if lock.exists() {
tracing::info!("clearing lock file");
std::fs::remove_file(&lock).expect("to be able to remove lockfile");
}
}
if lock_file.is_some() {
*lock_file = None;
}
@@ -149,10 +156,9 @@ impl Storage {
mod test {
use std::collections::BTreeMap;
use hyperlog_core::log::GraphItem;
use similar_asserts::assert_eq;
use crate::log::GraphItem;
use super::*;
#[test]

View File

@@ -7,6 +7,7 @@ repository = "https://git.front.kjuulh.io/kjuulh/hyperlog"
[dependencies]
hyperlog-core.workspace = true
hyperlog-tui.workspace = true
hyperlog-server = { workspace = true, optional = true }
anyhow.workspace = true
tokio.workspace = true
@@ -15,21 +16,17 @@ tracing-subscriber.workspace = true
clap.workspace = true
dotenv.workspace = true
axum.workspace = true
serde.workspace = true
serde_json.workspace = true
uuid.workspace = true
serde = { version = "1.0.201", features = ["derive"] }
sqlx = { version = "0.7.4", features = [
"runtime-tokio",
"tls-rustls",
"postgres",
"uuid",
"time",
] }
uuid = { version = "1.8.0", features = ["v4"] }
tower-http = { version = "0.5.2", features = ["cors", "trace"] }
bus = "2.4.1"
dirs = "5.0.1"
dirs = "6.0.0"
[dev-dependencies]
similar-asserts = "1.5.0"
tempfile = "3.10.1"
[features]
default = ["include_server"]
include_server = ["dep:hyperlog-server"]

View File

@@ -1 +0,0 @@
-- Add migration script here

View File

@@ -1,22 +1,43 @@
use std::net::SocketAddr;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use hyperlog_core::{commander, state};
use crate::server::serve;
use clap::{Parser, Subcommand, ValueEnum};
use hyperlog_tui::{
commander,
core_state::{Backend, State},
};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Command {
#[command(subcommand)]
command: Option<Commands>,
#[arg(long, default_value = "local")]
backend: BackendArg,
#[arg(long = "backend-url", required_if_eq("backend", "remote"))]
backend_url: Option<String>,
#[arg(long = "local-path")]
local_path: Option<PathBuf>,
}
#[derive(ValueEnum, Clone)]
enum BackendArg {
Local,
Remote,
}
#[derive(Subcommand)]
enum Commands {
#[cfg(feature = "include_server")]
Serve {
#[arg(env = "SERVICE_HOST", long, default_value = "127.0.0.1:3000")]
host: SocketAddr,
#[arg(env = "EXTERNAL_HOST", long, default_value = "127.0.0.1:3000")]
external_host: std::net::SocketAddr,
#[arg(env = "INTERNAL_HOST", long, default_value = "127.0.0.1:3001")]
internal_host: std::net::SocketAddr,
#[arg(env = "EXTERNAL_GRPC_HOST", long, default_value = "127.0.0.1:4000")]
external_grpc_host: std::net::SocketAddr,
},
Exec {
#[command(subcommand)]
@@ -70,58 +91,97 @@ pub async fn execute() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
}
let state = state::State::new()?;
let backend = cli.backend;
let backend_url = cli.backend_url;
let backend = match backend {
BackendArg::Local => Backend::Local {
path_override: cli.local_path.clone(),
},
BackendArg::Remote => Backend::Remote {
url: backend_url.expect("backend-url to be set"),
},
};
match cli.command {
Some(Commands::Serve { host }) => {
#[cfg(feature = "include_server")]
Some(Commands::Serve {
external_host,
internal_host,
external_grpc_host,
}) => {
tracing::info!("Starting service");
serve(host).await?;
hyperlog_server::serve(hyperlog_server::ServeOptions {
external_http: external_host,
internal_http: internal_host,
external_grpc: external_grpc_host,
})
.await?;
}
Some(Commands::Exec { commands }) => match commands {
ExecCommands::CreateRoot { root } => state
.commander
.execute(commander::Command::CreateRoot { root })?,
ExecCommands::CreateSection { root, path } => {
state.commander.execute(commander::Command::CreateSection {
root,
path: path
.unwrap_or_default()
.split('.')
.map(|s| s.to_string())
.filter(|s| !s.is_empty())
.collect::<Vec<String>>(),
})?
Some(Commands::Exec { commands }) => {
let state = State::new(backend).await?;
match commands {
ExecCommands::CreateRoot { root } => {
state
.commander
.execute(commander::Command::CreateRoot { root })
.await?
}
ExecCommands::CreateSection { root, path } => {
state
.commander
.execute(commander::Command::CreateSection {
root,
path: path
.unwrap_or_default()
.split('.')
.map(|s| s.to_string())
.filter(|s| !s.is_empty())
.collect::<Vec<String>>(),
})
.await?
}
}
},
Some(Commands::Query { commands }) => match commands {
QueryCommands::Get { root, path } => {
let res = state.querier.get(
&root,
path.unwrap_or_default()
.split('.')
.filter(|s| !s.is_empty()),
);
}
Some(Commands::Query { commands }) => {
let state = State::new(backend).await?;
match commands {
QueryCommands::Get { root, path } => {
let res = state.querier.get(
&root,
path.unwrap_or_default()
.split('.')
.filter(|s| !s.is_empty()),
);
let output = serde_json::to_string_pretty(&res)?;
let output = serde_json::to_string_pretty(&res)?;
println!("{}", output);
println!("{}", output);
}
}
},
}
Some(Commands::CreateRoot { name }) => {
let state = State::new(backend).await?;
state
.commander
.execute(commander::Command::CreateRoot { root: name })?;
.execute(commander::Command::CreateRoot { root: name })
.await?;
println!("Root was successfully created, now run:\n\n$ hyperlog");
}
Some(Commands::Info {}) => {
println!("graph stored at: {}", state.storage.info()?)
let state = State::new(backend).await?;
if let Some(info) = state.info() {
println!("graph stored at: {}", info?);
}
}
Some(Commands::ClearLock {}) => {
state.storage.clear_lock_file();
let state = State::new(backend).await?;
state.unlock();
println!("cleared lock file");
}
None => {
let state = State::new(backend).await?;
hyperlog_tui::execute(state).await?;
}
}

View File

@@ -1,6 +1,4 @@
mod cli;
pub(crate) mod server;
pub(crate) mod state;
#[tokio::main]
async fn main() -> anyhow::Result<()> {

View File

@@ -1,42 +1 @@
use std::{net::SocketAddr, sync::Arc};
use axum::{extract::MatchedPath, http::Request, routing::get, Router};
use tower_http::trace::TraceLayer;
use crate::state::{SharedState, State};
async fn root() -> &'static str {
"Hello, hyperlog!"
}
pub async fn serve(host: SocketAddr) -> anyhow::Result<()> {
let state = SharedState(Arc::new(State::new().await?));
let app = Router::new()
.route("/", get(root))
.with_state(state.clone())
.layer(
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
// Log the matched route's path (with placeholders not filled in).
// Use request.uri() or OriginalUri if you want the real path.
let matched_path = request
.extensions()
.get::<MatchedPath>()
.map(MatchedPath::as_str);
tracing::info_span!(
"http_request",
method = ?request.method(),
matched_path,
some_other_field = tracing::field::Empty,
)
}), // ...
);
tracing::info!("listening on {}", host);
let listener = tokio::net::TcpListener::bind(host).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
Ok(())
}

View File

@@ -1,37 +1 @@
use std::{ops::Deref, sync::Arc};
use anyhow::Context;
use sqlx::{Pool, Postgres};
#[derive(Clone)]
pub struct SharedState(pub Arc<State>);
impl Deref for SharedState {
type Target = Arc<State>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub struct State {
pub _db: Pool<Postgres>,
}
impl State {
pub async fn new() -> anyhow::Result<Self> {
let db = sqlx::PgPool::connect(
&std::env::var("DATABASE_URL").context("DATABASE_URL is not set")?,
)
.await?;
sqlx::migrate!("migrations/crdb")
.set_locking(false)
.run(&db)
.await?;
let _ = sqlx::query("SELECT 1;").fetch_one(&db).await?;
Ok(Self { _db: db })
}
}

View File

@@ -1,11 +1,34 @@
# yaml-language-server: $schema=https://git.front.kjuulh.io/kjuulh/cuddle/raw/branch/main/schemas/base.json
base: "git@git.front.kjuulh.io:kjuulh/cuddle-rust-cli-plan.git"
base: "git@git.front.kjuulh.io:kjuulh/cuddle-rust-service-plan.git"
vars:
service: "hyperlog"
registry: kasperhermansen
database:
crdb: "true"
ingress:
- external: "true"
- internal: "true"
- external_grpc: "true"
- internal_grpc: "true"
cuddle/clusters:
dev:
env:
external.host: "0.0.0.0:3000"
internal.host: "0.0.0.0:3001"
external.grpc.host: "0.0.0.0:4000"
rust.log: hyperlog=trace
prod:
env:
external.host: "0.0.0.0:3000"
internal.host: "0.0.0.0:3001"
external.grpc.host: "0.0.0.0:4000"
rust.log: hyperlog=trace
please:
project:
owner: kjuulh
@@ -15,3 +38,10 @@ please:
api_url: https://git.front.kjuulh.io
actions:
rust:
scripts:
dev:
type: shell
install:
type: shell

187
demo.cast
View File

@@ -1,187 +0,0 @@
{"version": 2, "width": 121, "height": 31, "timestamp": 1715413204, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}}
[0.28385, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[0.356577, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[38;2;255;153;102mhyperlog\u001b[0m \u001b[90mmain\u001b[0m\u001b[38;2;255;153;102m \u001b[0m\u001b[1;31mrs \u001b[0m\r\n\u001b[38;2;255;153;102m\u001b[0m \u001b[K"]
[0.35759, "o", "\u001b[6 q"]
[0.358564, "o", "\u001b[6 q"]
[0.358794, "o", "\u001b[?2004h"]
[1.196152, "o", "c"]
[1.200489, "o", "\b\u001b[32mc\u001b[39m"]
[1.241408, "o", "\b\u001b[32mc\u001b[39m\u001b[90mlear\u001b[39m\b\b\b\b"]
[1.340197, "o", "\b\u001b[32mc\u001b[32ma\u001b[39m\u001b[39m \u001b[39m \u001b[39m \b\b\b"]
[1.369897, "o", "\b\b\u001b[1m\u001b[31mc\u001b[1m\u001b[31ma\u001b[0m\u001b[39m"]
[1.37249, "o", "\u001b[90mrgo run\u001b[39m\b\b\b\b\b\b\b"]
[1.424331, "o", "\b\b\u001b[1m\u001b[31mc\u001b[1m\u001b[31ma\u001b[1m\u001b[31mr\u001b[0m\u001b[39m"]
[1.592372, "o", "\b\u001b[1m\u001b[31mr\u001b[1m\u001b[31mg\u001b[0m\u001b[39m"]
[1.666639, "o", "\b\u001b[1m\u001b[31mg\u001b[1m\u001b[31mo\u001b[0m\u001b[39m"]
[1.671179, "o", "\b\b\b\b\b\u001b[0m\u001b[32mc\u001b[0m\u001b[32ma\u001b[0m\u001b[32mr\u001b[0m\u001b[32mg\u001b[0m\u001b[32mo\u001b[39m"]
[2.080073, "o", "\b\u001b[32mo\u001b[32m \u001b[39m"]
[2.084222, "o", "\b\b\u001b[32mo\u001b[39m\u001b[39m "]
[2.246919, "o", "\u001b[39mr"]
[2.252663, "o", "\b\u001b[4mr\u001b[24m"]
[2.347417, "o", "\b\u001b[4mr\u001b[39m\u001b[4mu\u001b[24m"]
[2.351929, "o", "\b\b\u001b[24mr\u001b[24mu"]
[2.501908, "o", "\u001b[39mn"]
[2.800437, "o", "\u001b[?1l\u001b>"]
[2.800875, "o", "\u001b[?2004l"]
[2.807783, "o", "\u001b[0 q"]
[2.808107, "o", "\r\r\n"]
[3.35867, "o", "\u001b[1m\u001b[32m Finished\u001b[0m `dev` profile [unoptimized + debuginfo] target(s) in 0.49s\r\n"]
[3.365514, "o", "\u001b[1m\u001b[32m Running\u001b[0m `target/debug/hyperlog`\r\n"]
[3.740156, "o", "\u001b[?1049h"]
[3.743543, "o", "\u001b[1;1H\u001b[38;5;2mhyperlog\u001b[2;1H\u001b[39m─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[3;1Hsomething\u001b[3;11H~\u001b[3;13H(items:\u001b[3;21H3)\u001b[4;5Ha\u001b[4;7H~\u001b[4;9H(items:\u001b[4;17H2)\u001b[5;5H\u001b[38;5;8m...\u001b[6;5H\u001b[39mc\u001b[6;7H~\u001b[6;9H(items:\u001b[6;17H0)\u001b[31;2H--\u001b[31;5HVIEW\u001b[31;10H--\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.001932, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.260086, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.458203, "o", "\u001b[3;1H\u001b[38;2;255;165;0msomething ~ (items: 3)\u001b[5;5H\u001b[39m \u001b[5;9H[\u001b[5;11H]\u001b[5;13Hitem\u001b[6;5H \u001b[6;7H \u001b[6;9H[ ] something\u001b[7;5Hb\u001b[7;7H~\u001b[7;9H(items:\u001b[7;17H1)\u001b[8;9H[\u001b[8;11H]\u001b[8;13Hitem\u001b[9;5Hc\u001b[9;7H~\u001b[9;9H(items:\u001b[9;17H0)\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.716099, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.974526, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.235168, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.471231, "o", "\u001b[31;1H: \u001b[31;5H \u001b[31;10H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.670365, "o", "\u001b[31;2Hs\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.776552, "o", "\u001b[31;3Hh\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.907952, "o", "\u001b[31;4Ho\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.035263, "o", "\u001b[31;5Ht\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.187704, "o", "\u001b[31;6H-\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.299824, "o", "\u001b[31;7Ha\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.438169, "o", "\u001b[31;8Hl\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.577971, "o", "\u001b[31;9Hl\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.836633, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.968556, "o", "\u001b[31;9H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.106562, "o", "\u001b[31;8H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.251411, "o", "\u001b[31;7H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.395354, "o", "\u001b[31;6H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.54286, "o", "\u001b[31;5H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.731859, "o", "\u001b[31;5Hw\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.989235, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.121554, "o", "\u001b[31;6H-\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.299653, "o", "\u001b[31;7Ha\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.399794, "o", "\u001b[31;8Hl\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.549891, "o", "\u001b[31;9Hl\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.785214, "o", "\u001b[9;17H5\u001b[10;9H[\u001b[38;2;127;255;0mx\u001b[39m]\u001b[10;13Hitem\u001b[11;9H\u001b[38;5;8m...\u001b[12;9H\u001b[39m[\u001b[38;2;127;255;0mx\u001b[39m]\u001b[12;13Hitem-d\u001b[31;1H -- VIEW --\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.044234, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.303396, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.554819, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.813405, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.073436, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.077833, "o", "\u001b[3;1Hsomething ~ (items: 3)\u001b[5;5H\u001b[38;5;8m...\u001b[5;9H\u001b[39m \u001b[5;11H \u001b[5;13H \u001b[6;5Hc\u001b[6;7H~\u001b[6;9H(items: 5) \u001b[7;5H \u001b[7;7H \u001b[7;9H \u001b[7;17H \u001b[8;9H \u001b[8;11H \u001b[8;13H \u001b[9;5H \u001b[9;7H \u001b[9;9H \u001b[9;17H \u001b[10;9H \u001b[10;13H \u001b[11;9H \u001b[12;9H \u001b[12;13H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.33073, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.590022, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.750514, "o", "\u001b[3;1H\u001b[38;2;255;165;0msomething ~ (items: 3)\u001b[5;5H\u001b[39m \u001b[5;9H[\u001b[5;11H]\u001b[5;13Hitem\u001b[6;5H \u001b[6;7H \u001b[6;9H[ ] something\u001b[7;5Hb\u001b[7;7H~\u001b[7;9H(items:\u001b[7;17H1)\u001b[8;9H[\u001b[8;11H]\u001b[8;13Hitem\u001b[9;5Hc\u001b[9;7H~\u001b[9;9H(items:\u001b[9;17H5)\u001b[10;9H[\u001b[38;2;127;255;0mx\u001b[39m]\u001b[10;13Hitem\u001b[11;9H\u001b[38;5;8m...\u001b[12;9H\u001b[39m[\u001b[38;2;127;255;0mx\u001b[39m]\u001b[12;13Hitem-d\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.009542, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.101987, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.310184, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.569878, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.646194, "o", "\u001b[3;1Hsomething ~ (items: 3)\u001b[4;5H\u001b[38;2;255;165;0ma ~ (items: 2)\u001b[7;5H\u001b[39m \u001b[7;7H \u001b[7;9H \u001b[7;17H \u001b[8;5Hb\u001b[8;7H~\u001b[8;9H(items: 1)\u001b[9;5H \u001b[9;7H \u001b[9;9H[ ] item \u001b[10;5Hc\u001b[10;7H~\u001b[10;9H(items: 5)\u001b[11;9H[\u001b[38;2;127;255;0mx\u001b[39m]\u001b[11;13Hitem\u001b[12;9H\u001b[38;5;8m...\u001b[12;13H\u001b[39m \u001b[13;9H[\u001b[38;2;127;255;0mx\u001b[39m]\u001b[13;13Hitem-d\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.905614, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.9512, "o", "\u001b[4;5Ha ~ (items: 2)\u001b[7;5H\u001b[38;2;255;165;0mb ~ (items: 1)\u001b[8;5H\u001b[39m \u001b[8;7H \u001b[8;9H[ ] item \u001b[9;9H \u001b[9;11H \u001b[9;13H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.171517, "o", "\u001b[7;5Hb ~ (items: 1)\u001b[9;5H\u001b[38;2;255;165;0mc ~ (items: 5)\u001b[10;5H\u001b[39m \u001b[10;7H \u001b[10;9H[\u001b[38;2;127;255;0mx\u001b[39m] item \u001b[11;17H-a\u001b[12;9H[\u001b[38;2;127;255;0mx\u001b[39m]\u001b[12;13Hitem-b\u001b[13;18Hc\u001b[14;9H[\u001b[38;2;127;255;0mx\u001b[39m]\u001b[14;13Hitem-d\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.428831, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.687811, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.710816, "o", "\u001b[4;5Hc\u001b[4;17H5\u001b[5;9H\u001b[38;2;255;165;0m[x] item\u001b[6;10H\u001b[38;2;127;255;0mx\u001b[6;13H\u001b[39mitem-a \u001b[7;5H \u001b[7;7H \u001b[7;9H[\u001b[38;2;127;255;0mx\u001b[39m] item-b\u001b[8;10H\u001b[38;2;127;255;0mx\u001b[8;17H\u001b[39m-c\u001b[9;5H [\u001b[38;2;127;255;0mx\u001b[39m] item-d\u001b[10;9H \u001b[10;13H \u001b[11;9H \u001b[11;13H \u001b[12;9H \u001b[12;13H \u001b[13;9H \u001b[13;13H \u001b[14;9H \u001b[14;13H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.970938, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.229238, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.402499, "o", "\u001b[5;10H\u001b[38;2;255;165;0m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.661623, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.709679, "o", "\u001b[5;9H[ ] item\u001b[6;9H\u001b[38;2;255;165;0m[x] item-a\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.969177, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.980927, "o", "\u001b[6;10H\u001b[38;2;255;165;0m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.240333, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.453095, "o", "\u001b[4;5Ha\u001b[4;17H2\u001b[6;9H[ ] something\u001b[7;5Hb\u001b[7;7H~\u001b[7;9H(items: 1)\u001b[8;10H \u001b[8;17H \u001b[9;5H\u001b[38;2;255;165;0mc ~ (items: 5)\u001b[10;9H\u001b[39m[\u001b[10;11H]\u001b[10;13Hitem\u001b[11;9H[\u001b[11;11H]\u001b[11;13Hitem-a\u001b[12;9H[\u001b[38;2;127;255;0mx\u001b[39m]\u001b[12;13Hitem-b\u001b[13;9H[\u001b[38;2;127;255;0mx\u001b[39m]\u001b[13;13Hitem-c\u001b[14;9H[\u001b[38;2;127;255;0mx\u001b[39m]\u001b[14;13Hitem-d\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.638745, "o", "\u001b[3;1H\u001b[38;2;255;165;0msomething ~ (items: 3)\u001b[9;5H\u001b[39mc ~ (items: 5)\u001b[11;9H\u001b[38;5;8m...\u001b[11;13H\u001b[39m \u001b[12;18Hd\u001b[13;9H \u001b[13;13H \u001b[14;9H \u001b[14;13H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.897952, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.0327, "o", "\u001b[31;1H: \u001b[31;5H \u001b[31;10H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.291396, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.549911, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.658184, "o", "\u001b[31;2Hh\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.809793, "o", "\u001b[31;3Hi\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.949529, "o", "\u001b[31;4Hd\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.096886, "o", "\u001b[31;5He\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.254773, "o", "\u001b[31;6H-\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.470747, "o", "\u001b[31;7Hd\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.551481, "o", "\u001b[31;8Ho\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.709128, "o", "\u001b[31;9Hn\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.827375, "o", "\u001b[31;10He\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.005258, "o", "\u001b[9;17H2\u001b[11;9H[ ]\u001b[11;13Hitem-a\u001b[12;9H \u001b[12;13H \u001b[31;1H -- VIEW --\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.263877, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.523133, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.570586, "o", "\u001b[3;1Hsomething ~ (items: 3)\u001b[4;5H\u001b[38;2;255;165;0ma ~ (items: 2)\u001b[7;5H\u001b[39m \u001b[7;7H \u001b[7;9H \u001b[7;17H \u001b[8;5Hb\u001b[8;7H~\u001b[8;9H(items: 1)\u001b[9;5H \u001b[9;7H \u001b[9;9H[ ] item \u001b[10;5Hc\u001b[10;7H~\u001b[10;9H(items: 2)\u001b[11;17H \u001b[12;9H[\u001b[12;11H]\u001b[12;13Hitem-a\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.829292, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.986953, "o", "\u001b[4;5Ha ~ (items: 2)\u001b[7;5H\u001b[38;2;255;165;0mb ~ (items: 1)\u001b[8;5H\u001b[39m \u001b[8;7H \u001b[8;9H[ ] item \u001b[9;9H \u001b[9;11H \u001b[9;13H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.191003, "o", "\u001b[7;5Hb ~ (items: 1)\u001b[9;5H\u001b[38;2;255;165;0mc ~ (items: 2)\u001b[10;5H\u001b[39m \u001b[10;7H \u001b[10;9H[ ] item \u001b[11;17H-a\u001b[12;9H \u001b[12;11H \u001b[12;13H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.44961, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.709575, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.968623, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.193329, "o", "\u001b[31;1H: \u001b[31;5H \u001b[31;10H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.45148, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.45588, "o", "\u001b[31;2Hs\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.506083, "o", "\u001b[31;3Hh\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.767374, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.772362, "o", "\u001b[31;4Hw\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.031712, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.156021, "o", "\u001b[31;4H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.414819, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.422959, "o", "\u001b[31;4Ho\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.564511, "o", "\u001b[31;5Hw\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.686556, "o", "\u001b[31;6H-\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[20.891174, "o", "\u001b[31;7Ha\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.014086, "o", "\u001b[31;8Hl\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.168709, "o", "\u001b[31;9Hl\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.427999, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.436176, "o", "\u001b[9;17H\u001b[38;2;255;165;0m5\u001b[12;9H\u001b[39m[\u001b[38;2;127;255;0mx\u001b[39m]\u001b[12;13Hitem-b\u001b[13;9H[\u001b[38;2;127;255;0mx\u001b[39m]\u001b[13;13Hitem-c\u001b[14;9H[\u001b[38;2;127;255;0mx\u001b[39m]\u001b[14;13Hitem-d\u001b[31;1H -- VIEW --\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.695984, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[21.957095, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.04995, "o", "\u001b[7;5H\u001b[38;2;255;165;0mb ~ (items: 1)\u001b[9;5H\u001b[39m \u001b[10;5Hc\u001b[10;7H~\u001b[10;9H(items: 5)\u001b[11;17H \u001b[12;9H\u001b[38;5;8m...\u001b[12;13H\u001b[39m \u001b[13;18Hd\u001b[14;9H \u001b[14;13H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.311034, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.568875, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[22.795873, "o", "\u001b[4;5Hb\u001b[4;17H1\u001b[5;9H\u001b[38;2;255;165;0m[ ] item\u001b[6;9H\u001b[39m \u001b[6;11H \u001b[6;13H \u001b[7;5H \u001b[8;9H \u001b[8;11H \u001b[8;13H \u001b[10;5H \u001b[10;7H \u001b[10;9H \u001b[10;17H \u001b[11;9H \u001b[11;11H \u001b[11;13H \u001b[12;9H \u001b[13;9H \u001b[13;13H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.054478, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.24198, "o", "\u001b[5;10H\u001b[38;2;255;165;0mx\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.500408, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.646615, "o", "\u001b[4;5Ha\u001b[4;17H2\u001b[5;9H[ ] item\u001b[6;9H[\u001b[6;11H]\u001b[6;13Hsomething\u001b[7;5H\u001b[38;2;255;165;0mb ~ (items: 1)\u001b[8;9H\u001b[39m[\u001b[38;2;127;255;0mx\u001b[39m]\u001b[8;13Hitem\u001b[10;5Hc\u001b[10;7H~\u001b[10;9H(items:\u001b[10;17H5)\u001b[11;9H[\u001b[11;11H]\u001b[11;13Hitem\u001b[12;9H\u001b[38;5;8m...\u001b[13;9H\u001b[39m[\u001b[38;2;127;255;0mx\u001b[39m]\u001b[13;13Hitem-d\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[23.90405, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.162272, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.335133, "o", "\u001b[31;1H: \u001b[31;5H \u001b[31;10H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.590424, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.825885, "o", "\u001b[31;2Hh\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[24.994253, "o", "\u001b[31;3Hi\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.102804, "o", "\u001b[31;4Hd\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.2455, "o", "\u001b[31;5He\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.327672, "o", "\u001b[31;6H-\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.47797, "o", "\u001b[31;7Hd\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.603255, "o", "\u001b[31;8Ho\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.749521, "o", "\u001b[31;9Hn\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[25.862533, "o", "\u001b[31;10He\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.026357, "o", "\u001b[7;17H\u001b[38;2;255;165;0m0\u001b[8;5H\u001b[39mc\u001b[8;7H~\u001b[8;9H(items: 2)\u001b[9;9H[\u001b[9;11H]\u001b[9;13Hitem\u001b[10;5H \u001b[10;7H \u001b[10;9H[ ] item-a\u001b[11;9H \u001b[11;11H \u001b[11;13H \u001b[12;9H \u001b[13;9H \u001b[13;13H \u001b[31;1H -- VIEW --\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.285752, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.546241, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[26.80384, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.061451, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.318053, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.435387, "o", "\u001b[31;1H: \u001b[31;5H \u001b[31;10H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.678991, "o", "\u001b[31;2Hq\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[27.801458, "o", "\u001b[?1049l\u001b[?25h"]
[27.803865, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[27.863445, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[38;2;255;153;102mhyperlog\u001b[0m \u001b[90mmain\u001b[0m\u001b[38;2;255;153;102m \u001b[0m\u001b[1;31mrs \u001b[0m\u001b[33m25s\u001b[0m \r\n\u001b[38;2;255;153;102m\u001b[0m \u001b[K"]
[27.864508, "o", "\u001b[6 q"]
[27.865397, "o", "\u001b[6 q"]
[27.865567, "o", "\u001b[?2004h"]
[28.397871, "o", "c"]
[28.402538, "o", "\b\u001b[32mc\u001b[39m"]
[28.440037, "o", "\b\u001b[32mc\u001b[39m\u001b[90margo run\u001b[39m\u001b[8D"]
[28.478861, "o", "\b\u001b[32mc\u001b[32ml\u001b[39m\u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \b\b\b\b\b\b\b"]
[28.484213, "o", "\b\b\u001b[1m\u001b[31mc\u001b[1m\u001b[31ml\u001b[0m\u001b[39m"]
[28.505375, "o", "\u001b[90mear\u001b[39m\b\b\b"]
[28.619796, "o", "\b\b\u001b[1m\u001b[31mc\u001b[1m\u001b[31ml\u001b[1m\u001b[31me\u001b[0m\u001b[39m"]
[28.674043, "o", "\b\u001b[1m\u001b[31me\u001b[1m\u001b[31ma\u001b[0m\u001b[39m"]
[28.762176, "o", "\b\u001b[1m\u001b[31ma\u001b[1m\u001b[31mr\u001b[0m\u001b[39m"]
[28.764782, "o", "\b\b\b\b\b\u001b[0m\u001b[32mc\u001b[0m\u001b[32ml\u001b[0m\u001b[32me\u001b[0m\u001b[32ma\u001b[0m\u001b[32mr\u001b[39m"]
[28.816778, "o", "\u001b[?1l\u001b>"]
[28.817025, "o", "\u001b[?2004l"]
[28.821673, "o", "\u001b[0 q"]
[28.82202, "o", "\r\r\n"]
[28.866023, "o", "\u001b[3J\u001b[H\u001b[2J"]
[28.866306, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[28.910374, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[38;2;255;153;102mhyperlog\u001b[0m \u001b[90mmain\u001b[0m\u001b[38;2;255;153;102m \u001b[0m\u001b[1;31mrs \u001b[0m\r\n\u001b[38;2;255;153;102m\u001b[0m \u001b[K"]
[28.911365, "o", "\u001b[6 q"]
[28.912156, "o", "\u001b[6 q"]
[28.91233, "o", "\u001b[?2004h"]
[29.510648, "o", "\u001b[?2004l\r\r\n"]

15
scripts/dev.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env zsh
echo "starting services"
docker compose -f templates/docker-compose.yaml up -d --remove-orphans
sleep 5
tear_down() {
echo "cleaning up services in the background"
(docker compose -f templates/docker-compose.yaml down -v &) > /dev/null 2>&1
}
trap tear_down SIGINT
RUST_LOG=info,hyperlog=trace cargo watch -x 'run -F include_server -- serve'

5
scripts/install.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env zsh
set -eo pipefail
cargo install --path crates/hyperlog --force

View File

@@ -1,4 +1,3 @@
version: "3"
services:
crdb:
restart: 'always'
@@ -11,5 +10,5 @@ services:
retries: 5
start_period: '20s'
ports:
- 8080:8080
- 28080:8080
- '26257:26257'