diff options
-rw-r--r-- | .github/workflows/main.yml | 118 | ||||
-rw-r--r-- | Cargo.lock | 164 | ||||
-rw-r--r-- | Cargo.toml | 9 | ||||
-rw-r--r-- | readme.md | 8 | ||||
-rw-r--r-- | src/app/impl_self.rs | 60 | ||||
-rw-r--r-- | src/app/impl_view.rs | 38 | ||||
-rw-r--r-- | src/command.rs | 87 | ||||
-rw-r--r-- | src/habit/bit.rs | 4 | ||||
-rw-r--r-- | src/main.rs | 23 | ||||
-rw-r--r-- | src/theme.rs | 20 | ||||
-rw-r--r-- | src/utils.rs | 130 | ||||
-rw-r--r-- | src/views.rs | 35 |
12 files changed, 568 insertions, 128 deletions
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..8786029 --- /dev/null +++ b/.github/workflows/main.yml | |||
@@ -0,0 +1,118 @@ | |||
1 | name: Rust | ||
2 | |||
3 | on: | ||
4 | push: | ||
5 | tags: | ||
6 | - '*' | ||
7 | |||
8 | jobs: | ||
9 | rustfmt: | ||
10 | runs-on: ubuntu-latest | ||
11 | steps: | ||
12 | - uses: actions/checkout@v1 | ||
13 | - run: rustup component add rustfmt | ||
14 | - run: cargo fmt -- --check | ||
15 | |||
16 | build-linux: | ||
17 | runs-on: ubuntu-latest | ||
18 | |||
19 | steps: | ||
20 | - name: Checkout | ||
21 | uses: actions/checkout@v1 | ||
22 | # cache the build assets so they dont recompile every time. | ||
23 | - name: Cache Rust dependencies | ||
24 | uses: actions/[email protected] | ||
25 | with: | ||
26 | path: target | ||
27 | key: ${{ runner.OS }}-build-${{ hashFiles('**/Cargo.lock') }} | ||
28 | restore-keys: | | ||
29 | ${{ runner.OS }}-build- | ||
30 | - name: Install latest rust toolchain | ||
31 | uses: actions-rs/toolchain@v1 | ||
32 | with: | ||
33 | toolchain: beta | ||
34 | default: true | ||
35 | override: true | ||
36 | - name: Install system dependencies | ||
37 | run: | | ||
38 | sudo apt-get update \ | ||
39 | && sudo apt-get install -y \ | ||
40 | libdbus-1-dev libncurses5-dev libncursesw5-dev | ||
41 | - name: Build | ||
42 | run: cargo build --release && strip target/release/dijo | ||
43 | |||
44 | - name: Upload binaries to release | ||
45 | uses: svenstaro/upload-release-action@v1-release | ||
46 | with: | ||
47 | repo_token: ${{ secrets.GITHUB_TOKEN }} | ||
48 | file: target/release/dijo | ||
49 | asset_name: dijo-x86_64-linux | ||
50 | tag: ${{ github.ref }} | ||
51 | overwrite: true | ||
52 | |||
53 | build-apple: | ||
54 | runs-on: macos-latest | ||
55 | |||
56 | steps: | ||
57 | - name: Checkout | ||
58 | uses: actions/checkout@v1 | ||
59 | - name: Cache Rust dependencies | ||
60 | uses: actions/[email protected] | ||
61 | with: | ||
62 | path: target | ||
63 | key: ${{ runner.OS }}-build-${{ hashFiles('**/Cargo.lock') }} | ||
64 | restore-keys: | | ||
65 | ${{ runner.OS }}-build- | ||
66 | - name: Install latest rust toolchain | ||
67 | uses: actions-rs/toolchain@v1 | ||
68 | with: | ||
69 | toolchain: beta | ||
70 | target: x86_64-apple-darwin | ||
71 | default: true | ||
72 | override: true | ||
73 | |||
74 | - name: Build for mac | ||
75 | run: cargo build --release && strip target/release/dijo | ||
76 | |||
77 | - name: Upload binaries to release | ||
78 | uses: svenstaro/upload-release-action@v1-release | ||
79 | with: | ||
80 | repo_token: ${{ secrets.GITHUB_TOKEN }} | ||
81 | file: target/release/dijo | ||
82 | asset_name: dijo-x86_64-apple | ||
83 | tag: ${{ github.ref }} | ||
84 | overwrite: true | ||
85 | |||
86 | build-windows: | ||
87 | runs-on: windows-latest | ||
88 | |||
89 | steps: | ||
90 | - name: Checkout | ||
91 | uses: actions/checkout@v1 | ||
92 | - name: Cache Rust dependencies | ||
93 | uses: actions/[email protected] | ||
94 | with: | ||
95 | path: target | ||
96 | key: ${{ runner.OS }}-build-${{ hashFiles('**/Cargo.lock') }} | ||
97 | restore-keys: | | ||
98 | ${{ runner.OS }}-build- | ||
99 | - name: Install latest rust toolchain | ||
100 | uses: actions-rs/toolchain@v1 | ||
101 | with: | ||
102 | toolchain: beta | ||
103 | target: x86_64-pc-windows-msvc | ||
104 | default: true | ||
105 | override: true | ||
106 | |||
107 | - name: Build for windows | ||
108 | shell: bash | ||
109 | run: cargo build --release --no-default-features --features "crossterm-backend" | ||
110 | |||
111 | - name: Upload binaries to release | ||
112 | uses: svenstaro/upload-release-action@v1-release | ||
113 | with: | ||
114 | repo_token: ${{ secrets.GITHUB_TOKEN }} | ||
115 | file: target/release/dijo.exe | ||
116 | asset_name: dijo-x86_64-windows.exe | ||
117 | tag: ${{ github.ref }} | ||
118 | overwrite: true | ||
@@ -125,6 +125,15 @@ dependencies = [ | |||
125 | ] | 125 | ] |
126 | 126 | ||
127 | [[package]] | 127 | [[package]] |
128 | name = "cloudabi" | ||
129 | version = "0.0.3" | ||
130 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
131 | checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" | ||
132 | dependencies = [ | ||
133 | "bitflags", | ||
134 | ] | ||
135 | |||
136 | [[package]] | ||
128 | name = "const-random" | 137 | name = "const-random" |
129 | version = "0.1.8" | 138 | version = "0.1.8" |
130 | source = "registry+https://github.com/rust-lang/crates.io-index" | 139 | source = "registry+https://github.com/rust-lang/crates.io-index" |
@@ -152,12 +161,12 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" | |||
152 | 161 | ||
153 | [[package]] | 162 | [[package]] |
154 | name = "crossbeam-channel" | 163 | name = "crossbeam-channel" |
155 | version = "0.4.2" | 164 | version = "0.4.3" |
156 | source = "registry+https://github.com/rust-lang/crates.io-index" | 165 | source = "registry+https://github.com/rust-lang/crates.io-index" |
157 | checksum = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061" | 166 | checksum = "09ee0cc8804d5393478d743b035099520087a5186f3b93fa58cec08fa62407b6" |
158 | dependencies = [ | 167 | dependencies = [ |
168 | "cfg-if", | ||
159 | "crossbeam-utils", | 169 | "crossbeam-utils", |
160 | "maybe-uninit", | ||
161 | ] | 170 | ] |
162 | 171 | ||
163 | [[package]] | 172 | [[package]] |
@@ -172,6 +181,31 @@ dependencies = [ | |||
172 | ] | 181 | ] |
173 | 182 | ||
174 | [[package]] | 183 | [[package]] |
184 | name = "crossterm" | ||
185 | version = "0.17.7" | ||
186 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
187 | checksum = "6f4919d60f26ae233e14233cc39746c8c8bb8cd7b05840ace83604917b51b6c7" | ||
188 | dependencies = [ | ||
189 | "bitflags", | ||
190 | "crossterm_winapi", | ||
191 | "lazy_static", | ||
192 | "libc", | ||
193 | "mio 0.7.0", | ||
194 | "parking_lot", | ||
195 | "signal-hook", | ||
196 | "winapi 0.3.9", | ||
197 | ] | ||
198 | |||
199 | [[package]] | ||
200 | name = "crossterm_winapi" | ||
201 | version = "0.6.1" | ||
202 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
203 | checksum = "057b7146d02fb50175fd7dbe5158f6097f33d02831f43b4ee8ae4ddf67b68f5c" | ||
204 | dependencies = [ | ||
205 | "winapi 0.3.9", | ||
206 | ] | ||
207 | |||
208 | [[package]] | ||
175 | name = "ctor" | 209 | name = "ctor" |
176 | version = "0.1.15" | 210 | version = "0.1.15" |
177 | source = "registry+https://github.com/rust-lang/crates.io-index" | 211 | source = "registry+https://github.com/rust-lang/crates.io-index" |
@@ -190,6 +224,7 @@ dependencies = [ | |||
190 | "ahash 0.3.8", | 224 | "ahash 0.3.8", |
191 | "cfg-if", | 225 | "cfg-if", |
192 | "crossbeam-channel", | 226 | "crossbeam-channel", |
227 | "crossterm", | ||
193 | "cursive_core", | 228 | "cursive_core", |
194 | "enumset", | 229 | "enumset", |
195 | "lazy_static", | 230 | "lazy_static", |
@@ -260,7 +295,7 @@ dependencies = [ | |||
260 | 295 | ||
261 | [[package]] | 296 | [[package]] |
262 | name = "dijo" | 297 | name = "dijo" |
263 | version = "0.1.4" | 298 | version = "0.2.3" |
264 | dependencies = [ | 299 | dependencies = [ |
265 | "chrono", | 300 | "chrono", |
266 | "clap", | 301 | "clap", |
@@ -271,6 +306,7 @@ dependencies = [ | |||
271 | "notify", | 306 | "notify", |
272 | "serde", | 307 | "serde", |
273 | "serde_json", | 308 | "serde_json", |
309 | "toml", | ||
274 | "typetag", | 310 | "typetag", |
275 | ] | 311 | ] |
276 | 312 | ||
@@ -522,6 +558,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
522 | checksum = "bd7d4bd64732af4bf3a67f367c27df8520ad7e230c5817b8ff485864d80242b9" | 558 | checksum = "bd7d4bd64732af4bf3a67f367c27df8520ad7e230c5817b8ff485864d80242b9" |
523 | 559 | ||
524 | [[package]] | 560 | [[package]] |
561 | name = "lock_api" | ||
562 | version = "0.3.4" | ||
563 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
564 | checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" | ||
565 | dependencies = [ | ||
566 | "scopeguard", | ||
567 | ] | ||
568 | |||
569 | [[package]] | ||
525 | name = "log" | 570 | name = "log" |
526 | version = "0.4.11" | 571 | version = "0.4.11" |
527 | source = "registry+https://github.com/rust-lang/crates.io-index" | 572 | source = "registry+https://github.com/rust-lang/crates.io-index" |
@@ -531,12 +576,6 @@ dependencies = [ | |||
531 | ] | 576 | ] |
532 | 577 | ||
533 | [[package]] | 578 | [[package]] |
534 | name = "maybe-uninit" | ||
535 | version = "2.0.0" | ||
536 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
537 | checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" | ||
538 | |||
539 | [[package]] | ||
540 | name = "mio" | 579 | name = "mio" |
541 | version = "0.6.22" | 580 | version = "0.6.22" |
542 | source = "registry+https://github.com/rust-lang/crates.io-index" | 581 | source = "registry+https://github.com/rust-lang/crates.io-index" |
@@ -549,13 +588,27 @@ dependencies = [ | |||
549 | "kernel32-sys", | 588 | "kernel32-sys", |
550 | "libc", | 589 | "libc", |
551 | "log", | 590 | "log", |
552 | "miow", | 591 | "miow 0.2.1", |
553 | "net2", | 592 | "net2", |
554 | "slab", | 593 | "slab", |
555 | "winapi 0.2.8", | 594 | "winapi 0.2.8", |
556 | ] | 595 | ] |
557 | 596 | ||
558 | [[package]] | 597 | [[package]] |
598 | name = "mio" | ||
599 | version = "0.7.0" | ||
600 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
601 | checksum = "6e9971bc8349a361217a8f2a41f5d011274686bd4436465ba51730921039d7fb" | ||
602 | dependencies = [ | ||
603 | "lazy_static", | ||
604 | "libc", | ||
605 | "log", | ||
606 | "miow 0.3.5", | ||
607 | "ntapi", | ||
608 | "winapi 0.3.9", | ||
609 | ] | ||
610 | |||
611 | [[package]] | ||
559 | name = "mio-extras" | 612 | name = "mio-extras" |
560 | version = "2.0.6" | 613 | version = "2.0.6" |
561 | source = "registry+https://github.com/rust-lang/crates.io-index" | 614 | source = "registry+https://github.com/rust-lang/crates.io-index" |
@@ -563,7 +616,7 @@ checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" | |||
563 | dependencies = [ | 616 | dependencies = [ |
564 | "lazycell", | 617 | "lazycell", |
565 | "log", | 618 | "log", |
566 | "mio", | 619 | "mio 0.6.22", |
567 | "slab", | 620 | "slab", |
568 | ] | 621 | ] |
569 | 622 | ||
@@ -580,6 +633,16 @@ dependencies = [ | |||
580 | ] | 633 | ] |
581 | 634 | ||
582 | [[package]] | 635 | [[package]] |
636 | name = "miow" | ||
637 | version = "0.3.5" | ||
638 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
639 | checksum = "07b88fb9795d4d36d62a012dfbf49a8f5cf12751f36d31a9dbe66d528e58979e" | ||
640 | dependencies = [ | ||
641 | "socket2", | ||
642 | "winapi 0.3.9", | ||
643 | ] | ||
644 | |||
645 | [[package]] | ||
583 | name = "net2" | 646 | name = "net2" |
584 | version = "0.2.34" | 647 | version = "0.2.34" |
585 | source = "registry+https://github.com/rust-lang/crates.io-index" | 648 | source = "registry+https://github.com/rust-lang/crates.io-index" |
@@ -602,13 +665,22 @@ dependencies = [ | |||
602 | "fsevent-sys", | 665 | "fsevent-sys", |
603 | "inotify", | 666 | "inotify", |
604 | "libc", | 667 | "libc", |
605 | "mio", | 668 | "mio 0.6.22", |
606 | "mio-extras", | 669 | "mio-extras", |
607 | "walkdir", | 670 | "walkdir", |
608 | "winapi 0.3.9", | 671 | "winapi 0.3.9", |
609 | ] | 672 | ] |
610 | 673 | ||
611 | [[package]] | 674 | [[package]] |
675 | name = "ntapi" | ||
676 | version = "0.3.4" | ||
677 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
678 | checksum = "7a31937dea023539c72ddae0e3571deadc1414b300483fa7aaec176168cfa9d2" | ||
679 | dependencies = [ | ||
680 | "winapi 0.3.9", | ||
681 | ] | ||
682 | |||
683 | [[package]] | ||
612 | name = "num" | 684 | name = "num" |
613 | version = "0.3.0" | 685 | version = "0.3.0" |
614 | source = "registry+https://github.com/rust-lang/crates.io-index" | 686 | source = "registry+https://github.com/rust-lang/crates.io-index" |
@@ -687,6 +759,30 @@ dependencies = [ | |||
687 | ] | 759 | ] |
688 | 760 | ||
689 | [[package]] | 761 | [[package]] |
762 | name = "parking_lot" | ||
763 | version = "0.10.2" | ||
764 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
765 | checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" | ||
766 | dependencies = [ | ||
767 | "lock_api", | ||
768 | "parking_lot_core", | ||
769 | ] | ||
770 | |||
771 | [[package]] | ||
772 | name = "parking_lot_core" | ||
773 | version = "0.7.2" | ||
774 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
775 | checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" | ||
776 | dependencies = [ | ||
777 | "cfg-if", | ||
778 | "cloudabi", | ||
779 | "libc", | ||
780 | "redox_syscall", | ||
781 | "smallvec", | ||
782 | "winapi 0.3.9", | ||
783 | ] | ||
784 | |||
785 | [[package]] | ||
690 | name = "proc-macro-hack" | 786 | name = "proc-macro-hack" |
691 | version = "0.5.16" | 787 | version = "0.5.16" |
692 | source = "registry+https://github.com/rust-lang/crates.io-index" | 788 | source = "registry+https://github.com/rust-lang/crates.io-index" |
@@ -694,9 +790,9 @@ checksum = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4" | |||
694 | 790 | ||
695 | [[package]] | 791 | [[package]] |
696 | name = "proc-macro2" | 792 | name = "proc-macro2" |
697 | version = "1.0.18" | 793 | version = "1.0.19" |
698 | source = "registry+https://github.com/rust-lang/crates.io-index" | 794 | source = "registry+https://github.com/rust-lang/crates.io-index" |
699 | checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa" | 795 | checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12" |
700 | dependencies = [ | 796 | dependencies = [ |
701 | "unicode-xid", | 797 | "unicode-xid", |
702 | ] | 798 | ] |
@@ -764,6 +860,12 @@ dependencies = [ | |||
764 | ] | 860 | ] |
765 | 861 | ||
766 | [[package]] | 862 | [[package]] |
863 | name = "scopeguard" | ||
864 | version = "1.1.0" | ||
865 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
866 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" | ||
867 | |||
868 | [[package]] | ||
767 | name = "serde" | 869 | name = "serde" |
768 | version = "1.0.114" | 870 | version = "1.0.114" |
769 | source = "registry+https://github.com/rust-lang/crates.io-index" | 871 | source = "registry+https://github.com/rust-lang/crates.io-index" |
@@ -801,6 +903,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
801 | checksum = "604508c1418b99dfe1925ca9224829bb2a8a9a04dda655cc01fcad46f4ab05ed" | 903 | checksum = "604508c1418b99dfe1925ca9224829bb2a8a9a04dda655cc01fcad46f4ab05ed" |
802 | dependencies = [ | 904 | dependencies = [ |
803 | "libc", | 905 | "libc", |
906 | "mio 0.7.0", | ||
804 | "signal-hook-registry", | 907 | "signal-hook-registry", |
805 | ] | 908 | ] |
806 | 909 | ||
@@ -821,6 +924,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
821 | checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" | 924 | checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" |
822 | 925 | ||
823 | [[package]] | 926 | [[package]] |
927 | name = "smallvec" | ||
928 | version = "1.4.1" | ||
929 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
930 | checksum = "3757cb9d89161a2f24e1cf78efa0c1fcff485d18e3f55e0aa3480824ddaa0f3f" | ||
931 | |||
932 | [[package]] | ||
933 | name = "socket2" | ||
934 | version = "0.3.12" | ||
935 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
936 | checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918" | ||
937 | dependencies = [ | ||
938 | "cfg-if", | ||
939 | "libc", | ||
940 | "redox_syscall", | ||
941 | "winapi 0.3.9", | ||
942 | ] | ||
943 | |||
944 | [[package]] | ||
824 | name = "stable_deref_trait" | 945 | name = "stable_deref_trait" |
825 | version = "1.2.0" | 946 | version = "1.2.0" |
826 | source = "registry+https://github.com/rust-lang/crates.io-index" | 947 | source = "registry+https://github.com/rust-lang/crates.io-index" |
@@ -840,9 +961,9 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" | |||
840 | 961 | ||
841 | [[package]] | 962 | [[package]] |
842 | name = "syn" | 963 | name = "syn" |
843 | version = "1.0.34" | 964 | version = "1.0.35" |
844 | source = "registry+https://github.com/rust-lang/crates.io-index" | 965 | source = "registry+https://github.com/rust-lang/crates.io-index" |
845 | checksum = "936cae2873c940d92e697597c5eee105fb570cd5689c695806f672883653349b" | 966 | checksum = "fb7f4c519df8c117855e19dd8cc851e89eb746fe7a73f0157e0d95fdec5369b0" |
846 | dependencies = [ | 967 | dependencies = [ |
847 | "proc-macro2", | 968 | "proc-macro2", |
848 | "quote", | 969 | "quote", |
@@ -881,6 +1002,15 @@ dependencies = [ | |||
881 | ] | 1002 | ] |
882 | 1003 | ||
883 | [[package]] | 1004 | [[package]] |
1005 | name = "toml" | ||
1006 | version = "0.5.6" | ||
1007 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
1008 | checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" | ||
1009 | dependencies = [ | ||
1010 | "serde", | ||
1011 | ] | ||
1012 | |||
1013 | [[package]] | ||
884 | name = "typetag" | 1014 | name = "typetag" |
885 | version = "0.1.5" | 1015 | version = "0.1.5" |
886 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" |
@@ -1,6 +1,6 @@ | |||
1 | [package] | 1 | [package] |
2 | name = "dijo" | 2 | name = "dijo" |
3 | version = "0.1.4" | 3 | version = "0.2.3" |
4 | authors = ["Akshay <[email protected]>"] | 4 | authors = ["Akshay <[email protected]>"] |
5 | edition = "2018" | 5 | edition = "2018" |
6 | description = "Scriptable, curses-based, digital habit tracker" | 6 | description = "Scriptable, curses-based, digital habit tracker" |
@@ -19,11 +19,11 @@ typetag = "0.1.4" | |||
19 | directories = "3.0.1" | 19 | directories = "3.0.1" |
20 | clap = "2.33" | 20 | clap = "2.33" |
21 | notify = "4.0" | 21 | notify = "4.0" |
22 | toml = "0.5.6" | ||
22 | 23 | ||
23 | [dependencies.cursive] | 24 | [dependencies.cursive] |
24 | version = "0.15" | 25 | version = "0.15" |
25 | default-features = false | 26 | default-features = false |
26 | features = ["termion-backend"] | ||
27 | 27 | ||
28 | [dependencies.chrono] | 28 | [dependencies.chrono] |
29 | version = "0.4" | 29 | version = "0.4" |
@@ -32,3 +32,8 @@ features = ["serde"] | |||
32 | [dependencies.serde] | 32 | [dependencies.serde] |
33 | version = "1.0.103" | 33 | version = "1.0.103" |
34 | features = ["derive"] | 34 | features = ["derive"] |
35 | |||
36 | [features] | ||
37 | default = ["termion-backend"] | ||
38 | termion-backend = ["cursive/termion-backend"] | ||
39 | crossterm-backend = ["cursive/crossterm-backend"] | ||
@@ -19,7 +19,7 @@ much like a certain text editor. | |||
19 | - **vim like command mode**: add with `:add`, delete with | 19 | - **vim like command mode**: add with `:add`, delete with |
20 | `:delete` and above all, quit with `:q`!. | 20 | `:delete` and above all, quit with `:q`!. |
21 | - **fully scriptable**: [configure `dijo` to | 21 | - **fully scriptable**: [configure `dijo` to |
22 | track your `git` commits](./Auto-Habits)! | 22 | track your `git` commits](https://github.com/NerdyPepper/dijo/wiki/Auto-Habits)! |
23 | 23 | ||
24 | ### Install | 24 | ### Install |
25 | 25 | ||
@@ -32,6 +32,12 @@ $ rustup update | |||
32 | $ cargo install dijo | 32 | $ cargo install dijo |
33 | ``` | 33 | ``` |
34 | 34 | ||
35 | `dijo` on nixpkgs (maintained by [@Infinisil](https://github.com/Infinisil)): | ||
36 | |||
37 | ``` | ||
38 | $ nix-env -f channel:nixpkgs-unstable -iA dijo | ||
39 | ``` | ||
40 | |||
35 | If you aren't familiar with `cargo` or Rust, read the [complete | 41 | If you aren't familiar with `cargo` or Rust, read the [complete |
36 | installation](https://github.com/NerdyPepper/dijo/wiki/Install) | 42 | installation](https://github.com/NerdyPepper/dijo/wiki/Install) |
37 | guide. | 43 | guide. |
diff --git a/src/app/impl_self.rs b/src/app/impl_self.rs index 744f906..5cd9616 100644 --- a/src/app/impl_self.rs +++ b/src/app/impl_self.rs | |||
@@ -13,8 +13,7 @@ use notify::{watcher, RecursiveMode, Watcher}; | |||
13 | 13 | ||
14 | use crate::command::{Command, CommandLineError}; | 14 | use crate::command::{Command, CommandLineError}; |
15 | use crate::habit::{Bit, Count, HabitWrapper, TrackEvent, ViewMode}; | 15 | use crate::habit::{Bit, Count, HabitWrapper, TrackEvent, ViewMode}; |
16 | use crate::utils; | 16 | use crate::utils::{self, GRID_WIDTH, VIEW_HEIGHT, VIEW_WIDTH}; |
17 | use crate::CONFIGURATION; | ||
18 | 17 | ||
19 | use crate::app::{App, MessageKind, StatusLine}; | 18 | use crate::app::{App, MessageKind, StatusLine}; |
20 | 19 | ||
@@ -37,6 +36,10 @@ impl App { | |||
37 | self.habits.push(h); | 36 | self.habits.push(h); |
38 | } | 37 | } |
39 | 38 | ||
39 | pub fn list_habits(&self) -> Vec<String> { | ||
40 | self.habits.iter().map(|x| x.name()).collect::<Vec<_>>() | ||
41 | } | ||
42 | |||
40 | pub fn delete_by_name(&mut self, name: &str) { | 43 | pub fn delete_by_name(&mut self, name: &str) { |
41 | let old_len = self.habits.len(); | 44 | let old_len = self.habits.len(); |
42 | self.habits.retain(|h| h.name() != name); | 45 | self.habits.retain(|h| h.name() != name); |
@@ -83,7 +86,6 @@ impl App { | |||
83 | } | 86 | } |
84 | 87 | ||
85 | pub fn set_focus(&mut self, d: Absolute) { | 88 | pub fn set_focus(&mut self, d: Absolute) { |
86 | let grid_width = CONFIGURATION.grid_width; | ||
87 | match d { | 89 | match d { |
88 | Absolute::Right => { | 90 | Absolute::Right => { |
89 | if self.focus != self.habits.len() - 1 { | 91 | if self.focus != self.habits.len() - 1 { |
@@ -96,15 +98,15 @@ impl App { | |||
96 | } | 98 | } |
97 | } | 99 | } |
98 | Absolute::Down => { | 100 | Absolute::Down => { |
99 | if self.focus + grid_width < self.habits.len() - 1 { | 101 | if self.focus + GRID_WIDTH < self.habits.len() - 1 { |
100 | self.focus += grid_width; | 102 | self.focus += GRID_WIDTH; |
101 | } else { | 103 | } else { |
102 | self.focus = self.habits.len() - 1; | 104 | self.focus = self.habits.len() - 1; |
103 | } | 105 | } |
104 | } | 106 | } |
105 | Absolute::Up => { | 107 | Absolute::Up => { |
106 | if self.focus as isize - grid_width as isize >= 0 { | 108 | if self.focus as isize - GRID_WIDTH as isize >= 0 { |
107 | self.focus -= grid_width; | 109 | self.focus -= GRID_WIDTH; |
108 | } else { | 110 | } else { |
109 | self.focus = 0; | 111 | self.focus = 0; |
110 | } | 112 | } |
@@ -118,13 +120,13 @@ impl App { | |||
118 | } | 120 | } |
119 | 121 | ||
120 | pub fn status(&self) -> StatusLine { | 122 | pub fn status(&self) -> StatusLine { |
121 | let today = chrono::Local::now().naive_utc().date(); | 123 | let today = chrono::Local::now().naive_local().date(); |
122 | let remaining = self.habits.iter().map(|h| h.remaining(today)).sum::<u32>(); | 124 | let remaining = self.habits.iter().map(|h| h.remaining(today)).sum::<u32>(); |
123 | let total = self.habits.iter().map(|h| h.goal()).sum::<u32>(); | 125 | let total = self.habits.iter().map(|h| h.goal()).sum::<u32>(); |
124 | let completed = total - remaining; | 126 | let completed = total - remaining; |
125 | 127 | ||
126 | let timestamp = if self.view_month_offset == 0 { | 128 | let timestamp = if self.view_month_offset == 0 { |
127 | format!("{}", Local::now().date().format("%d/%b/%y"),) | 129 | format!("{}", Local::now().naive_local().date().format("%d/%b/%y"),) |
128 | } else { | 130 | } else { |
129 | let months = self.view_month_offset; | 131 | let months = self.view_month_offset; |
130 | format!("{}", format!("{} months ago", months),) | 132 | format!("{}", format!("{} months ago", months),) |
@@ -142,12 +144,10 @@ impl App { | |||
142 | } | 144 | } |
143 | 145 | ||
144 | pub fn max_size(&self) -> Vec2 { | 146 | pub fn max_size(&self) -> Vec2 { |
145 | let grid_width = CONFIGURATION.grid_width; | 147 | let width = GRID_WIDTH * VIEW_WIDTH; |
146 | let width = grid_width * CONFIGURATION.view_width; | ||
147 | let height = { | 148 | let height = { |
148 | if !self.habits.is_empty() { | 149 | if !self.habits.is_empty() { |
149 | (CONFIGURATION.view_height as f64 | 150 | (VIEW_HEIGHT as f64 * (self.habits.len() as f64 / GRID_WIDTH as f64).ceil()) |
150 | * (self.habits.len() as f64 / grid_width as f64).ceil()) | ||
151 | as usize | 151 | as usize |
152 | } else { | 152 | } else { |
153 | 0 | 153 | 0 |
@@ -207,12 +207,18 @@ impl App { | |||
207 | .iter_mut() | 207 | .iter_mut() |
208 | .find(|x| x.name() == name && x.is_auto()); | 208 | .find(|x| x.name() == name && x.is_auto()); |
209 | if let Some(h) = target_habit { | 209 | if let Some(h) = target_habit { |
210 | h.modify(Local::now().naive_utc().date(), event); | 210 | h.modify(Local::now().naive_local().date(), event); |
211 | } | 211 | } |
212 | }; | 212 | }; |
213 | match result { | 213 | match result { |
214 | Ok(c) => match c { | 214 | Ok(c) => match c { |
215 | Command::Add(name, goal, auto) => { | 215 | Command::Add(name, goal, auto) => { |
216 | if let Some(_) = self.habits.iter().find(|x| x.name() == name) { | ||
217 | self.message.set_kind(MessageKind::Error); | ||
218 | self.message | ||
219 | .set_message(format!("Habit `{}` already exist", &name)); | ||
220 | return; | ||
221 | } | ||
216 | let kind = if goal == Some(1) { "bit" } else { "count" }; | 222 | let kind = if goal == Some(1) { "bit" } else { "count" }; |
217 | if kind == "count" { | 223 | if kind == "count" { |
218 | self.add_habit(Box::new(Count::new(name, goal.unwrap_or(0), auto))); | 224 | self.add_habit(Box::new(Count::new(name, goal.unwrap_or(0), auto))); |
@@ -230,7 +236,31 @@ impl App { | |||
230 | Command::TrackDown(name) => { | 236 | Command::TrackDown(name) => { |
231 | _track(&name, TrackEvent::Decrement); | 237 | _track(&name, TrackEvent::Decrement); |
232 | } | 238 | } |
233 | Command::Quit => self.save_state(), | 239 | Command::Help(input) => { |
240 | if let Some(topic) = input.as_ref().map(String::as_ref) { | ||
241 | self.message.set_message( | ||
242 | match topic { | ||
243 | "a" | "add" => "add <habit-name> [goal] (alias: a)", | ||
244 | "aa" | "add-auto" => "add-auto <habit-name> [goal] (alias: aa)", | ||
245 | "d" | "delete" => "delete <habit-name> (alias: d)", | ||
246 | "mprev" | "month-prev" => "month-prev (alias: mprev)", | ||
247 | "mnext" | "month-next" => "month-next (alias: mnext)", | ||
248 | "tup" | "track-up" => "track-up <auto-habit-name> (alias: tup)", | ||
249 | "tdown" | "track-down" => "track-down <auto-habit-name> (alias: tdown)", | ||
250 | "q" | "quit" => "quit dijo", | ||
251 | "w" | "write" => "write current state to disk (alias: w)", | ||
252 | "h"|"?" | "help" => "help [<command>|commands|keys] (aliases: h, ?)", | ||
253 | "cmds" | "commands" => "add, add-auto, delete, month-{prev,next}, track-{up,down}, help, quit", | ||
254 | "keys" => "TODO", // TODO (view?) | ||
255 | _ => "unknown command or help topic.", | ||
256 | } | ||
257 | ) | ||
258 | } else { | ||
259 | // TODO (view?) | ||
260 | self.message.set_message("help <command>|commands|keys") | ||
261 | } | ||
262 | } | ||
263 | Command::Quit | Command::Write => self.save_state(), | ||
234 | Command::MonthNext => self.sift_forward(), | 264 | Command::MonthNext => self.sift_forward(), |
235 | Command::MonthPrev => self.sift_backward(), | 265 | Command::MonthPrev => self.sift_backward(), |
236 | Command::Blank => {} | 266 | Command::Blank => {} |
diff --git a/src/app/impl_view.rs b/src/app/impl_view.rs index 892b00c..395cac4 100644 --- a/src/app/impl_view.rs +++ b/src/app/impl_view.rs | |||
@@ -12,21 +12,17 @@ use notify::DebouncedEvent; | |||
12 | 12 | ||
13 | use crate::app::{App, MessageKind}; | 13 | use crate::app::{App, MessageKind}; |
14 | use crate::habit::{HabitWrapper, ViewMode}; | 14 | use crate::habit::{HabitWrapper, ViewMode}; |
15 | use crate::utils; | 15 | use crate::utils::{self, GRID_WIDTH, VIEW_HEIGHT, VIEW_WIDTH}; |
16 | use crate::CONFIGURATION; | ||
17 | 16 | ||
18 | impl View for App { | 17 | impl View for App { |
19 | fn draw(&self, printer: &Printer) { | 18 | fn draw(&self, printer: &Printer) { |
20 | let grid_width = CONFIGURATION.grid_width; | ||
21 | let view_width = CONFIGURATION.view_width; | ||
22 | let view_height = CONFIGURATION.view_height; | ||
23 | let mut offset = Vec2::zero(); | 19 | let mut offset = Vec2::zero(); |
24 | for (idx, habit) in self.habits.iter().enumerate() { | 20 | for (idx, habit) in self.habits.iter().enumerate() { |
25 | if idx >= grid_width && idx % grid_width == 0 { | 21 | if idx >= GRID_WIDTH && idx % GRID_WIDTH == 0 { |
26 | offset = offset.map_y(|y| y + view_height).map_x(|_| 0); | 22 | offset = offset.map_y(|y| y + VIEW_HEIGHT).map_x(|_| 0); |
27 | } | 23 | } |
28 | habit.draw(&printer.offset(offset).focused(self.focus == idx)); | 24 | habit.draw(&printer.offset(offset).focused(self.focus == idx)); |
29 | offset = offset.map_x(|x| x + view_width + 2); | 25 | offset = offset.map_x(|x| x + VIEW_WIDTH + 2); |
30 | } | 26 | } |
31 | 27 | ||
32 | offset = offset.map_x(|_| 0).map_y(|_| self.max_size().y - 2); | 28 | offset = offset.map_x(|_| 0).map_y(|_| self.max_size().y - 2); |
@@ -45,13 +41,10 @@ impl View for App { | |||
45 | } | 41 | } |
46 | 42 | ||
47 | fn required_size(&mut self, _: Vec2) -> Vec2 { | 43 | fn required_size(&mut self, _: Vec2) -> Vec2 { |
48 | let grid_width = CONFIGURATION.grid_width; | 44 | let width = GRID_WIDTH * (VIEW_WIDTH + 2); |
49 | let view_width = CONFIGURATION.view_width; | ||
50 | let view_height = CONFIGURATION.view_height; | ||
51 | let width = grid_width * (view_width + 2); | ||
52 | let height = { | 45 | let height = { |
53 | if self.habits.len() > 0 { | 46 | if self.habits.len() > 0 { |
54 | (view_height as f64 * (self.habits.len() as f64 / grid_width as f64).ceil()) | 47 | (VIEW_HEIGHT as f64 * (self.habits.len() as f64 / GRID_WIDTH as f64).ceil()) |
55 | as usize | 48 | as usize |
56 | } else { | 49 | } else { |
57 | 0 | 50 | 0 |
@@ -102,25 +95,6 @@ impl View for App { | |||
102 | self.set_focus(Absolute::Down); | 95 | self.set_focus(Absolute::Down); |
103 | return EventResult::Consumed(None); | 96 | return EventResult::Consumed(None); |
104 | } | 97 | } |
105 | Event::Char('d') => { | ||
106 | if self.habits.is_empty() { | ||
107 | return EventResult::Consumed(None); | ||
108 | } | ||
109 | self.habits.remove(self.focus); | ||
110 | self.focus = self.focus.checked_sub(1).unwrap_or(0); | ||
111 | return EventResult::Consumed(None); | ||
112 | } | ||
113 | Event::Char('w') => { | ||
114 | // helper bind to test write to file | ||
115 | let j = serde_json::to_string_pretty(&self.habits).unwrap(); | ||
116 | let mut file = File::create("foo.txt").unwrap(); | ||
117 | file.write_all(j.as_bytes()).unwrap(); | ||
118 | return EventResult::Consumed(None); | ||
119 | } | ||
120 | Event::Char('q') => { | ||
121 | self.save_state(); | ||
122 | return EventResult::with_cb(|s| s.quit()); | ||
123 | } | ||
124 | Event::Char('v') => { | 98 | Event::Char('v') => { |
125 | if self.habits.is_empty() { | 99 | if self.habits.is_empty() { |
126 | return EventResult::Consumed(None); | 100 | return EventResult::Consumed(None); |
diff --git a/src/command.rs b/src/command.rs index f856b00..4f3e491 100644 --- a/src/command.rs +++ b/src/command.rs | |||
@@ -1,21 +1,73 @@ | |||
1 | use std::fmt; | 1 | use std::fmt; |
2 | 2 | ||
3 | use cursive::event::{Event, EventResult, Key}; | ||
3 | use cursive::theme::{BaseColor, Color, ColorStyle}; | 4 | use cursive::theme::{BaseColor, Color, ColorStyle}; |
4 | use cursive::view::Resizable; | 5 | use cursive::view::Resizable; |
5 | use cursive::views::{EditView, LinearLayout, TextView}; | 6 | use cursive::views::{EditView, LinearLayout, OnEventView, TextView}; |
6 | use cursive::Cursive; | 7 | use cursive::Cursive; |
7 | 8 | ||
8 | use crate::{app::App, CONFIGURATION}; | 9 | use crate::app::App; |
10 | use crate::utils::{GRID_WIDTH, VIEW_WIDTH}; | ||
11 | |||
12 | static COMMANDS: &'static [&'static str] = &[ | ||
13 | "add", | ||
14 | "add-auto", | ||
15 | "delete", | ||
16 | "track-up", | ||
17 | "track-down", | ||
18 | "month-prev", | ||
19 | "month-next", | ||
20 | "quit", | ||
21 | "write", | ||
22 | "help", | ||
23 | ]; | ||
24 | |||
25 | fn get_command_completion(prefix: &str) -> Option<String> { | ||
26 | let first_match = COMMANDS.iter().filter(|&x| x.starts_with(prefix)).next(); | ||
27 | return first_match.map(|&x| x.into()); | ||
28 | } | ||
29 | |||
30 | fn get_habit_completion(prefix: &str, habit_names: &[String]) -> Option<String> { | ||
31 | let first_match = habit_names.iter().filter(|&x| x.starts_with(prefix)).next(); | ||
32 | return first_match.map(|x| x.into()); | ||
33 | } | ||
9 | 34 | ||
10 | pub fn open_command_window(s: &mut Cursive) { | 35 | pub fn open_command_window(s: &mut Cursive) { |
11 | let command_window = EditView::new() | 36 | let habit_list: Vec<String> = s |
12 | .filler(" ") | 37 | .call_on_name("Main", |view: &mut App| { |
13 | .on_submit(call_on_app) | 38 | return view.list_habits(); |
14 | .style(ColorStyle::new( | 39 | }) |
15 | Color::Dark(BaseColor::Black), | 40 | .unwrap(); |
16 | Color::Dark(BaseColor::White), | 41 | let style = ColorStyle::new(Color::Dark(BaseColor::Black), Color::Dark(BaseColor::White)); |
17 | )) | 42 | let command_window = OnEventView::new( |
18 | .fixed_width(CONFIGURATION.view_width * CONFIGURATION.grid_width); | 43 | EditView::new() |
44 | .filler(" ") | ||
45 | .on_submit(call_on_app) | ||
46 | .style(style), | ||
47 | ) | ||
48 | .on_event_inner( | ||
49 | Event::Key(Key::Tab), | ||
50 | move |view: &mut EditView, _: &Event| { | ||
51 | let contents = view.get_content(); | ||
52 | if !contents.contains(" ") { | ||
53 | let completion = get_command_completion(&*contents); | ||
54 | if let Some(c) = completion { | ||
55 | let cb = view.set_content(c); | ||
56 | return Some(EventResult::Consumed(Some(cb))); | ||
57 | }; | ||
58 | return None; | ||
59 | } else { | ||
60 | let word = contents.split(' ').last().unwrap(); | ||
61 | let completion = get_habit_completion(word, &habit_list); | ||
62 | if let Some(c) = completion { | ||
63 | let cb = view.set_content(format!("{}", contents) + &c[word.len()..]); | ||
64 | return Some(EventResult::Consumed(Some(cb))); | ||
65 | }; | ||
66 | return None; | ||
67 | } | ||
68 | }, | ||
69 | ) | ||
70 | .fixed_width(VIEW_WIDTH * GRID_WIDTH); | ||
19 | s.call_on_name("Frame", |view: &mut LinearLayout| { | 71 | s.call_on_name("Frame", |view: &mut LinearLayout| { |
20 | let mut commandline = LinearLayout::horizontal() | 72 | let mut commandline = LinearLayout::horizontal() |
21 | .child(TextView::new(":")) | 73 | .child(TextView::new(":")) |
@@ -59,15 +111,17 @@ pub enum Command { | |||
59 | Delete(String), | 111 | Delete(String), |
60 | TrackUp(String), | 112 | TrackUp(String), |
61 | TrackDown(String), | 113 | TrackDown(String), |
114 | Help(Option<String>), | ||
115 | Write, | ||
62 | Quit, | 116 | Quit, |
63 | Blank, | 117 | Blank, |
64 | } | 118 | } |
65 | 119 | ||
66 | #[derive(Debug)] | 120 | #[derive(Debug)] |
67 | pub enum CommandLineError { | 121 | pub enum CommandLineError { |
68 | InvalidCommand(String), | 122 | InvalidCommand(String), // command name |
69 | InvalidArg(u32), // position | 123 | InvalidArg(u32), // position |
70 | NotEnoughArgs(String, u32), | 124 | NotEnoughArgs(String, u32), // command name, required no. of args |
71 | } | 125 | } |
72 | 126 | ||
73 | impl std::error::Error for CommandLineError {} | 127 | impl std::error::Error for CommandLineError {} |
@@ -134,9 +188,16 @@ impl Command { | |||
134 | } | 188 | } |
135 | return Ok(Command::TrackDown(args[0].to_string())); | 189 | return Ok(Command::TrackDown(args[0].to_string())); |
136 | } | 190 | } |
191 | "h" | "?" | "help" => { | ||
192 | if args.is_empty() { | ||
193 | return Ok(Command::Help(None)); | ||
194 | } | ||
195 | return Ok(Command::Help(Some(args[0].to_string()))); | ||
196 | } | ||
137 | "mprev" | "month-prev" => return Ok(Command::MonthPrev), | 197 | "mprev" | "month-prev" => return Ok(Command::MonthPrev), |
138 | "mnext" | "month-next" => return Ok(Command::MonthNext), | 198 | "mnext" | "month-next" => return Ok(Command::MonthNext), |
139 | "q" | "quit" => return Ok(Command::Quit), | 199 | "q" | "quit" => return Ok(Command::Quit), |
200 | "w" | "write" => return Ok(Command::Write), | ||
140 | "" => return Ok(Command::Blank), | 201 | "" => return Ok(Command::Blank), |
141 | s => return Err(CommandLineError::InvalidCommand(s.into())), | 202 | s => return Err(CommandLineError::InvalidCommand(s.into())), |
142 | } | 203 | } |
diff --git a/src/habit/bit.rs b/src/habit/bit.rs index 8fa14c2..2bbb0ac 100644 --- a/src/habit/bit.rs +++ b/src/habit/bit.rs | |||
@@ -18,9 +18,9 @@ impl fmt::Display for CustomBool { | |||
18 | f, | 18 | f, |
19 | "{:^3}", | 19 | "{:^3}", |
20 | if self.0 { | 20 | if self.0 { |
21 | CONFIGURATION.true_chr | 21 | CONFIGURATION.look.true_chr |
22 | } else { | 22 | } else { |
23 | CONFIGURATION.false_chr | 23 | CONFIGURATION.look.false_chr |
24 | } | 24 | } |
25 | ) | 25 | ) |
26 | } | 26 | } |
diff --git a/src/main.rs b/src/main.rs index 14ddf03..5280b35 100644 --- a/src/main.rs +++ b/src/main.rs | |||
@@ -12,7 +12,13 @@ use crate::command::{open_command_window, Command}; | |||
12 | use crate::utils::{load_configuration_file, AppConfig}; | 12 | use crate::utils::{load_configuration_file, AppConfig}; |
13 | 13 | ||
14 | use clap::{App as ClapApp, Arg}; | 14 | use clap::{App as ClapApp, Arg}; |
15 | |||
16 | #[cfg(any(feature = "termion-backend", feature = "default"))] | ||
15 | use cursive::termion; | 17 | use cursive::termion; |
18 | |||
19 | #[cfg(feature = "crossterm-backend")] | ||
20 | use cursive::crossterm; | ||
21 | |||
16 | use cursive::views::{LinearLayout, NamedView}; | 22 | use cursive::views::{LinearLayout, NamedView}; |
17 | use lazy_static::lazy_static; | 23 | use lazy_static::lazy_static; |
18 | 24 | ||
@@ -33,6 +39,14 @@ fn main() { | |||
33 | .value_name("CMD") | 39 | .value_name("CMD") |
34 | .help("run a dijo command"), | 40 | .help("run a dijo command"), |
35 | ) | 41 | ) |
42 | .arg( | ||
43 | Arg::with_name("list") | ||
44 | .short("l") | ||
45 | .long("list") | ||
46 | .takes_value(false) | ||
47 | .help("list dijo habits") | ||
48 | .conflicts_with("command"), | ||
49 | ) | ||
36 | .get_matches(); | 50 | .get_matches(); |
37 | if let Some(c) = matches.value_of("command") { | 51 | if let Some(c) = matches.value_of("command") { |
38 | let command = Command::from_string(c); | 52 | let command = Command::from_string(c); |
@@ -49,8 +63,17 @@ fn main() { | |||
49 | "Commands other than `track-up` and `track-down` are currently not supported!" | 63 | "Commands other than `track-up` and `track-down` are currently not supported!" |
50 | ), | 64 | ), |
51 | } | 65 | } |
66 | } else if matches.is_present("list") { | ||
67 | for h in App::load_state().list_habits() { | ||
68 | println!("{}", h); | ||
69 | } | ||
52 | } else { | 70 | } else { |
71 | #[cfg(any(feature = "termion-backend", feature = "default"))] | ||
53 | let mut s = termion().unwrap(); | 72 | let mut s = termion().unwrap(); |
73 | |||
74 | #[cfg(feature = "crossterm-backend")] | ||
75 | let mut s = crossterm().unwrap(); | ||
76 | |||
54 | let app = App::load_state(); | 77 | let app = App::load_state(); |
55 | let layout = NamedView::new( | 78 | let layout = NamedView::new( |
56 | "Frame", | 79 | "Frame", |
diff --git a/src/theme.rs b/src/theme.rs index 4194777..1d2cc36 100644 --- a/src/theme.rs +++ b/src/theme.rs | |||
@@ -1,18 +1,18 @@ | |||
1 | use cursive::theme::Color::*; | 1 | use cursive::theme::Color::*; |
2 | use cursive::theme::PaletteColor::*; | 2 | use cursive::theme::PaletteColor::*; |
3 | use cursive::theme::{BaseColor, BorderStyle, Palette, Theme}; | 3 | use cursive::theme::{BorderStyle, Palette, Theme}; |
4 | 4 | ||
5 | pub fn pallete_gen() -> Palette { | 5 | pub fn pallete_gen() -> Palette { |
6 | let mut p = Palette::default(); | 6 | let mut p = Palette::default(); |
7 | p[Background] = Dark(BaseColor::Black); | 7 | p[Background] = TerminalDefault; |
8 | p[Shadow] = Light(BaseColor::Black); | 8 | p[Shadow] = TerminalDefault; |
9 | p[View] = Dark(BaseColor::Black); | 9 | p[View] = TerminalDefault; |
10 | p[Primary] = Dark(BaseColor::White); | 10 | p[Primary] = TerminalDefault; |
11 | p[Secondary] = Dark(BaseColor::Black); | 11 | p[Secondary] = TerminalDefault; |
12 | p[Tertiary] = Dark(BaseColor::Green); | 12 | p[Tertiary] = TerminalDefault; |
13 | p[TitlePrimary] = Light(BaseColor::White); | 13 | p[TitlePrimary] = TerminalDefault; |
14 | p[Highlight] = Dark(BaseColor::Red); | 14 | p[Highlight] = TerminalDefault; |
15 | p[HighlightInactive] = Dark(BaseColor::Black); | 15 | p[HighlightInactive] = TerminalDefault; |
16 | 16 | ||
17 | return p; | 17 | return p; |
18 | } | 18 | } |
diff --git a/src/utils.rs b/src/utils.rs index e6ec6ac..2453aa6 100644 --- a/src/utils.rs +++ b/src/utils.rs | |||
@@ -1,37 +1,117 @@ | |||
1 | use cursive::theme::{BaseColor, Color}; | 1 | use cursive::theme::{BaseColor, Color}; |
2 | use directories::ProjectDirs; | 2 | use directories::ProjectDirs; |
3 | use std::fs; | 3 | use serde::{Deserialize, Serialize}; |
4 | |||
5 | use std; | ||
6 | use std::default::Default; | ||
7 | use std::fs::{self, File, OpenOptions}; | ||
8 | use std::io::{Read, Write}; | ||
4 | use std::path::PathBuf; | 9 | use std::path::PathBuf; |
5 | 10 | ||
6 | pub struct AppConfig { | 11 | pub const VIEW_WIDTH: usize = 25; |
12 | pub const VIEW_HEIGHT: usize = 8; | ||
13 | pub const GRID_WIDTH: usize = 3; | ||
14 | |||
15 | #[derive(Serialize, Deserialize)] | ||
16 | pub struct Characters { | ||
17 | #[serde(default = "base_char")] | ||
7 | pub true_chr: char, | 18 | pub true_chr: char, |
19 | #[serde(default = "base_char")] | ||
8 | pub false_chr: char, | 20 | pub false_chr: char, |
21 | #[serde(default = "base_char")] | ||
9 | pub future_chr: char, | 22 | pub future_chr: char, |
23 | } | ||
10 | 24 | ||
11 | // view dimensions | 25 | fn base_char() -> char { |
12 | pub view_width: usize, | 26 | '·' |
13 | pub view_height: usize, | 27 | } |
14 | 28 | ||
15 | // app dimensions | 29 | impl Default for Characters { |
16 | pub grid_width: usize, | 30 | fn default() -> Self { |
31 | Characters { | ||
32 | true_chr: '·', | ||
33 | false_chr: '·', | ||
34 | future_chr: '·', | ||
35 | } | ||
36 | } | ||
37 | } | ||
17 | 38 | ||
18 | pub reached_color: Color, | 39 | #[derive(Serialize, Deserialize)] |
19 | pub todo_color: Color, | 40 | pub struct Colors { |
20 | pub future_color: Color, | 41 | #[serde(default = "cyan")] |
42 | pub reached: String, | ||
43 | #[serde(default = "magenta")] | ||
44 | pub todo: String, | ||
45 | #[serde(default = "light_black")] | ||
46 | pub inactive: String, | ||
47 | } | ||
48 | |||
49 | fn cyan() -> String { | ||
50 | "cyan".into() | ||
51 | } | ||
52 | fn magenta() -> String { | ||
53 | "magenta".into() | ||
54 | } | ||
55 | fn light_black() -> String { | ||
56 | "light black".into() | ||
57 | } | ||
58 | |||
59 | impl Default for Colors { | ||
60 | fn default() -> Self { | ||
61 | Colors { | ||
62 | reached: cyan(), | ||
63 | todo: magenta(), | ||
64 | inactive: light_black(), | ||
65 | } | ||
66 | } | ||
67 | } | ||
68 | |||
69 | #[derive(Serialize, Deserialize)] | ||
70 | pub struct AppConfig { | ||
71 | #[serde(default)] | ||
72 | pub look: Characters, | ||
73 | |||
74 | #[serde(default)] | ||
75 | pub colors: Colors, | ||
76 | } | ||
77 | |||
78 | impl Default for AppConfig { | ||
79 | fn default() -> Self { | ||
80 | AppConfig { | ||
81 | look: Default::default(), | ||
82 | colors: Default::default(), | ||
83 | } | ||
84 | } | ||
85 | } | ||
86 | |||
87 | impl AppConfig { | ||
88 | // TODO: implement string parsing from config.json | ||
89 | pub fn reached_color(&self) -> Color { | ||
90 | return Color::parse(&self.colors.reached).unwrap_or(Color::Dark(BaseColor::Cyan)); | ||
91 | } | ||
92 | pub fn todo_color(&self) -> Color { | ||
93 | return Color::parse(&self.colors.todo).unwrap_or(Color::Dark(BaseColor::Magenta)); | ||
94 | } | ||
95 | pub fn inactive_color(&self) -> Color { | ||
96 | return Color::parse(&self.colors.inactive).unwrap_or(Color::Light(BaseColor::Black)); | ||
97 | } | ||
21 | } | 98 | } |
22 | 99 | ||
23 | pub fn load_configuration_file() -> AppConfig { | 100 | pub fn load_configuration_file() -> AppConfig { |
24 | return AppConfig { | 101 | let cf = config_file(); |
25 | true_chr: '·', | 102 | if let Ok(ref mut f) = File::open(&cf) { |
26 | false_chr: '·', | 103 | let mut j = String::new(); |
27 | future_chr: '·', | 104 | f.read_to_string(&mut j); |
28 | view_width: 25, | 105 | return toml::from_str(&j).unwrap(); |
29 | view_height: 8, | 106 | } else { |
30 | grid_width: 3, | 107 | if let Ok(dc) = toml::to_string(&AppConfig::default()) { |
31 | reached_color: Color::Dark(BaseColor::Cyan), | 108 | match OpenOptions::new().create(true).write(true).open(&cf) { |
32 | todo_color: Color::Dark(BaseColor::Magenta), | 109 | Ok(ref mut file) => file.write(dc.as_bytes()).unwrap(), |
33 | future_color: Color::Light(BaseColor::Black), | 110 | Err(_) => 0, |
34 | }; | 111 | }; |
112 | } | ||
113 | return Default::default(); | ||
114 | } | ||
35 | } | 115 | } |
36 | 116 | ||
37 | fn project_dirs() -> ProjectDirs { | 117 | fn project_dirs() -> ProjectDirs { |
@@ -39,6 +119,14 @@ fn project_dirs() -> ProjectDirs { | |||
39 | .unwrap_or_else(|| panic!("Invalid home directory!")) | 119 | .unwrap_or_else(|| panic!("Invalid home directory!")) |
40 | } | 120 | } |
41 | 121 | ||
122 | pub fn config_file() -> PathBuf { | ||
123 | let proj_dirs = project_dirs(); | ||
124 | let mut data_file = PathBuf::from(proj_dirs.config_dir()); | ||
125 | fs::create_dir_all(&data_file); | ||
126 | data_file.push("config.toml"); | ||
127 | return data_file; | ||
128 | } | ||
129 | |||
42 | pub fn habit_file() -> PathBuf { | 130 | pub fn habit_file() -> PathBuf { |
43 | let proj_dirs = project_dirs(); | 131 | let proj_dirs = project_dirs(); |
44 | let mut data_file = PathBuf::from(proj_dirs.data_dir()); | 132 | let mut data_file = PathBuf::from(proj_dirs.data_dir()); |
diff --git a/src/views.rs b/src/views.rs index 24c8a4d..efd1391 100644 --- a/src/views.rs +++ b/src/views.rs | |||
@@ -8,6 +8,7 @@ use chrono::prelude::*; | |||
8 | use chrono::{Duration, Local, NaiveDate}; | 8 | use chrono::{Duration, Local, NaiveDate}; |
9 | 9 | ||
10 | use crate::habit::{Bit, Count, Habit, TrackEvent, ViewMode}; | 10 | use crate::habit::{Bit, Count, Habit, TrackEvent, ViewMode}; |
11 | use crate::utils::VIEW_WIDTH; | ||
11 | 12 | ||
12 | use crate::CONFIGURATION; | 13 | use crate::CONFIGURATION; |
13 | 14 | ||
@@ -36,14 +37,14 @@ where | |||
36 | let year = now.year(); | 37 | let year = now.year(); |
37 | let month = now.month(); | 38 | let month = now.month(); |
38 | 39 | ||
39 | let goal_reached_style = Style::from(CONFIGURATION.reached_color); | 40 | let goal_reached_style = Style::from(CONFIGURATION.reached_color()); |
40 | let todo_style = Style::from(CONFIGURATION.todo_color); | 41 | let todo_style = Style::from(CONFIGURATION.todo_color()); |
41 | let future_style = Style::from(CONFIGURATION.future_color); | 42 | let future_style = Style::from(CONFIGURATION.inactive_color()); |
42 | 43 | ||
43 | let strikethrough = Style::from(Effect::Strikethrough); | 44 | let strikethrough = Style::from(Effect::Strikethrough); |
44 | 45 | ||
45 | let goal_status = | 46 | let goal_status = |
46 | self.view_month_offset() == 0 && self.reached_goal(Local::now().naive_utc().date()); | 47 | self.view_month_offset() == 0 && self.reached_goal(Local::now().naive_local().date()); |
47 | 48 | ||
48 | printer.with_style( | 49 | printer.with_style( |
49 | Style::merge(&[ | 50 | Style::merge(&[ |
@@ -61,11 +62,7 @@ where | |||
61 | |p| { | 62 | |p| { |
62 | p.print( | 63 | p.print( |
63 | (0, 0), | 64 | (0, 0), |
64 | &format!( | 65 | &format!(" {:.width$} ", self.name(), width = VIEW_WIDTH - 6), |
65 | " {:.width$} ", | ||
66 | self.name(), | ||
67 | width = CONFIGURATION.view_width - 6 | ||
68 | ), | ||
69 | ); | 66 | ); |
70 | }, | 67 | }, |
71 | ); | 68 | ); |
@@ -77,12 +74,20 @@ where | |||
77 | .collect::<Vec<_>>(); | 74 | .collect::<Vec<_>>(); |
78 | for (week, line_nr) in days.chunks(7).zip(2..) { | 75 | for (week, line_nr) in days.chunks(7).zip(2..) { |
79 | let weekly_goal = self.goal() * week.len() as u32; | 76 | let weekly_goal = self.goal() * week.len() as u32; |
80 | let is_this_week = week.contains(&Local::now().naive_utc().date()); | 77 | let is_this_week = week.contains(&Local::now().naive_local().date()); |
81 | let remaining = week.iter().map(|&i| self.remaining(i)).sum::<u32>(); | 78 | let remaining = week.iter().map(|&i| self.remaining(i)).sum::<u32>(); |
82 | let completions = weekly_goal - remaining; | 79 | let completions = weekly_goal - remaining; |
83 | let full = CONFIGURATION.view_width - 8; | 80 | let full = VIEW_WIDTH - 8; |
84 | let bars_to_fill = if weekly_goal > 0 {(completions * full as u32) / weekly_goal} else {0}; | 81 | let bars_to_fill = if weekly_goal > 0 { |
85 | let percentage = if weekly_goal > 0 {(completions as f64 * 100.) / weekly_goal as f64} else {0.0}; | 82 | (completions * full as u32) / weekly_goal |
83 | } else { | ||
84 | 0 | ||
85 | }; | ||
86 | let percentage = if weekly_goal > 0 { | ||
87 | (completions as f64 * 100.) / weekly_goal as f64 | ||
88 | } else { | ||
89 | 0.0 | ||
90 | }; | ||
86 | printer.with_style(future_style, |p| { | 91 | printer.with_style(future_style, |p| { |
87 | p.print((4, line_nr), &"─".repeat(full)); | 92 | p.print((4, line_nr), &"─".repeat(full)); |
88 | }); | 93 | }); |
@@ -118,7 +123,7 @@ where | |||
118 | }); | 123 | }); |
119 | } else { | 124 | } else { |
120 | printer.with_style(future_style, |p| { | 125 | printer.with_style(future_style, |p| { |
121 | p.print(coords, &format!("{:^3}", CONFIGURATION.future_chr)); | 126 | p.print(coords, &format!("{:^3}", CONFIGURATION.look.future_chr)); |
122 | }); | 127 | }); |
123 | } | 128 | } |
124 | i += 1; | 129 | i += 1; |
@@ -141,7 +146,7 @@ where | |||
141 | } | 146 | } |
142 | 147 | ||
143 | fn on_event(&mut self, e: Event) -> EventResult { | 148 | fn on_event(&mut self, e: Event) -> EventResult { |
144 | let now = Local::now().naive_utc().date(); | 149 | let now = Local::now().naive_local().date(); |
145 | if self.is_auto() { | 150 | if self.is_auto() { |
146 | return EventResult::Ignored; | 151 | return EventResult::Ignored; |
147 | } | 152 | } |