Compare commits
14 commits
Author | SHA1 | Date | |
---|---|---|---|
8b648f422e | |||
7f53459116 | |||
9a16557016 | |||
45672c2964 | |||
2702cfd049 | |||
8bd588070e | |||
73ab922c77 | |||
a8f8e23cf4 | |||
1224eb2d39 | |||
e2ac408c35 | |||
11557e1de2 | |||
6a31732d1b | |||
862b3218e5 | |||
eb78dad91a |
20 changed files with 672 additions and 312 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
use flake
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1 +1,3 @@
|
||||||
/target
|
/target
|
||||||
|
|
||||||
|
.direnv/
|
||||||
|
|
137
Cargo.lock
generated
137
Cargo.lock
generated
|
@ -2,6 +2,17 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -14,6 +25,12 @@ version = "1.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.0.73"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
@ -76,6 +93,18 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-iterator"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-streaming-iterator"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.7"
|
version = "0.2.7"
|
||||||
|
@ -92,6 +121,18 @@ name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashlink"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d452c155cb93fecdfb02a73dd57b5d8e442c2063bd7aac72f1bc5e4263a43086"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
|
@ -121,6 +162,17 @@ version = "0.2.127"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "505e71a4706fa491e9b1b55f51b95d4037d0821ee40131190475f692b35b009b"
|
checksum = "505e71a4706fa491e9b1b55f51b95d4037d0821ee40131190475f692b35b009b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libsqlite3-sys"
|
||||||
|
version = "0.25.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f0455f2c1bc9a7caa792907026e469c1d91761fb0ea37cbb16427c77280cf35"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.17"
|
version = "0.4.17"
|
||||||
|
@ -130,6 +182,15 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_threads"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.13.0"
|
version = "1.13.0"
|
||||||
|
@ -142,6 +203,12 @@ version = "6.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4"
|
checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-error"
|
name = "proc-macro-error"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
@ -205,41 +272,25 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "rusqlite"
|
||||||
version = "1.0.11"
|
version = "0.28.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
|
checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde"
|
|
||||||
version = "1.0.142"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e590c437916fb6b221e1d00df6e3294f3fccd70ca7e92541c475d6ed6ef5fee2"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"bitflags",
|
||||||
|
"fallible-iterator",
|
||||||
|
"fallible-streaming-iterator",
|
||||||
|
"hashlink",
|
||||||
|
"libsqlite3-sys",
|
||||||
|
"smallvec",
|
||||||
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "smallvec"
|
||||||
version = "1.0.142"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34b5b8d809babe02f538c2cfec6f2c1ed10804c0e5a6a041a049a4f5588ccc2e"
|
checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_json"
|
|
||||||
version = "1.0.83"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7"
|
|
||||||
dependencies = [
|
|
||||||
"itoa",
|
|
||||||
"ryu",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
|
@ -258,8 +309,8 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"rusqlite",
|
||||||
"serde_json",
|
"time",
|
||||||
"xdg",
|
"xdg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -289,12 +340,36 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db76ff9fa4b1458b3c7f077f3ff9887394058460d21e634355b273aaf11eea45"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"libc",
|
||||||
|
"num_threads",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
|
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
|
|
@ -5,6 +5,7 @@ repository = "https://git.pleshevski.ru/pleshevskiy/tas"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "GPL-3.0+"
|
license = "GPL-3.0+"
|
||||||
|
default-run = "tas"
|
||||||
|
|
||||||
[badges]
|
[badges]
|
||||||
maintenance = { status = "experimental" }
|
maintenance = { status = "experimental" }
|
||||||
|
@ -12,6 +13,8 @@ maintenance = { status = "experimental" }
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "3.2.16", default-features = false, features = ["derive", "std"] }
|
clap = { version = "3.2.16", default-features = false, features = ["derive", "std"] }
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
serde = { version = "1.0.142", features = ["derive"] }
|
rusqlite = { version = "0.28.0", features = ["bundled", "time"] }
|
||||||
serde_json = "1.0.83"
|
time = "0.3"
|
||||||
xdg = "2.4.1"
|
xdg = "2.4.1"
|
||||||
|
|
||||||
|
|
||||||
|
|
26
database/migrations/202208162308.sql
Normal file
26
database/migrations/202208162308.sql
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
CREATE TABLE _tas_info (
|
||||||
|
version INTEGER PRIMARY KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE tasks (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
project TEXT ,
|
||||||
|
link TEXT ,
|
||||||
|
dir_path TEXT ,
|
||||||
|
|
||||||
|
current BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
finished_at DATETIME
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE VIEW active_tasks
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
t.*,
|
||||||
|
row_number() OVER (ORDER BY t.created_at) AS idx
|
||||||
|
FROM tasks AS t
|
||||||
|
WHERE t.finished_at IS NULL
|
||||||
|
ORDER BY t.created_at
|
||||||
|
;
|
9
database/migrations/202208201623.sql
Normal file
9
database/migrations/202208201623.sql
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
CREATE VIEW finished_tasks
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
t.*,
|
||||||
|
row_number() OVER (ORDER BY t.finished_at DESC) AS idx
|
||||||
|
FROM tasks AS t
|
||||||
|
WHERE t.finished_at IS NOT NULL
|
||||||
|
ORDER BY t.finished_at DESC
|
||||||
|
;
|
36
database/schema.sql
Normal file
36
database/schema.sql
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
CREATE TABLE _tas_info (
|
||||||
|
version INTEGER PRIMARY KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE tasks (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
project TEXT ,
|
||||||
|
link TEXT ,
|
||||||
|
dir_path TEXT ,
|
||||||
|
|
||||||
|
current BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
finished_at DATETIME
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE VIEW active_tasks
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
t.*,
|
||||||
|
row_number() OVER (ORDER BY t.created_at) AS idx
|
||||||
|
FROM tasks AS t
|
||||||
|
WHERE t.finished_at IS NULL
|
||||||
|
ORDER BY t.created_at
|
||||||
|
;
|
||||||
|
|
||||||
|
CREATE VIEW finished_tasks
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
t.*,
|
||||||
|
row_number() OVER (ORDER BY t.finished_at DESC) AS idx
|
||||||
|
FROM tasks AS t
|
||||||
|
WHERE t.finished_at IS NOT NULL
|
||||||
|
ORDER BY t.finished_at DESC
|
||||||
|
;
|
7
default.nix
Normal file
7
default.nix
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
(import (
|
||||||
|
fetchTarball {
|
||||||
|
url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz";
|
||||||
|
sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; }
|
||||||
|
) {
|
||||||
|
src = ./.;
|
||||||
|
}).defaultNix
|
43
flake.lock
Normal file
43
flake.lock
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1661008273,
|
||||||
|
"narHash": "sha256-UpDqsGzUswIHG7FwzeIewjWlElF17UVLNbI2pwlbcBY=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "0cc6444e74cd21e8da8d81ef4cd778492e10f843",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"utils": "utils"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"utils": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1659877975,
|
||||||
|
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
31
flake.nix
Normal file
31
flake.nix
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, utils }:
|
||||||
|
utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs { inherit system; };
|
||||||
|
cargoToml = with builtins; (fromTOML (readFile ./Cargo.toml));
|
||||||
|
in
|
||||||
|
rec {
|
||||||
|
packages.default = pkgs.rustPlatform.buildRustPackage {
|
||||||
|
inherit (cargoToml.package) name version;
|
||||||
|
src = nixpkgs.lib.cleanSource ./.;
|
||||||
|
doCheck = true;
|
||||||
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
|
};
|
||||||
|
|
||||||
|
apps.default = utils.lib.mkApp {
|
||||||
|
inherit (cargoToml.package) name;
|
||||||
|
drv = packages.default;
|
||||||
|
};
|
||||||
|
|
||||||
|
devShell = with pkgs; mkShell {
|
||||||
|
packages = [ cargo rustc rustfmt clippy rust-analyzer ];
|
||||||
|
RUST_SRC_PATH = rustPlatform.rustLibSrc;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
8
makefile
Normal file
8
makefile
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
|
||||||
|
install:
|
||||||
|
cargo install --path .
|
||||||
|
|
||||||
|
new-migration:
|
||||||
|
touch ./database/migrations/$$(date +%Y%m%d%H%M).sql
|
||||||
|
|
7
shell.nix
Normal file
7
shell.nix
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
(import (
|
||||||
|
fetchTarball {
|
||||||
|
url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz";
|
||||||
|
sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; }
|
||||||
|
) {
|
||||||
|
src = ./.;
|
||||||
|
}).shellNix
|
17
shell/zsh
17
shell/zsh
|
@ -3,11 +3,19 @@ function __tas_task_idx() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function __tas_show() {
|
function __tas_show() {
|
||||||
tas show $(__tas_task_idx "$@")
|
tas show $(__tas_task_idx "$1")
|
||||||
}
|
}
|
||||||
|
|
||||||
function __tas_start() {
|
function __tas_start() {
|
||||||
tas start $(__tas_task_idx "$@")
|
if [[ "$1" != "" ]]; then
|
||||||
|
tas start $(__tas_task_idx "$1")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function __tas_remove() {
|
||||||
|
if [[ "$1" != "" ]]; then
|
||||||
|
tas remove $(__tas_task_idx "$1")
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function __tas_list() {
|
function __tas_list() {
|
||||||
|
@ -28,6 +36,11 @@ function taz() {
|
||||||
|
|
||||||
"start" | "st")
|
"start" | "st")
|
||||||
__tas_start "$(__tas_list)"
|
__tas_start "$(__tas_list)"
|
||||||
|
;;
|
||||||
|
|
||||||
|
"remove" | "rm")
|
||||||
|
__tas_remove "$(__tas_list)"
|
||||||
|
;;
|
||||||
|
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,19 +18,26 @@ use crate::repo::Repository;
|
||||||
|
|
||||||
#[derive(clap::Args)]
|
#[derive(clap::Args)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
|
#[clap(short, long)]
|
||||||
|
finished: bool,
|
||||||
|
|
||||||
projects: Vec<String>,
|
projects: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute(repo: impl Repository, args: Args) {
|
pub fn execute(repo: impl Repository, args: Args) {
|
||||||
let tasks = match repo.get_tasks() {
|
let tasks = match repo.get_tasks(args.finished) {
|
||||||
Ok(tasks) => tasks,
|
Ok(tasks) => tasks,
|
||||||
Err(err) => return eprintln!("Cannot read tasks: {}", err),
|
Err(err) => return eprintln!("Cannot read tasks: {}", err),
|
||||||
};
|
};
|
||||||
|
|
||||||
let cur_task = match repo.get_current_task_opt() {
|
let cur_task = if args.finished {
|
||||||
Ok(cur_task) => cur_task,
|
None
|
||||||
Err(err) => {
|
} else {
|
||||||
return eprintln!("Cannot read current task: {}", err);
|
match repo.get_current_task_opt() {
|
||||||
|
Ok(cur_task) => cur_task,
|
||||||
|
Err(err) => {
|
||||||
|
return eprintln!("Cannot read current task: {}", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -15,14 +15,14 @@
|
||||||
//!
|
//!
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub type TaskId = usize;
|
pub type TaskIdx = usize;
|
||||||
|
|
||||||
pub struct Task {
|
pub struct Task {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub project: Option<String>,
|
pub project: Option<String>,
|
||||||
pub link: Option<String>,
|
pub link: Option<String>,
|
||||||
pub dir_path: Option<PathBuf>,
|
pub dir_path: Option<PathBuf>,
|
||||||
// created_at
|
pub created_at: time::OffsetDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CurrentTaskInfo {
|
pub struct CurrentTaskInfo {
|
||||||
|
|
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod domain;
|
||||||
|
pub mod repo;
|
11
src/main.rs
11
src/main.rs
|
@ -27,7 +27,7 @@
|
||||||
//---------------------------------------------------------------------
|
//---------------------------------------------------------------------
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use repo::fs::FsRepo;
|
use repo::sqlite::SqliteRepo;
|
||||||
use xdg::BaseDirectories;
|
use xdg::BaseDirectories;
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
|
@ -38,7 +38,14 @@ fn main() {
|
||||||
let args = cli::Args::parse();
|
let args = cli::Args::parse();
|
||||||
|
|
||||||
let xdg_dirs = BaseDirectories::with_prefix(env!("CARGO_PKG_NAME")).unwrap();
|
let xdg_dirs = BaseDirectories::with_prefix(env!("CARGO_PKG_NAME")).unwrap();
|
||||||
let repo = FsRepo::new(xdg_dirs);
|
let repo = match SqliteRepo::new(xdg_dirs) {
|
||||||
|
Ok(repo) => repo,
|
||||||
|
Err(err) => return eprintln!("Cannot connect to repository: {err}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = repo.upgrade() {
|
||||||
|
return eprintln!("Cannot upgrade database: {err}");
|
||||||
|
}
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
cli::SubCommand::Add(args) => {
|
cli::SubCommand::Add(args) => {
|
||||||
|
|
22
src/repo.rs
22
src/repo.rs
|
@ -13,7 +13,7 @@
|
||||||
//! You should have received a copy of the GNU General Public License
|
//! You should have received a copy of the GNU General Public License
|
||||||
//! along with tas. If not, see <https://www.gnu.org/licenses/>.
|
//! along with tas. If not, see <https://www.gnu.org/licenses/>.
|
||||||
//!
|
//!
|
||||||
pub mod fs;
|
pub mod sqlite;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
@ -21,17 +21,27 @@ use crate::domain;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
Connect,
|
||||||
|
PrepareQuery,
|
||||||
|
QueryData,
|
||||||
NotFound,
|
NotFound,
|
||||||
InvalidData,
|
InvalidData,
|
||||||
InsertData,
|
InsertData,
|
||||||
|
UpdateData,
|
||||||
|
RemoveData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Error {
|
impl std::fmt::Display for Error {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
Error::Connect => f.write_str("Cannot connect to the repository"),
|
||||||
|
Error::PrepareQuery => f.write_str("Cannot prepare query"),
|
||||||
|
Error::QueryData => f.write_str("Cannot query data"),
|
||||||
Error::NotFound => f.write_str("Cannot find data"),
|
Error::NotFound => f.write_str("Cannot find data"),
|
||||||
Error::InvalidData => f.write_str("Invalid data format"),
|
Error::InvalidData => f.write_str("Invalid data format"),
|
||||||
Error::InsertData => f.write_str("Cannot insert data"),
|
Error::InsertData => f.write_str("Cannot insert data"),
|
||||||
|
Error::UpdateData => f.write_str("Cannot update data"),
|
||||||
|
Error::RemoveData => f.write_str("Cannot remove data"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,21 +66,21 @@ pub struct UpdateTaskData {
|
||||||
pub trait Repository {
|
pub trait Repository {
|
||||||
fn get_current_task_opt(&self) -> Result<Option<domain::CurrentTaskInfo>, Error>;
|
fn get_current_task_opt(&self) -> Result<Option<domain::CurrentTaskInfo>, Error>;
|
||||||
|
|
||||||
fn get_task_opt(&self, id: domain::TaskId) -> Result<Option<domain::Task>, Error>;
|
fn get_task_opt(&self, id: domain::TaskIdx) -> Result<Option<domain::Task>, Error>;
|
||||||
|
|
||||||
fn get_tasks(&self) -> Result<Vec<domain::Task>, Error>;
|
fn get_tasks(&self, finished: bool) -> Result<Vec<domain::Task>, Error>;
|
||||||
|
|
||||||
fn remove_task(&self, id: domain::TaskId) -> Result<domain::Task, Error>;
|
fn remove_task(&self, id: domain::TaskIdx) -> Result<domain::Task, Error>;
|
||||||
|
|
||||||
fn update_task(
|
fn update_task(
|
||||||
&self,
|
&self,
|
||||||
id: domain::TaskId,
|
id: domain::TaskIdx,
|
||||||
update_data: UpdateTaskData,
|
update_data: UpdateTaskData,
|
||||||
) -> Result<domain::Task, Error>;
|
) -> Result<domain::Task, Error>;
|
||||||
|
|
||||||
fn insert_task(&self, insert_data: InsertTaskData) -> Result<domain::Task, Error>;
|
fn insert_task(&self, insert_data: InsertTaskData) -> Result<domain::Task, Error>;
|
||||||
|
|
||||||
fn start_task(&self, id: domain::TaskId) -> Result<domain::Task, Error>;
|
fn start_task(&self, id: domain::TaskIdx) -> Result<domain::Task, Error>;
|
||||||
|
|
||||||
fn stop_task(&self) -> Result<domain::Task, Error>;
|
fn stop_task(&self) -> Result<domain::Task, Error>;
|
||||||
|
|
||||||
|
|
262
src/repo/fs.rs
262
src/repo/fs.rs
|
@ -1,262 +0,0 @@
|
||||||
//! Copyright (C) 2022, Dmitriy Pleshevskiy <dmitriy@ideascup.me>
|
|
||||||
//!
|
|
||||||
//! tas is free software: you can redistribute it and/or modify
|
|
||||||
//! it under the terms of the GNU General Public License as published by
|
|
||||||
//! the Free Software Foundation, either version 3 of the License, or
|
|
||||||
//! (at your option) any later version.
|
|
||||||
//!
|
|
||||||
//! tas is distributed in the hope that it will be useful,
|
|
||||||
//! but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
//! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
//! GNU General Public License for more details.
|
|
||||||
//!
|
|
||||||
//! You should have received a copy of the GNU General Public License
|
|
||||||
//! along with tas. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
//!
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use crate::domain;
|
|
||||||
use crate::repo::{Error, InsertTaskData, Repository, UpdateTaskData};
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use xdg::BaseDirectories;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
|
||||||
pub struct Task {
|
|
||||||
name: String,
|
|
||||||
group: Option<String>,
|
|
||||||
link: Option<String>,
|
|
||||||
path: Option<String>,
|
|
||||||
// created_at
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Task> for domain::Task {
|
|
||||||
fn from(repo: Task) -> Self {
|
|
||||||
domain::Task {
|
|
||||||
name: repo.name,
|
|
||||||
project: repo.group,
|
|
||||||
link: repo.link,
|
|
||||||
dir_path: repo.path.map(PathBuf::from),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
|
||||||
pub struct CurrentTaskInfo {
|
|
||||||
task_idx: usize,
|
|
||||||
task: Task,
|
|
||||||
// started_at
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<CurrentTaskInfo> for domain::CurrentTaskInfo {
|
|
||||||
fn from(repo: CurrentTaskInfo) -> Self {
|
|
||||||
domain::CurrentTaskInfo {
|
|
||||||
task_idx: repo.task_idx,
|
|
||||||
task: repo.task.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const CURRENT_TASK_FILE: &str = "current.json";
|
|
||||||
const DATA_FILE: &str = "data.json";
|
|
||||||
const FINISHED_DATA_FILE: &str = "finished_data.json";
|
|
||||||
|
|
||||||
pub struct FsRepo {
|
|
||||||
xdg_dirs: BaseDirectories,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FsRepo {
|
|
||||||
pub fn new(xdg_dirs: BaseDirectories) -> Self {
|
|
||||||
Self { xdg_dirs }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Repository for FsRepo {
|
|
||||||
fn get_current_task_opt(&self) -> Result<Option<domain::CurrentTaskInfo>, Error> {
|
|
||||||
self.get_current_task_impl()
|
|
||||||
.map(|cur_task| cur_task.map(From::from))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_task_opt(&self, id: domain::TaskId) -> Result<Option<domain::Task>, Error> {
|
|
||||||
let tasks = self.get_tasks_impl()?;
|
|
||||||
if id == 0 || id > tasks.len() {
|
|
||||||
return Err(Error::NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(tasks[id - 1].clone().into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_tasks(&self) -> Result<Vec<domain::Task>, Error> {
|
|
||||||
self.get_tasks_impl()
|
|
||||||
.map(|tasks| tasks.into_iter().map(Task::into).collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_task(&self, id: domain::TaskId) -> Result<domain::Task, Error> {
|
|
||||||
let mut tasks = self.get_tasks_impl()?;
|
|
||||||
if id == 0 || id > tasks.len() {
|
|
||||||
return Err(Error::NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
let removed = tasks.remove(id - 1);
|
|
||||||
self.save_tasks_impl(tasks)?;
|
|
||||||
|
|
||||||
Ok(removed.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_task(
|
|
||||||
&self,
|
|
||||||
id: domain::TaskId,
|
|
||||||
update_data: UpdateTaskData,
|
|
||||||
) -> Result<domain::Task, Error> {
|
|
||||||
let mut tasks = self.get_tasks_impl()?;
|
|
||||||
if id == 0 || id > tasks.len() {
|
|
||||||
return Err(Error::NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut task = &mut tasks[id - 1];
|
|
||||||
if let Some(name) = update_data.name {
|
|
||||||
task.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(group) = update_data.project {
|
|
||||||
task.group = group;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(link) = update_data.link {
|
|
||||||
task.link = link;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(path) = update_data.dir_path {
|
|
||||||
task.path = path.and_then(|p| p.into_os_string().into_string().ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_task = task.clone();
|
|
||||||
|
|
||||||
self.save_tasks_impl(tasks)?;
|
|
||||||
|
|
||||||
Ok(new_task.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_task(&self, insert_data: InsertTaskData) -> Result<domain::Task, Error> {
|
|
||||||
let new_task = Task {
|
|
||||||
name: insert_data.name,
|
|
||||||
group: insert_data.project,
|
|
||||||
link: insert_data.link,
|
|
||||||
path: insert_data
|
|
||||||
.dir_path
|
|
||||||
.and_then(|p| p.into_os_string().into_string().ok()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut tasks = self.get_tasks_impl()?;
|
|
||||||
|
|
||||||
match insert_data.index {
|
|
||||||
None => tasks.push(new_task.clone()),
|
|
||||||
Some(idx) => tasks.insert(idx, new_task.clone()),
|
|
||||||
}
|
|
||||||
|
|
||||||
self.save_tasks_impl(tasks)?;
|
|
||||||
|
|
||||||
Ok(new_task.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_task(&self, id: domain::TaskId) -> Result<domain::Task, Error> {
|
|
||||||
let tasks = self.get_tasks_impl()?;
|
|
||||||
if id == 0 || id > tasks.len() {
|
|
||||||
return Err(Error::NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
let task = &tasks[id - 1];
|
|
||||||
let mut cur_task = self.get_current_task_impl()?;
|
|
||||||
|
|
||||||
cur_task.replace(CurrentTaskInfo {
|
|
||||||
task_idx: id,
|
|
||||||
task: task.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
self.save_current_task_impl(cur_task)?;
|
|
||||||
Ok(task.clone().into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stop_task(&self) -> Result<domain::Task, Error> {
|
|
||||||
let mut cur_task = self.get_current_task_impl()?;
|
|
||||||
let old = cur_task.take().ok_or(Error::NotFound)?;
|
|
||||||
self.save_current_task_impl(cur_task)?;
|
|
||||||
Ok(old.task.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finish_task(&self) -> Result<domain::Task, Error> {
|
|
||||||
let mut cur_task = self.get_current_task_impl()?;
|
|
||||||
let old = cur_task.take().ok_or(Error::NotFound)?;
|
|
||||||
|
|
||||||
let mut finished_tasks = self.get_finished_tasks_impl()?;
|
|
||||||
let mut tasks = self.get_tasks_impl()?;
|
|
||||||
|
|
||||||
let task = tasks.remove(old.task_idx - 1);
|
|
||||||
finished_tasks.push(task.clone());
|
|
||||||
|
|
||||||
self.save_current_task_impl(cur_task)?;
|
|
||||||
self.save_tasks_impl(tasks)?;
|
|
||||||
self.save_finished_tasks_impl(finished_tasks)?;
|
|
||||||
|
|
||||||
Ok(task.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FsRepo {
|
|
||||||
fn get_current_task_impl(&self) -> Result<Option<CurrentTaskInfo>, Error> {
|
|
||||||
let file_path = self.xdg_dirs.get_data_file(CURRENT_TASK_FILE);
|
|
||||||
Ok(File::open(&file_path)
|
|
||||||
.ok()
|
|
||||||
.and_then(|file| serde_json::from_reader(file).ok()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save_current_task_impl(&self, cur_task: Option<CurrentTaskInfo>) -> Result<(), Error> {
|
|
||||||
let file_path = self
|
|
||||||
.xdg_dirs
|
|
||||||
.place_data_file(CURRENT_TASK_FILE)
|
|
||||||
.map_err(|_| Error::InsertData)?;
|
|
||||||
let mut file = File::create(&file_path).map_err(|_| Error::InsertData)?;
|
|
||||||
let new_data = serde_json::to_vec(&cur_task).map_err(|_| Error::InvalidData)?;
|
|
||||||
file.write_all(&new_data).map_err(|_| Error::InsertData)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_tasks_impl(&self) -> Result<Vec<Task>, Error> {
|
|
||||||
let file_path = self.xdg_dirs.get_data_file(DATA_FILE);
|
|
||||||
File::open(&file_path)
|
|
||||||
.ok()
|
|
||||||
.map(|file| serde_json::from_reader(file).map_err(|_| Error::InvalidData))
|
|
||||||
.transpose()
|
|
||||||
.map(|tasks| tasks.unwrap_or_default())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save_tasks_impl(&self, tasks: Vec<Task>) -> Result<(), Error> {
|
|
||||||
let file_path = self
|
|
||||||
.xdg_dirs
|
|
||||||
.place_data_file(DATA_FILE)
|
|
||||||
.map_err(|_| Error::InsertData)?;
|
|
||||||
let mut file = File::create(&file_path).map_err(|_| Error::InsertData)?;
|
|
||||||
let new_data = serde_json::to_vec(&tasks).map_err(|_| Error::InvalidData)?;
|
|
||||||
file.write_all(&new_data).map_err(|_| Error::InsertData)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_finished_tasks_impl(&self) -> Result<Vec<Task>, Error> {
|
|
||||||
let file_path = self.xdg_dirs.get_data_file(FINISHED_DATA_FILE);
|
|
||||||
File::open(&file_path)
|
|
||||||
.ok()
|
|
||||||
.map(|file| serde_json::from_reader(file).map_err(|_| Error::InvalidData))
|
|
||||||
.transpose()
|
|
||||||
.map(|tasks| tasks.unwrap_or_default())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save_finished_tasks_impl(&self, tasks: Vec<Task>) -> Result<(), Error> {
|
|
||||||
let file_path = self
|
|
||||||
.xdg_dirs
|
|
||||||
.place_data_file(FINISHED_DATA_FILE)
|
|
||||||
.map_err(|_| Error::InsertData)?;
|
|
||||||
let mut file = File::create(&file_path).map_err(|_| Error::InsertData)?;
|
|
||||||
let new_data = serde_json::to_vec(&tasks).map_err(|_| Error::InvalidData)?;
|
|
||||||
file.write_all(&new_data).map_err(|_| Error::InsertData)
|
|
||||||
}
|
|
||||||
}
|
|
335
src/repo/sqlite.rs
Normal file
335
src/repo/sqlite.rs
Normal file
|
@ -0,0 +1,335 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use rusqlite::{Connection, OptionalExtension};
|
||||||
|
use xdg::BaseDirectories;
|
||||||
|
|
||||||
|
use crate::domain;
|
||||||
|
use crate::repo::{Error, Repository};
|
||||||
|
|
||||||
|
struct Task {
|
||||||
|
id: i64,
|
||||||
|
name: String,
|
||||||
|
project: Option<String>,
|
||||||
|
link: Option<String>,
|
||||||
|
dir_path: Option<String>,
|
||||||
|
/*
|
||||||
|
current: bool,
|
||||||
|
finished_at: Option<time::OffsetDateTime>,
|
||||||
|
*/
|
||||||
|
created_at: time::OffsetDateTime,
|
||||||
|
|
||||||
|
idx: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Task> for domain::Task {
|
||||||
|
fn from(repo: Task) -> Self {
|
||||||
|
Self {
|
||||||
|
name: repo.name,
|
||||||
|
project: repo.project,
|
||||||
|
link: repo.link,
|
||||||
|
dir_path: repo.dir_path.map(PathBuf::from),
|
||||||
|
created_at: repo.created_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r> TryFrom<&'r rusqlite::Row<'_>> for Task {
|
||||||
|
type Error = rusqlite::Error;
|
||||||
|
|
||||||
|
fn try_from(row: &'r rusqlite::Row<'_>) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
id: row.get("id")?,
|
||||||
|
name: row.get("name")?,
|
||||||
|
project: row.get("project")?,
|
||||||
|
link: row.get("link")?,
|
||||||
|
dir_path: row.get("dir_path")?,
|
||||||
|
/*
|
||||||
|
current: row.get("current")?,
|
||||||
|
finished_at: row.get("finished_at")?,
|
||||||
|
*/
|
||||||
|
created_at: row.get("created_at")?,
|
||||||
|
idx: row.get("idx")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCHEMA_FILE: &str = "tas.db";
|
||||||
|
|
||||||
|
pub struct SqliteRepo {
|
||||||
|
conn: Connection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SqliteRepo {
|
||||||
|
pub fn new(xdg_dirs: BaseDirectories) -> Result<Self, Error> {
|
||||||
|
let file_path = xdg_dirs.get_data_file(SCHEMA_FILE);
|
||||||
|
let conn = Connection::open(file_path).map_err(|_| Error::Connect)?;
|
||||||
|
|
||||||
|
Ok(Self { conn })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Repository for SqliteRepo {
|
||||||
|
fn get_current_task_opt(&self) -> Result<Option<domain::CurrentTaskInfo>, Error> {
|
||||||
|
let db_task = self.get_current_task_opt_impl()?;
|
||||||
|
|
||||||
|
Ok(db_task.map(|db_task| domain::CurrentTaskInfo {
|
||||||
|
task_idx: db_task.idx as usize,
|
||||||
|
task: db_task.into(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_task_opt(&self, id: domain::TaskIdx) -> Result<Option<domain::Task>, Error> {
|
||||||
|
self.get_task_opt_impl(id).map(|t| t.map(From::from))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_tasks(&self, finished: bool) -> Result<Vec<domain::Task>, Error> {
|
||||||
|
let mut stmt = if finished {
|
||||||
|
self.conn
|
||||||
|
.prepare("SELECT * FROM finished_tasks")
|
||||||
|
.map_err(|_| Error::PrepareQuery)?
|
||||||
|
} else {
|
||||||
|
self.conn
|
||||||
|
.prepare("SELECT * FROM active_tasks")
|
||||||
|
.map_err(|_| Error::PrepareQuery)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let rows = stmt
|
||||||
|
.query_map([], |row| Task::try_from(row))
|
||||||
|
.map_err(|_| Error::QueryData)?;
|
||||||
|
|
||||||
|
rows.into_iter()
|
||||||
|
.map(|db_task| db_task.map(From::from).map_err(|_| Error::QueryData))
|
||||||
|
.collect::<Result<Vec<_>, Error>>()
|
||||||
|
.map_err(|_| Error::InvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_task(&self, idx: domain::TaskIdx) -> Result<domain::Task, Error> {
|
||||||
|
let db_task = self.get_task_opt_impl(idx)?.ok_or(Error::NotFound)?;
|
||||||
|
|
||||||
|
let mut stmt = self
|
||||||
|
.conn
|
||||||
|
.prepare("DELETE FROM tasks WHERE id = ?")
|
||||||
|
.map_err(|_| Error::PrepareQuery)?;
|
||||||
|
|
||||||
|
stmt.execute([&(db_task.id)])
|
||||||
|
.map_err(|_| Error::RemoveData)?;
|
||||||
|
|
||||||
|
Ok(db_task.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_task(&self, insert_data: super::InsertTaskData) -> Result<domain::Task, Error> {
|
||||||
|
let mut stmt = self
|
||||||
|
.conn
|
||||||
|
.prepare(
|
||||||
|
"INSERT INTO tasks (name, project, link, dir_path)
|
||||||
|
VALUES (?1, ?2, ?3, ?4)",
|
||||||
|
)
|
||||||
|
.map_err(|_| Error::PrepareQuery)?;
|
||||||
|
|
||||||
|
let id = stmt
|
||||||
|
.insert((
|
||||||
|
&insert_data.name,
|
||||||
|
&insert_data.project,
|
||||||
|
&insert_data.link,
|
||||||
|
&insert_data
|
||||||
|
.dir_path
|
||||||
|
.and_then(|p| p.into_os_string().into_string().ok()),
|
||||||
|
))
|
||||||
|
.map_err(|_| Error::InsertData)?;
|
||||||
|
|
||||||
|
self.get_task_by_id_impl(id).map(From::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_task(
|
||||||
|
&self,
|
||||||
|
idx: domain::TaskIdx,
|
||||||
|
update_data: super::UpdateTaskData,
|
||||||
|
) -> Result<domain::Task, Error> {
|
||||||
|
let task = self.get_task_opt_impl(idx)?.ok_or(Error::NotFound)?;
|
||||||
|
|
||||||
|
let mut stmt = self
|
||||||
|
.conn
|
||||||
|
.prepare(
|
||||||
|
"UPDATE tasks
|
||||||
|
SET name = ?1,
|
||||||
|
project = ?2,
|
||||||
|
link = ?3,
|
||||||
|
dir_path = ?4
|
||||||
|
WHERE id = ?5
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.map_err(|_| Error::PrepareQuery)?;
|
||||||
|
|
||||||
|
stmt.execute((
|
||||||
|
&update_data.name.unwrap_or(task.name),
|
||||||
|
&update_data.project.unwrap_or(task.project),
|
||||||
|
&update_data.link.unwrap_or(task.link),
|
||||||
|
&update_data
|
||||||
|
.dir_path
|
||||||
|
.unwrap_or(task.dir_path.map(PathBuf::from))
|
||||||
|
.and_then(|p| p.into_os_string().into_string().ok()),
|
||||||
|
&task.id,
|
||||||
|
))
|
||||||
|
.map_err(|_| Error::UpdateData)?;
|
||||||
|
|
||||||
|
self.get_task_opt(idx)?.ok_or(Error::NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_task(&self, idx: domain::TaskIdx) -> Result<domain::Task, Error> {
|
||||||
|
let db_task = self.get_task_opt_impl(idx)?.ok_or(Error::NotFound)?;
|
||||||
|
|
||||||
|
self.conn
|
||||||
|
.execute(
|
||||||
|
"UPDATE tasks SET current = true WHERE id = ?1",
|
||||||
|
[&db_task.id],
|
||||||
|
)
|
||||||
|
.map_err(|_| Error::UpdateData)?;
|
||||||
|
|
||||||
|
Ok(db_task.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_task(&self) -> Result<domain::Task, Error> {
|
||||||
|
let db_task = self.get_current_task_opt_impl()?.ok_or(Error::NotFound)?;
|
||||||
|
|
||||||
|
self.conn
|
||||||
|
.execute(
|
||||||
|
"UPDATE tasks SET current = false WHERE id = ?1",
|
||||||
|
[&db_task.id],
|
||||||
|
)
|
||||||
|
.map_err(|_| Error::UpdateData)?;
|
||||||
|
|
||||||
|
Ok(db_task.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_task(&self) -> Result<domain::Task, Error> {
|
||||||
|
let db_task = self.get_current_task_opt_impl()?.ok_or(Error::NotFound)?;
|
||||||
|
|
||||||
|
self.conn
|
||||||
|
.execute(
|
||||||
|
"UPDATE tasks
|
||||||
|
SET current = false,
|
||||||
|
finished_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?1",
|
||||||
|
[&db_task.id],
|
||||||
|
)
|
||||||
|
.map_err(|_| Error::UpdateData)?;
|
||||||
|
|
||||||
|
Ok(db_task.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SqliteRepo {
|
||||||
|
fn get_task_by_id_impl(&self, id: i64) -> Result<Task, Error> {
|
||||||
|
let mut stmt = self
|
||||||
|
.conn
|
||||||
|
.prepare("SELECT * FROM active_tasks WHERE id = ?")
|
||||||
|
.map_err(|_| Error::PrepareQuery)?;
|
||||||
|
|
||||||
|
stmt.query_row([id], |row| Task::try_from(row))
|
||||||
|
.map_err(|_| Error::QueryData)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_task_opt_impl(&self, idx: domain::TaskIdx) -> Result<Option<Task>, Error> {
|
||||||
|
let mut stmt = self
|
||||||
|
.conn
|
||||||
|
.prepare("SELECT * FROM active_tasks WHERE idx = ?")
|
||||||
|
.map_err(|_| Error::PrepareQuery)?;
|
||||||
|
|
||||||
|
stmt.query_row([idx as i64], |row| Task::try_from(row))
|
||||||
|
.optional()
|
||||||
|
.map_err(|_| Error::QueryData)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_current_task_opt_impl(&self) -> Result<Option<Task>, Error> {
|
||||||
|
let mut stmt = self
|
||||||
|
.conn
|
||||||
|
.prepare("SELECT * FROM active_tasks WHERE current IS true")
|
||||||
|
.map_err(|_| Error::PrepareQuery)?;
|
||||||
|
|
||||||
|
let row = stmt
|
||||||
|
.query_row([], |row| Task::try_from(row))
|
||||||
|
.optional()
|
||||||
|
.map_err(|_| Error::QueryData)?;
|
||||||
|
|
||||||
|
Ok(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! run_migration {
|
||||||
|
($this:ident, $ver:ident = $version:expr) => {
|
||||||
|
run_migration!($this, $ver = $version => concat!("migrations/", $version));
|
||||||
|
};
|
||||||
|
|
||||||
|
($this:ident, $ver:ident = $version:expr => $sql_name:expr) => {
|
||||||
|
$this
|
||||||
|
.conn
|
||||||
|
.execute_batch(&format!(
|
||||||
|
"BEGIN; {} COMMIT;",
|
||||||
|
include_str!(concat!("../../database/", $sql_name, ".sql"))
|
||||||
|
))
|
||||||
|
.map_err(|_| MigrationError::Upgrade)?;
|
||||||
|
|
||||||
|
$ver.replace($version);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move migration to a separate repository.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum MigrationError {
|
||||||
|
Upgrade,
|
||||||
|
DeleteInfo,
|
||||||
|
InsertInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for MigrationError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
MigrationError::Upgrade => f.write_str("Cannot upgrade the migration to a new version"),
|
||||||
|
MigrationError::DeleteInfo => {
|
||||||
|
f.write_str("Cannot delete the tas info from the database")
|
||||||
|
}
|
||||||
|
MigrationError::InsertInfo => {
|
||||||
|
f.write_str("Cannot insert a new tas information to the database")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for MigrationError {}
|
||||||
|
|
||||||
|
const LATEST_VERSION: i64 = 202208201623;
|
||||||
|
|
||||||
|
impl SqliteRepo {
|
||||||
|
pub fn upgrade(&self) -> Result<(), MigrationError> {
|
||||||
|
let mut version = self.version();
|
||||||
|
match version {
|
||||||
|
Some(LATEST_VERSION) => return Ok(()),
|
||||||
|
None => {
|
||||||
|
run_migration!(self, version = LATEST_VERSION => "schema");
|
||||||
|
}
|
||||||
|
Some(v) => {
|
||||||
|
if v == 202208162308 {
|
||||||
|
run_migration!(self, version = 202208201623);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.conn
|
||||||
|
.execute("DELETE FROM _tas_info", [])
|
||||||
|
.map_err(|_| MigrationError::DeleteInfo)?;
|
||||||
|
self.conn
|
||||||
|
.execute(
|
||||||
|
"INSERT INTO _tas_info (version) VALUES (?)",
|
||||||
|
[version.unwrap()],
|
||||||
|
)
|
||||||
|
.map_err(|_| MigrationError::InsertInfo)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn version(&self) -> Option<i64> {
|
||||||
|
self.conn
|
||||||
|
.query_row("SELECT version FROM _tas_info", [], |r| r.get("version"))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue