Compare commits
140 Commits
v0.1.0
...
7c6d8ffc54
Author | SHA1 | Date | |
---|---|---|---|
7c6d8ffc54 | |||
10f832c1d1 | |||
c57b1bc434 | |||
6b2cc8925a | |||
e9b810837e | |||
12fd200185 | |||
bb4e6ba6ab | |||
66099d5bb7 | |||
817d1db963 | |||
4e5ed0c27f | |||
245aa67f09 | |||
43593c5852 | |||
657b11c3f7 | |||
8ce42aac25 | |||
ada480ea02 | |||
8a995b191c | |||
4897e56d7d | |||
c69ebb0fe4 | |||
0b71fd889e | |||
68f6ce63e5 | |||
e7e169352d | |||
fc392ac43b | |||
25f854f2fe | |||
eaaa63e0b6 | |||
000f96f965 | |||
8870b89378 | |||
2704b0b0c3 | |||
39b05501f1 | |||
2d4ae16de8 | |||
f036db202d | |||
384ee78652 | |||
f5ca5970c1 | |||
6a179f0881 | |||
09546907e5
|
|||
83f9816cce | |||
c261d6cb65 | |||
fb01406738 | |||
352fd86145 | |||
bea5258e8f | |||
a0a256ac7f | |||
9cd12f8636 | |||
34fba9754c
|
|||
ee0680194b | |||
38f8db78cd | |||
02b8b8cd59 | |||
09fb11f9d9 | |||
102b35e083 | |||
986d261bd7 | |||
0f70cf9f7b | |||
eb30858d9d | |||
22900ba92a | |||
44ef8a708c | |||
acc7e0cd6d | |||
e8d222f4ba | |||
3c5fb25fa3 | |||
b3b170c057 | |||
e64fc61926
|
|||
4eb1a8224a | |||
6dfe2bf0d6 | |||
0e38e23942 | |||
c35aad3cf8 | |||
10c2282c78 | |||
35c4fae36a | |||
b11d72ca05 | |||
a97ef32ffb | |||
2873ff3d7e | |||
4096790f2a | |||
0ce0e691ee | |||
40dfbcd031 | |||
4ff3261dbc | |||
53c2cdf018 | |||
c96eebf6f9 | |||
9d9d6be3b7 | |||
f106929cca | |||
d876891242 | |||
6961987a77 | |||
d3695eba50 | |||
f175c4ebcf | |||
58df153c6e | |||
2b7a05bc4e | |||
fcb0ea7393 | |||
3a1741d7dc | |||
8886af4a8f | |||
08a6f77146 | |||
c15c7f0ae2 | |||
04c6e97f30 | |||
29e0c37599 | |||
e655c57f21 | |||
28fb99e6f9 | |||
3a231cea96 | |||
b3c1784cae | |||
|
a790e7f039 | ||
20190ac784
|
|||
2df2412d39 | |||
615484d1cd
|
|||
|
530314c0f8 | ||
67d4fe31b3
|
|||
d0d5efc3e3 | |||
9587c60e72
|
|||
710fb431f7
|
|||
be28b4ff80
|
|||
73c6ba25b1
|
|||
3f22148bfa
|
|||
d773eff6fa
|
|||
4859c2767c
|
|||
e3bb6b0f60
|
|||
9159ce4eee
|
|||
b6c3dd1a80
|
|||
119731e2a0
|
|||
98feed2d71
|
|||
a70f1cd4b4 | |||
0927d36505 | |||
bc2b47a6c5 | |||
44d9ed2790 | |||
118aeb3898 | |||
65c2466f97
|
|||
832587b51d
|
|||
4ad8120cb5
|
|||
1f0f526e38
|
|||
91ad8eda01
|
|||
7496d0e964
|
|||
208b14583e
|
|||
4a91a564bf
|
|||
364f3992bb | |||
837caee5db
|
|||
816869e6f9
|
|||
7bdf8393b1
|
|||
699bac7159
|
|||
76f1c87663
|
|||
64d59e069f
|
|||
9bb5bc9e87
|
|||
2d63d3ad4c
|
|||
9cb3296cec
|
|||
874045dca8
|
|||
5548d8e36e
|
|||
cf26422673
|
|||
4a0fcd1bbb
|
|||
86cba91b16
|
|||
113c646334
|
|||
6a147ba0d2
|
@@ -1,2 +1,2 @@
|
||||
kind: template
|
||||
load: cuddle-rust-cli-plan.yaml
|
||||
load: cuddle-rust-service-plan.yaml
|
||||
|
115
CHANGELOG.md
115
CHANGELOG.md
@@ -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
|
||||
|
1774
Cargo.lock
generated
1774
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
15
Cargo.toml
@@ -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
4
buf.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
version: v2
|
||||
modules:
|
||||
- path: crates/hyperlog-protos/proto
|
||||
name: buf.build/noschemaplz/hyperlog
|
@@ -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"
|
||||
|
@@ -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;
|
||||
|
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
12
crates/hyperlog-protos/Cargo.toml
Normal file
12
crates/hyperlog-protos/Cargo.toml
Normal 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"
|
3
crates/hyperlog-protos/build.rs
Normal file
3
crates/hyperlog-protos/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tonic_build::compile_protos("proto/hyperlog.proto").unwrap();
|
||||
}
|
99
crates/hyperlog-protos/proto/hyperlog.proto
Normal file
99
crates/hyperlog-protos/proto/hyperlog.proto
Normal 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;
|
||||
}
|
3
crates/hyperlog-protos/src/lib.rs
Normal file
3
crates/hyperlog-protos/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod hyperlog {
|
||||
tonic::include_proto!("hyperlog"); // Specify the same package name as in your .proto file
|
||||
}
|
30
crates/hyperlog-server/Cargo.toml
Normal file
30
crates/hyperlog-server/Cargo.toml
Normal 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"
|
@@ -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);
|
||||
|
@@ -0,0 +1,3 @@
|
||||
-- Add migration script here
|
||||
|
||||
ALTER TABLE nodes ADD COLUMN status VARCHAR(20) DEFAULT 'active' NOT NULL;
|
170
crates/hyperlog-server/src/commands.rs
Normal file
170
crates/hyperlog-server/src/commands.rs
Normal 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(),
|
||||
)
|
||||
}
|
||||
}
|
486
crates/hyperlog-server/src/external_grpc.rs
Normal file
486
crates/hyperlog-server/src/external_grpc.rs
Normal 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(())
|
||||
}
|
40
crates/hyperlog-server/src/external_http.rs
Normal file
40
crates/hyperlog-server/src/external_http.rs
Normal 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(())
|
||||
}
|
40
crates/hyperlog-server/src/internal_http.rs
Normal file
40
crates/hyperlog-server/src/internal_http.rs
Normal 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(())
|
||||
}
|
50
crates/hyperlog-server/src/lib.rs
Normal file
50
crates/hyperlog-server/src/lib.rs
Normal 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(())
|
||||
}
|
62
crates/hyperlog-server/src/querier.rs
Normal file
62
crates/hyperlog-server/src/querier.rs
Normal 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())
|
||||
}
|
||||
}
|
9
crates/hyperlog-server/src/services.rs
Normal file
9
crates/hyperlog-server/src/services.rs
Normal 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;
|
70
crates/hyperlog-server/src/services/archive.rs
Normal file
70
crates/hyperlog-server/src/services/archive.rs
Normal 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())
|
||||
}
|
||||
}
|
104
crates/hyperlog-server/src/services/create_item.rs
Normal file
104
crates/hyperlog-server/src/services/create_item.rs
Normal 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())
|
||||
}
|
||||
}
|
38
crates/hyperlog-server/src/services/create_root.rs
Normal file
38
crates/hyperlog-server/src/services/create_root.rs
Normal 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())
|
||||
}
|
||||
}
|
61
crates/hyperlog-server/src/services/create_section.rs
Normal file
61
crates/hyperlog-server/src/services/create_section.rs
Normal 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())
|
||||
}
|
||||
}
|
51
crates/hyperlog-server/src/services/get_available_roots.rs
Normal file
51
crates/hyperlog-server/src/services/get_available_roots.rs
Normal 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())
|
||||
}
|
||||
}
|
369
crates/hyperlog-server/src/services/get_graph.rs
Normal file
369
crates/hyperlog-server/src/services/get_graph.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
105
crates/hyperlog-server/src/services/toggle_item.rs
Normal file
105
crates/hyperlog-server/src/services/toggle_item.rs
Normal 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())
|
||||
}
|
||||
}
|
107
crates/hyperlog-server/src/services/update_item.rs
Normal file
107
crates/hyperlog-server/src/services/update_item.rs
Normal 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())
|
||||
}
|
||||
}
|
37
crates/hyperlog-server/src/state.rs
Normal file
37
crates/hyperlog-server/src/state.rs
Normal 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 })
|
||||
}
|
||||
}
|
@@ -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.28.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"
|
||||
|
@@ -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> {
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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,
|
||||
|
78
crates/hyperlog-tui/src/commander.rs
Normal file
78
crates/hyperlog-tui/src/commander.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)?;
|
125
crates/hyperlog-tui/src/commander/remote.rs
Normal file
125
crates/hyperlog-tui/src/commander/remote.rs
Normal 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(())
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
57
crates/hyperlog-tui/src/commands/archive.rs
Normal file
57
crates/hyperlog-tui/src/commands/archive.rs
Normal 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())
|
||||
}
|
||||
}
|
41
crates/hyperlog-tui/src/commands/batch.rs
Normal file
41
crates/hyperlog-tui/src/commands/batch.rs
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
73
crates/hyperlog-tui/src/commands/create_item.rs
Normal file
73
crates/hyperlog-tui/src/commands/create_item.rs
Normal 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())
|
||||
}
|
||||
}
|
59
crates/hyperlog-tui/src/commands/create_section.rs
Normal file
59
crates/hyperlog-tui/src/commands/create_section.rs
Normal 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())
|
||||
}
|
||||
}
|
59
crates/hyperlog-tui/src/commands/open_item.rs
Normal file
59
crates/hyperlog-tui/src/commands/open_item.rs
Normal 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())
|
||||
}
|
||||
}
|
59
crates/hyperlog-tui/src/commands/open_update_item_dialog.rs
Normal file
59
crates/hyperlog-tui/src/commands/open_update_item_dialog.rs
Normal 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())
|
||||
}
|
||||
}
|
59
crates/hyperlog-tui/src/commands/toggle_item.rs
Normal file
59
crates/hyperlog-tui/src/commands/toggle_item.rs
Normal 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())
|
||||
}
|
||||
}
|
61
crates/hyperlog-tui/src/commands/update_graph.rs
Normal file
61
crates/hyperlog-tui/src/commands/update_graph.rs
Normal 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())
|
||||
}
|
||||
}
|
76
crates/hyperlog-tui/src/commands/update_item.rs
Normal file
76
crates/hyperlog-tui/src/commands/update_item.rs
Normal 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())
|
||||
}
|
||||
}
|
@@ -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 =
|
||||
|
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
81
crates/hyperlog-tui/src/core_state.rs
Normal file
81
crates/hyperlog-tui/src/core_state.rs
Normal 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
|
||||
}
|
||||
}
|
289
crates/hyperlog-tui/src/editor.rs
Normal file
289
crates/hyperlog-tui/src/editor.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
|
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
5
crates/hyperlog-tui/src/project_dirs.rs
Normal file
5
crates/hyperlog-tui/src/project_dirs.rs
Normal 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")
|
||||
}
|
68
crates/hyperlog-tui/src/querier.rs
Normal file
68
crates/hyperlog-tui/src/querier.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
118
crates/hyperlog-tui/src/querier/remote.rs
Normal file
118
crates/hyperlog-tui/src/querier/remote.rs
Normal 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 §ion.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,
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
@@ -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]
|
@@ -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"]
|
||||
|
@@ -1 +0,0 @@
|
||||
-- Add migration script here
|
@@ -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?;
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,4 @@
|
||||
mod cli;
|
||||
pub(crate) mod server;
|
||||
pub(crate) mod state;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
|
@@ -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(())
|
||||
}
|
||||
|
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
32
cuddle.yaml
32
cuddle.yaml
@@ -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
187
demo.cast
@@ -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
15
scripts/dev.sh
Executable 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
5
scripts/install.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
cargo install --path crates/hyperlog --force
|
@@ -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'
|
||||
|
Reference in New Issue
Block a user