104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "autocfg"
7 | version = "1.1.0"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
10 |
11 | [[package]]
12 | name = "bitflags"
13 | version = "1.3.2"
14 | source = "registry+https://github.com/rust-lang/crates.io-index"
15 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
16 |
17 | [[package]]
18 | name = "cassowary"
19 | version = "0.3.0"
20 | source = "registry+https://github.com/rust-lang/crates.io-index"
21 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
22 |
23 | [[package]]
24 | name = "cfg-if"
25 | version = "0.1.10"
26 | source = "registry+https://github.com/rust-lang/crates.io-index"
27 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
28 |
29 | [[package]]
30 | name = "cfg-if"
31 | version = "1.0.0"
32 | source = "registry+https://github.com/rust-lang/crates.io-index"
33 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
34 |
35 | [[package]]
36 | name = "crossterm"
37 | version = "0.23.2"
38 | source = "registry+https://github.com/rust-lang/crates.io-index"
39 | checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17"
40 | dependencies = [
41 | "bitflags",
42 | "crossterm_winapi",
43 | "libc",
44 | "mio",
45 | "parking_lot",
46 | "signal-hook",
47 | "signal-hook-mio",
48 | "winapi",
49 | ]
50 |
51 | [[package]]
52 | name = "crossterm_winapi"
53 | version = "0.9.0"
54 | source = "registry+https://github.com/rust-lang/crates.io-index"
55 | checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
56 | dependencies = [
57 | "winapi",
58 | ]
59 |
60 | [[package]]
61 | name = "dirs"
62 | version = "4.0.0"
63 | source = "registry+https://github.com/rust-lang/crates.io-index"
64 | checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
65 | dependencies = [
66 | "dirs-sys",
67 | ]
68 |
69 | [[package]]
70 | name = "dirs-sys"
71 | version = "0.3.7"
72 | source = "registry+https://github.com/rust-lang/crates.io-index"
73 | checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
74 | dependencies = [
75 | "libc",
76 | "redox_users",
77 | "winapi",
78 | ]
79 |
80 | [[package]]
81 | name = "getrandom"
82 | version = "0.2.7"
83 | source = "registry+https://github.com/rust-lang/crates.io-index"
84 | checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
85 | dependencies = [
86 | "cfg-if 1.0.0",
87 | "libc",
88 | "wasi",
89 | ]
90 |
91 | [[package]]
92 | name = "itoa"
93 | version = "0.4.6"
94 | source = "registry+https://github.com/rust-lang/crates.io-index"
95 | checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
96 |
97 | [[package]]
98 | name = "libc"
99 | version = "0.2.126"
100 | source = "registry+https://github.com/rust-lang/crates.io-index"
101 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
102 |
103 | [[package]]
104 | name = "lock_api"
105 | version = "0.4.7"
106 | source = "registry+https://github.com/rust-lang/crates.io-index"
107 | checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
108 | dependencies = [
109 | "autocfg",
110 | "scopeguard",
111 | ]
112 |
113 | [[package]]
114 | name = "log"
115 | version = "0.4.11"
116 | source = "registry+https://github.com/rust-lang/crates.io-index"
117 | checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
118 | dependencies = [
119 | "cfg-if 0.1.10",
120 | ]
121 |
122 | [[package]]
123 | name = "mio"
124 | version = "0.8.3"
125 | source = "registry+https://github.com/rust-lang/crates.io-index"
126 | checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799"
127 | dependencies = [
128 | "libc",
129 | "log",
130 | "wasi",
131 | "windows-sys",
132 | ]
133 |
134 | [[package]]
135 | name = "parking_lot"
136 | version = "0.12.1"
137 | source = "registry+https://github.com/rust-lang/crates.io-index"
138 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
139 | dependencies = [
140 | "lock_api",
141 | "parking_lot_core",
142 | ]
143 |
144 | [[package]]
145 | name = "parking_lot_core"
146 | version = "0.9.3"
147 | source = "registry+https://github.com/rust-lang/crates.io-index"
148 | checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
149 | dependencies = [
150 | "cfg-if 1.0.0",
151 | "libc",
152 | "redox_syscall",
153 | "smallvec",
154 | "windows-sys",
155 | ]
156 |
157 | [[package]]
158 | name = "proc-macro2"
159 | version = "1.0.24"
160 | source = "registry+https://github.com/rust-lang/crates.io-index"
161 | checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
162 | dependencies = [
163 | "unicode-xid",
164 | ]
165 |
166 | [[package]]
167 | name = "project_manager"
168 | version = "0.1.3"
169 | dependencies = [
170 | "crossterm",
171 | "dirs",
172 | "serde",
173 | "serde_json",
174 | "smart-default",
175 | "tui",
176 | ]
177 |
178 | [[package]]
179 | name = "quote"
180 | version = "1.0.7"
181 | source = "registry+https://github.com/rust-lang/crates.io-index"
182 | checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
183 | dependencies = [
184 | "proc-macro2",
185 | ]
186 |
187 | [[package]]
188 | name = "redox_syscall"
189 | version = "0.2.13"
190 | source = "registry+https://github.com/rust-lang/crates.io-index"
191 | checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
192 | dependencies = [
193 | "bitflags",
194 | ]
195 |
196 | [[package]]
197 | name = "redox_users"
198 | version = "0.4.3"
199 | source = "registry+https://github.com/rust-lang/crates.io-index"
200 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
201 | dependencies = [
202 | "getrandom",
203 | "redox_syscall",
204 | "thiserror",
205 | ]
206 |
207 | [[package]]
208 | name = "ryu"
209 | version = "1.0.5"
210 | source = "registry+https://github.com/rust-lang/crates.io-index"
211 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
212 |
213 | [[package]]
214 | name = "scopeguard"
215 | version = "1.1.0"
216 | source = "registry+https://github.com/rust-lang/crates.io-index"
217 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
218 |
219 | [[package]]
220 | name = "serde"
221 | version = "1.0.116"
222 | source = "registry+https://github.com/rust-lang/crates.io-index"
223 | checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5"
224 | dependencies = [
225 | "serde_derive",
226 | ]
227 |
228 | [[package]]
229 | name = "serde_derive"
230 | version = "1.0.116"
231 | source = "registry+https://github.com/rust-lang/crates.io-index"
232 | checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8"
233 | dependencies = [
234 | "proc-macro2",
235 | "quote",
236 | "syn",
237 | ]
238 |
239 | [[package]]
240 | name = "serde_json"
241 | version = "1.0.58"
242 | source = "registry+https://github.com/rust-lang/crates.io-index"
243 | checksum = "a230ea9107ca2220eea9d46de97eddcb04cd00e92d13dda78e478dd33fa82bd4"
244 | dependencies = [
245 | "itoa",
246 | "ryu",
247 | "serde",
248 | ]
249 |
250 | [[package]]
251 | name = "signal-hook"
252 | version = "0.3.14"
253 | source = "registry+https://github.com/rust-lang/crates.io-index"
254 | checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d"
255 | dependencies = [
256 | "libc",
257 | "signal-hook-registry",
258 | ]
259 |
260 | [[package]]
261 | name = "signal-hook-mio"
262 | version = "0.2.3"
263 | source = "registry+https://github.com/rust-lang/crates.io-index"
264 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
265 | dependencies = [
266 | "libc",
267 | "mio",
268 | "signal-hook",
269 | ]
270 |
271 | [[package]]
272 | name = "signal-hook-registry"
273 | version = "1.4.0"
274 | source = "registry+https://github.com/rust-lang/crates.io-index"
275 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
276 | dependencies = [
277 | "libc",
278 | ]
279 |
280 | [[package]]
281 | name = "smallvec"
282 | version = "1.8.0"
283 | source = "registry+https://github.com/rust-lang/crates.io-index"
284 | checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
285 |
286 | [[package]]
287 | name = "smart-default"
288 | version = "0.6.0"
289 | source = "registry+https://github.com/rust-lang/crates.io-index"
290 | checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6"
291 | dependencies = [
292 | "proc-macro2",
293 | "quote",
294 | "syn",
295 | ]
296 |
297 | [[package]]
298 | name = "syn"
299 | version = "1.0.67"
300 | source = "registry+https://github.com/rust-lang/crates.io-index"
301 | checksum = "6498a9efc342871f91cc2d0d694c674368b4ceb40f62b65a7a08c3792935e702"
302 | dependencies = [
303 | "proc-macro2",
304 | "quote",
305 | "unicode-xid",
306 | ]
307 |
308 | [[package]]
309 | name = "thiserror"
310 | version = "1.0.31"
311 | source = "registry+https://github.com/rust-lang/crates.io-index"
312 | checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
313 | dependencies = [
314 | "thiserror-impl",
315 | ]
316 |
317 | [[package]]
318 | name = "thiserror-impl"
319 | version = "1.0.31"
320 | source = "registry+https://github.com/rust-lang/crates.io-index"
321 | checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
322 | dependencies = [
323 | "proc-macro2",
324 | "quote",
325 | "syn",
326 | ]
327 |
328 | [[package]]
329 | name = "tui"
330 | version = "0.18.0"
331 | source = "registry+https://github.com/rust-lang/crates.io-index"
332 | checksum = "96fe69244ec2af261bced1d9046a6fee6c8c2a6b0228e59e5ba39bc8ba4ed729"
333 | dependencies = [
334 | "bitflags",
335 | "cassowary",
336 | "crossterm",
337 | "unicode-segmentation",
338 | "unicode-width",
339 | ]
340 |
341 | [[package]]
342 | name = "unicode-segmentation"
343 | version = "1.6.0"
344 | source = "registry+https://github.com/rust-lang/crates.io-index"
345 | checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
346 |
347 | [[package]]
348 | name = "unicode-width"
349 | version = "0.1.8"
350 | source = "registry+https://github.com/rust-lang/crates.io-index"
351 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
352 |
353 | [[package]]
354 | name = "unicode-xid"
355 | version = "0.2.1"
356 | source = "registry+https://github.com/rust-lang/crates.io-index"
357 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
358 |
359 | [[package]]
360 | name = "wasi"
361 | version = "0.11.0+wasi-snapshot-preview1"
362 | source = "registry+https://github.com/rust-lang/crates.io-index"
363 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
364 |
365 | [[package]]
366 | name = "winapi"
367 | version = "0.3.9"
368 | source = "registry+https://github.com/rust-lang/crates.io-index"
369 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
370 | dependencies = [
371 | "winapi-i686-pc-windows-gnu",
372 | "winapi-x86_64-pc-windows-gnu",
373 | ]
374 |
375 | [[package]]
376 | name = "winapi-i686-pc-windows-gnu"
377 | version = "0.4.0"
378 | source = "registry+https://github.com/rust-lang/crates.io-index"
379 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
380 |
381 | [[package]]
382 | name = "winapi-x86_64-pc-windows-gnu"
383 | version = "0.4.0"
384 | source = "registry+https://github.com/rust-lang/crates.io-index"
385 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
386 |
387 | [[package]]
388 | name = "windows-sys"
389 | version = "0.36.1"
390 | source = "registry+https://github.com/rust-lang/crates.io-index"
391 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
392 | dependencies = [
393 | "windows_aarch64_msvc",
394 | "windows_i686_gnu",
395 | "windows_i686_msvc",
396 | "windows_x86_64_gnu",
397 | "windows_x86_64_msvc",
398 | ]
399 |
400 | [[package]]
401 | name = "windows_aarch64_msvc"
402 | version = "0.36.1"
403 | source = "registry+https://github.com/rust-lang/crates.io-index"
404 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
405 |
406 | [[package]]
407 | name = "windows_i686_gnu"
408 | version = "0.36.1"
409 | source = "registry+https://github.com/rust-lang/crates.io-index"
410 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
411 |
412 | [[package]]
413 | name = "windows_i686_msvc"
414 | version = "0.36.1"
415 | source = "registry+https://github.com/rust-lang/crates.io-index"
416 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
417 |
418 | [[package]]
419 | name = "windows_x86_64_gnu"
420 | version = "0.36.1"
421 | source = "registry+https://github.com/rust-lang/crates.io-index"
422 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
423 |
424 | [[package]]
425 | name = "windows_x86_64_msvc"
426 | version = "0.36.1"
427 | source = "registry+https://github.com/rust-lang/crates.io-index"
428 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
429 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "project_manager"
3 | version = "0.1.3"
4 | authors = ["NicoDblc "]
5 | edition = "2018"
6 | exclude = ["Documents/*",".idea/*"]
7 | license-file = "LICENSE"
8 | readme = "Readme.md"
9 | repository = "https://github.com/NicoDblc/TUI_ProjectManager"
10 | keywords = ["todo", "project", "manager", "tui"]
11 | description = "A TUI app that act as a per project task list."
12 |
13 | [dependencies]
14 | serde = { version = "1.0", features = ["derive"] }
15 | serde_json = "1.0.58"
16 | crossterm = "0.23.2"
17 | tui = { version = "0.18.0", default-features = false, features = ['crossterm'] }
18 | dirs = "4.0.0"
19 | smart-default = "0.6.0"
--------------------------------------------------------------------------------
/Documents/Main_window_layout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NicoDblc/TUI_ProjectManager/58f1c83e519b95ad2bf8d3289eb4f5d4591e68da/Documents/Main_window_layout.png
--------------------------------------------------------------------------------
/Documents/Task_window_layout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NicoDblc/TUI_ProjectManager/58f1c83e519b95ad2bf8d3289eb4f5d4591e68da/Documents/Task_window_layout.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | #### TUI Project manager
2 | Acts as a simple todo list.
3 |
4 | Can be provided a path. Will open by default in `~/%username%/.pman`.
5 |
6 | Creates serialized json files with the `.pman` extension
7 |
8 | Only tested on windows not considering WSL.
9 |
10 | ##### Installing
11 | `cargo install project_manager`
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | mod structure;
2 | use crossterm::event::{self, Event as CEvent};
3 | use std::{
4 | sync::mpsc,
5 | thread,
6 | time::{Duration, Instant},
7 | };
8 |
9 | #[macro_use]
10 | extern crate smart_default;
11 |
12 | mod services;
13 | mod ui;
14 | mod utils;
15 |
16 | enum Event {
17 | Input(I),
18 | Tick,
19 | }
20 |
21 | fn main() {
22 | utils::create_working_folder_if_not_exist();
23 | let (tx, rx) = mpsc::channel();
24 | let tick_rate = Duration::from_millis(250);
25 | thread::spawn(move || {
26 | let mut last_tick = Instant::now();
27 | loop {
28 | // poll for tick rate duration, if no events, sent tick event.
29 | let timeout = tick_rate
30 | .checked_sub(last_tick.elapsed())
31 | .unwrap_or_else(|| Duration::from_secs(0));
32 | if event::poll(timeout).unwrap() {
33 | if let CEvent::Key(key) = event::read().unwrap() {
34 | tx.send(Event::Input(key)).unwrap();
35 | }
36 | }
37 | if last_tick.elapsed() >= tick_rate {
38 | tx.send(Event::Tick).unwrap();
39 | last_tick = Instant::now();
40 | }
41 | }
42 | });
43 |
44 | let mut app = structure::Application::new(utils::get_working_folder());
45 | while app.is_running {
46 | app.update();
47 | match rx.recv().unwrap() {
48 | Event::Input(event) => match event.code {
49 | _ => app.handle_inputs(event.code),
50 | },
51 | Event::Tick => {}
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/services/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod project_service;
2 | pub mod task_service;
3 |
4 | pub trait Service {
5 | fn set_working_directory(&mut self, path: std::path::PathBuf);
6 | }
7 |
--------------------------------------------------------------------------------
/src/services/project_service.rs:
--------------------------------------------------------------------------------
1 | use crate::structure::Project;
2 | use crate::ui::{
3 | Completable, DisplayList, Drawable, InputMode, InputReceptor, InputReturn, PopupBinaryChoice,
4 | PopupInputWindow, PopupMessageWindow,
5 | };
6 | use crate::{services, utils};
7 | use crossterm::event::KeyCode;
8 | use std::io::{Error, Stdout};
9 | use std::ops::Add;
10 | use std::path::PathBuf;
11 | use tui::backend::CrosstermBackend;
12 | use tui::layout::{Constraint, Direction, Layout, Rect};
13 | use tui::text::Text;
14 | use tui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap};
15 | use tui::Frame;
16 |
17 | enum ProjectInputType {
18 | ProjectAdd,
19 | ProjectDescriptionEdit,
20 | ProjectNameEdit,
21 | }
22 |
23 | pub struct ProjectManagementService<'a> {
24 | // Everything that is contained in the draw call for the main window
25 | projects_to_display: DisplayList,
26 | selected_project_active_tasks: Vec>,
27 | selected_project_completed_tasks: Vec>,
28 | project_input_popup: PopupInputWindow,
29 | input_mode: InputMode,
30 | input_type: ProjectInputType,
31 | program_work_path: PathBuf,
32 | message_popup: PopupMessageWindow,
33 | delete_project_popup: PopupBinaryChoice,
34 | }
35 |
36 | impl<'a> ProjectManagementService<'a> {
37 | pub fn new(working_path: PathBuf) -> ProjectManagementService<'a> {
38 | let mut project_window = ProjectManagementService {
39 | projects_to_display: DisplayList::from(utils::get_projects_in_path(
40 | working_path.clone(),
41 | )),
42 | selected_project_active_tasks: Vec::new(),
43 | selected_project_completed_tasks: Vec::new(),
44 | project_input_popup: PopupInputWindow::default(),
45 | input_mode: InputMode::CommandMode,
46 | input_type: ProjectInputType::ProjectAdd,
47 | program_work_path: working_path,
48 | message_popup: PopupMessageWindow::default(),
49 | delete_project_popup: PopupBinaryChoice::default(),
50 | };
51 | if project_window.projects_to_display.array.len() > 0 {
52 | project_window.update_project_selection();
53 | }
54 |
55 | project_window
56 | }
57 |
58 | fn update_projects(&mut self, projects: Vec) {
59 | self.projects_to_display = DisplayList::from(projects);
60 | }
61 |
62 | fn reload_projects(&mut self) {
63 | self.update_projects(utils::get_projects_in_path(self.program_work_path.clone()));
64 | }
65 |
66 | fn update_project_selection(&mut self) {
67 | self.selected_project_active_tasks = self.projects_to_display.array
68 | [self.projects_to_display.state.selected().unwrap()]
69 | .active_tasks
70 | .clone()
71 | .into_iter()
72 | .map(|a| ListItem::new(Text::from(a.name)))
73 | .collect();
74 | self.selected_project_completed_tasks = self.projects_to_display.array
75 | [self.projects_to_display.state.selected().unwrap()]
76 | .completed_tasks
77 | .clone()
78 | .into_iter()
79 | .map(|a| ListItem::new(Text::from(a.name)))
80 | .collect();
81 | }
82 |
83 | fn create_popup_with_message(&mut self, message: String) {
84 | self.message_popup = PopupMessageWindow::new(message);
85 | }
86 |
87 | fn add_project_request(&mut self) {
88 | self.input_mode = InputMode::WriteMode;
89 | self.input_type = ProjectInputType::ProjectAdd;
90 | self.project_input_popup = PopupInputWindow::new(String::from("Insert project name"));
91 | }
92 |
93 | fn write_project_to_disk(&self, project_to_write: Project) -> Result<(), Error> {
94 | let mut project_file_path = self.program_work_path.join(project_to_write.name.clone());
95 | project_file_path.set_extension(utils::PROJECT_FILE_EXTENSION);
96 | project_to_write.write_project_full_path(project_file_path)
97 | }
98 |
99 | fn delete_selected_project(&mut self) {
100 | if self.projects_to_display.array.len() > 0 {
101 | let popup_description =
102 | String::from("Delete project: ").add(self.get_selected_project_name().as_str());
103 | self.delete_project_popup = PopupBinaryChoice::new(popup_description);
104 | self.input_mode = InputMode::WriteMode;
105 | }
106 | }
107 |
108 | fn edit_selected_project_name(&mut self) {
109 | if self.projects_to_display.array.len() > 0 {
110 | self.input_mode = InputMode::WriteMode;
111 | self.input_type = ProjectInputType::ProjectNameEdit;
112 | self.project_input_popup = PopupInputWindow::new(String::from("Edit project name"));
113 | self.project_input_popup.set_input_string(
114 | self.projects_to_display.array[self.projects_to_display.state.selected().unwrap()]
115 | .name
116 | .clone(),
117 | );
118 | }
119 | }
120 |
121 | fn edit_selected_project_description(&mut self) {
122 | if self.projects_to_display.array.len() > 0 {
123 | self.input_mode = InputMode::WriteMode;
124 | self.input_type = ProjectInputType::ProjectDescriptionEdit;
125 | self.project_input_popup =
126 | PopupInputWindow::new(String::from("Edit project description"));
127 | self.project_input_popup.set_input_string(
128 | self.projects_to_display.array[self.projects_to_display.state.selected().unwrap()]
129 | .description
130 | .clone(),
131 | );
132 | }
133 | }
134 |
135 | fn get_selected_project_name(&self) -> String {
136 | self.projects_to_display.array[self.projects_to_display.state.selected().unwrap()]
137 | .clone()
138 | .name
139 | }
140 |
141 | pub fn get_selected_project_path_name(&self) -> Option {
142 | match self.projects_to_display.state.selected() {
143 | Some(val) => Option::Some(self.projects_to_display.array[val].clone().name),
144 | None => None,
145 | }
146 | }
147 | }
148 |
149 | impl<'a> InputReceptor for ProjectManagementService<'a> {
150 | fn handle_input_key(&mut self, key_code: KeyCode) {
151 | match self.input_mode {
152 | InputMode::CommandMode => match key_code {
153 | KeyCode::Up => {
154 | self.projects_to_display.previous();
155 | self.update_project_selection();
156 | }
157 | KeyCode::Down => {
158 | self.projects_to_display.next();
159 | self.update_project_selection();
160 | }
161 | KeyCode::Char('a') => {
162 | self.add_project_request();
163 | }
164 | KeyCode::Char('d') => {
165 | self.delete_selected_project();
166 | }
167 | KeyCode::Char('e') => {
168 | self.edit_selected_project_description();
169 | }
170 | KeyCode::Char('n') => {
171 | self.edit_selected_project_name();
172 | }
173 | _ => {}
174 | },
175 | InputMode::WriteMode => {
176 | if self.message_popup.is_active() {
177 | self.message_popup.handle_input_key(key_code);
178 | if self.message_popup.is_completed() {
179 | self.message_popup.set_active(false);
180 | }
181 | } else if self.project_input_popup.is_active() {
182 | self.project_input_popup.handle_input_key(key_code);
183 | } else if self.delete_project_popup.is_active() {
184 | self.delete_project_popup.handle_input_key(key_code);
185 | if self.delete_project_popup.is_completed() {
186 | if self.delete_project_popup.get_choice() {
187 | match utils::delete_project_of_name(
188 | self.get_selected_project_name(),
189 | self.program_work_path.clone(),
190 | ) {
191 | Ok(()) => {}
192 | Err(e) => {
193 | self.create_popup_with_message(e.to_string());
194 | }
195 | };
196 | self.update_projects(utils::get_projects_in_path(
197 | self.program_work_path.clone(),
198 | ));
199 | }
200 | self.delete_project_popup.set_active(false);
201 | }
202 | } else {
203 | self.input_mode = InputMode::CommandMode;
204 | self.handle_input_key(key_code);
205 | }
206 | }
207 | }
208 |
209 | if self.project_input_popup.is_active() & self.project_input_popup.is_completed() {
210 | match self.input_type {
211 | ProjectInputType::ProjectAdd => {
212 | let new_project = Project::new(self.project_input_popup.get_input_data());
213 | match self.write_project_to_disk(new_project) {
214 | Ok(_) => {
215 | self.reload_projects();
216 | self.project_input_popup.set_active(false);
217 | // self.input_mode = InputMode::CommandMode;
218 | }
219 | Err(e) => {
220 | self.create_popup_with_message(
221 | e.to_string()
222 | .add(" With path: ")
223 | .add(self.program_work_path.clone().to_str().unwrap()),
224 | );
225 | self.project_input_popup.reset_completion();
226 | return;
227 | }
228 | };
229 | }
230 | ProjectInputType::ProjectNameEdit => {
231 | let mut project = self.projects_to_display.array
232 | [self.projects_to_display.state.selected().unwrap()]
233 | .clone();
234 | let mut original_path = self.program_work_path.clone().join(project.name);
235 | original_path.set_extension(utils::PROJECT_FILE_EXTENSION);
236 | let mut new_path = self
237 | .program_work_path
238 | .clone()
239 | .join(self.project_input_popup.get_input_data());
240 | new_path.set_extension(utils::PROJECT_FILE_EXTENSION);
241 | project.name = self.project_input_popup.get_input_data();
242 | match std::fs::rename(original_path, new_path) {
243 | Ok(()) => {
244 | match self.write_project_to_disk(project) {
245 | Ok(()) => {}
246 | Err(e) => {
247 | self.create_popup_with_message(e.to_string());
248 | self.project_input_popup.reset_completion();
249 | return;
250 | }
251 | };
252 | self.reload_projects();
253 | self.project_input_popup.set_active(false);
254 | }
255 | Err(e) => {
256 | self.create_popup_with_message(e.to_string());
257 | self.project_input_popup.reset_completion();
258 | return;
259 | }
260 | }
261 | }
262 | ProjectInputType::ProjectDescriptionEdit => {
263 | let mut project = self.projects_to_display.array
264 | [self.projects_to_display.state.selected().unwrap()]
265 | .clone();
266 | let new_description = self.project_input_popup.get_input_data();
267 | project.description = new_description;
268 | match self.write_project_to_disk(project) {
269 | Ok(_) => {
270 | self.reload_projects();
271 | self.project_input_popup.set_active(false);
272 | }
273 | Err(e) => {
274 | self.create_popup_with_message(e.to_string());
275 | self.project_input_popup.reset_completion();
276 | return;
277 | }
278 | };
279 | }
280 | };
281 | }
282 | }
283 |
284 | fn get_controls_description(&self) -> String {
285 | if self.message_popup.is_active() {
286 | return self.message_popup.get_controls_description();
287 | } else if self.delete_project_popup.is_active() {
288 | return self.delete_project_popup.get_controls_description();
289 | } else if self.project_input_popup.is_active() {
290 | return self.project_input_popup.get_controls_description();
291 | }
292 | String::from("Q: Quit | A: Add project | D: Delete project | E: Edit Project Description | N: Edit Project name | Tab: Go to Tasks")
293 | }
294 |
295 | fn get_input_mode(&self) -> InputMode {
296 | match self.input_mode {
297 | InputMode::CommandMode => InputMode::CommandMode,
298 | InputMode::WriteMode => InputMode::WriteMode,
299 | }
300 | }
301 | }
302 |
303 | impl<'a> Drawable for ProjectManagementService<'a> {
304 | fn display(&self, frame: &mut Frame>, layout: Rect) {
305 | let main_layout = Layout::default()
306 | .direction(Direction::Horizontal)
307 | .margin(1)
308 | .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
309 | .split(layout);
310 |
311 | let project_layout = Layout::default()
312 | .direction(Direction::Vertical)
313 | .margin(0)
314 | .constraints([Constraint::Percentage(80), Constraint::Percentage(20)].as_ref())
315 | .split(main_layout[0]);
316 | let block = Block::default().title("Projects").borders(Borders::ALL);
317 | let p_list = List::new::>(
318 | self.projects_to_display
319 | .array
320 | .clone()
321 | .into_iter()
322 | .map(|p| ListItem::new(Text::from(p.name)))
323 | .collect(),
324 | )
325 | .block(block)
326 | .highlight_style(
327 | tui::style::Style::default()
328 | .bg(tui::style::Color::Green)
329 | .add_modifier(tui::style::Modifier::BOLD),
330 | )
331 | .highlight_symbol("-> ");
332 | frame.render_stateful_widget(
333 | p_list,
334 | project_layout[0],
335 | &mut self.projects_to_display.state.clone(),
336 | );
337 |
338 | let block = Block::default()
339 | .title("Project description")
340 | .borders(Borders::ALL);
341 | let p_description = match self.projects_to_display.array.len() > 0 {
342 | true => Paragraph::new(
343 | self.projects_to_display.array[self.projects_to_display.state.selected().unwrap()]
344 | .clone()
345 | .description,
346 | )
347 | .block(block).wrap(Wrap{trim : false}),
348 | false => Paragraph::new("").block(block),
349 | };
350 | frame.render_widget(p_description, project_layout[1]);
351 |
352 | let task_layout = Layout::default()
353 | .direction(Direction::Vertical)
354 | .margin(0)
355 | .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
356 | .split(main_layout[1]);
357 | let block = Block::default().title("Active tasks").borders(Borders::ALL);
358 | let current_task_list = List::new(self.selected_project_active_tasks.clone()).block(block);
359 | frame.render_widget(current_task_list, task_layout[0]);
360 | let block = Block::default()
361 | .title("Completed tasks")
362 | .borders(Borders::ALL);
363 | let completed_tasks_list =
364 | List::new(self.selected_project_completed_tasks.clone()).block(block);
365 | frame.render_widget(completed_tasks_list, task_layout[1]);
366 | if self.project_input_popup.is_active() {
367 | self.project_input_popup.display(frame, layout);
368 | }
369 | if self.delete_project_popup.is_active() {
370 | self.delete_project_popup.display(frame, layout);
371 | }
372 | if self.message_popup.is_active() {
373 | self.message_popup.display(frame, layout);
374 | }
375 | }
376 | }
377 |
378 | impl<'a> services::Service for ProjectManagementService<'a> {
379 | fn set_working_directory(&mut self, path: PathBuf) {
380 | self.program_work_path = path;
381 | }
382 | }
383 |
--------------------------------------------------------------------------------
/src/services/task_service.rs:
--------------------------------------------------------------------------------
1 | use crate::services::task_service::TaskInputChoice::{AddName, EditDescription};
2 | use crate::services::Service;
3 | use crate::structure::{Project, Task, TaskContainer};
4 | use crate::ui::InputMode::CommandMode;
5 | use crate::ui::{Completable, DisplayList, Drawable, InputMode, InputReceptor, InputReturn};
6 | use crate::ui::{PopupInputWindow, PopupMessageWindow};
7 | use crate::utils;
8 | use crossterm::event::KeyCode;
9 | use std::io::Stdout;
10 | use std::ops::Add;
11 | use std::path::PathBuf;
12 | use tui::backend::CrosstermBackend;
13 | use tui::layout::Direction::{Horizontal, Vertical};
14 | use tui::layout::{Constraint, Layout, Rect};
15 | use tui::text::Text;
16 | use tui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap};
17 | use tui::Frame;
18 |
19 | #[derive(SmartDefault)]
20 | enum TaskInputChoice {
21 | #[default]
22 | AddName,
23 | EditDescription,
24 | }
25 |
26 | #[derive(Default)]
27 | pub struct TaskService {
28 | working_path: PathBuf,
29 | selected_project: Project,
30 | active_tasks_list: DisplayList,
31 | completed_tasks_list: DisplayList,
32 | focused_on_active: bool,
33 | input_mode: InputMode,
34 | input_popup: PopupInputWindow,
35 | input_popup_type: TaskInputChoice,
36 | message_popup: PopupMessageWindow,
37 | }
38 |
39 | impl TaskService {
40 | pub fn new(working_path: PathBuf, project_name: String) -> TaskService {
41 | let mut project_path = working_path
42 | .join(String::from('.').add(utils::PROJECT_FILE_EXTENSION))
43 | .with_file_name(project_name);
44 | project_path.set_extension(utils::PROJECT_FILE_EXTENSION);
45 | let loaded_project = match utils::load_project_from_path(project_path.clone()) {
46 | Ok(loaded_project) => loaded_project,
47 | Err(_) => Project::default(),
48 | };
49 | TaskService {
50 | working_path: project_path,
51 | selected_project: loaded_project.clone(),
52 | active_tasks_list: DisplayList::from(loaded_project.active_tasks.clone()),
53 | completed_tasks_list: DisplayList::from(loaded_project.completed_tasks.clone()),
54 | focused_on_active: true,
55 | input_mode: InputMode::CommandMode,
56 | input_popup: PopupInputWindow::default(),
57 | input_popup_type: TaskInputChoice::AddName,
58 | message_popup: PopupMessageWindow::default(),
59 | }
60 | }
61 |
62 | fn add_task_command(&mut self) {
63 | self.input_popup_type = AddName;
64 | self.input_mode = InputMode::WriteMode;
65 | self.input_popup = PopupInputWindow::new(String::from("Enter Task Name"));
66 | }
67 |
68 | fn edit_task_description(&mut self) {
69 | let input_string = match self.focused_on_active {
70 | true => match self.active_tasks_list.state.selected() {
71 | Some(val) => self.active_tasks_list.array[val].description.clone(),
72 | None => String::from(""),
73 | },
74 | false => match self.completed_tasks_list.state.selected() {
75 | Some(val) => self.completed_tasks_list.array[val].description.clone(),
76 | None => String::from(""),
77 | },
78 | };
79 | self.input_popup_type = EditDescription;
80 | self.input_mode = InputMode::WriteMode;
81 | self.input_popup = PopupInputWindow::new(String::from("Edit tasks description"));
82 | self.input_popup.set_input_string(input_string);
83 | }
84 |
85 | fn mark_selected_task_as_completed(&mut self) {
86 | match self.active_tasks_list.state.selected() {
87 | Some(val) => {
88 | self.completed_tasks_list
89 | .array
90 | .push(self.active_tasks_list.array[val].clone());
91 | self.active_tasks_list.array.remove(val);
92 | self.selected_project.active_tasks = self.active_tasks_list.array.clone();
93 | self.selected_project.completed_tasks = self.completed_tasks_list.array.clone();
94 | match self
95 | .selected_project
96 | .write_project_full_path(self.working_path.clone())
97 | {
98 | Ok(_) => {}
99 | Err(e) => self.create_message_popup(e.to_string()),
100 | };
101 | }
102 | None => {}
103 | }
104 | }
105 |
106 | fn mark_selected_task_as_uncompleted(&mut self) {
107 | match self.completed_tasks_list.state.selected() {
108 | Some(val) => {
109 | self.active_tasks_list
110 | .array
111 | .push(self.completed_tasks_list.array[val].clone());
112 | self.completed_tasks_list.array.remove(val);
113 | self.selected_project.completed_tasks = self.completed_tasks_list.array.clone();
114 | self.selected_project.active_tasks = self.active_tasks_list.array.clone();
115 | match self
116 | .selected_project
117 | .write_project_full_path(self.working_path.clone())
118 | {
119 | Ok(_) => {}
120 | Err(e) => self.create_message_popup(e.to_string()),
121 | };
122 | }
123 | None => {}
124 | }
125 | }
126 |
127 | fn create_message_popup(&mut self, message: String) {
128 | self.message_popup = PopupMessageWindow::new(message);
129 | }
130 |
131 | fn update_project(&mut self) {
132 | self.selected_project = match utils::load_project_from_path(self.working_path.clone()) {
133 | Ok(updated_project) => updated_project,
134 | Err(e) => {
135 | self.create_message_popup(e.to_string());
136 | Project::default()
137 | }
138 | };
139 | self.active_tasks_list = DisplayList::from(self.selected_project.active_tasks.clone());
140 | self.completed_tasks_list =
141 | DisplayList::from(self.selected_project.completed_tasks.clone());
142 | }
143 | }
144 |
145 | impl Service for TaskService {
146 | fn set_working_directory(&mut self, path: PathBuf) {
147 | self.working_path = path;
148 | }
149 | }
150 |
151 | impl Drawable for TaskService {
152 | fn display(&self, frame: &mut Frame>, layout: Rect) {
153 | let initial_layout = Layout::default()
154 | .direction(Vertical)
155 | .constraints([Constraint::Percentage(60), Constraint::Percentage(40)])
156 | .split(layout);
157 |
158 | // upper layout
159 | let task_layout = Layout::default()
160 | .direction(Horizontal)
161 | .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
162 | .split(initial_layout[0]);
163 |
164 | let active_task_block = Block::default().borders(Borders::ALL).title("Active Tasks");
165 |
166 | let active_task_display_list = List::new::>(
167 | self.active_tasks_list
168 | .array
169 | .clone()
170 | .into_iter()
171 | .map(|task| ListItem::new(Text::from(utils::wrap(task.name, task_layout[0].width as u32))))
172 | .collect(),
173 | )
174 | .block(active_task_block)
175 | .highlight_style(
176 | tui::style::Style::default()
177 | .bg(tui::style::Color::Green)
178 | .add_modifier(tui::style::Modifier::BOLD),
179 | )
180 | .highlight_symbol("-> ");
181 |
182 | let completed_task_block = Block::default()
183 | .borders(Borders::ALL)
184 | .title("Completed Tasks");
185 | let completed_task_display_list = List::new::>(
186 | self.completed_tasks_list
187 | .array
188 | .clone()
189 | .into_iter()
190 | .map(|task| ListItem::new(Text::from(utils::wrap(task.name, task_layout[1].width as u32))))
191 | .collect(),
192 | )
193 | .block(completed_task_block)
194 | .highlight_style(
195 | tui::style::Style::default()
196 | .bg(tui::style::Color::Green)
197 | .add_modifier(tui::style::Modifier::BOLD),
198 | )
199 | .highlight_symbol("-> ");
200 | if self.focused_on_active {
201 | frame.render_stateful_widget(
202 | active_task_display_list,
203 | task_layout[0],
204 | &mut self.active_tasks_list.state.clone(),
205 | );
206 | frame.render_widget(completed_task_display_list, task_layout[1]);
207 | } else {
208 | frame.render_widget(active_task_display_list, task_layout[0]);
209 | frame.render_stateful_widget(
210 | completed_task_display_list,
211 | task_layout[1],
212 | &mut self.completed_tasks_list.state.clone(),
213 | );
214 | }
215 | // Lower layout
216 | let description_block = Block::default().title("Description").borders(Borders::ALL);
217 | let description: String = match self.focused_on_active {
218 | true => match self.active_tasks_list.state.selected() {
219 | Some(val) => self.active_tasks_list.array[val].description.clone(),
220 | None => String::from("No task selected"),
221 | },
222 | false => match self.completed_tasks_list.state.selected() {
223 | Some(val) => self.completed_tasks_list.array[val].description.clone(),
224 | None => String::from("No task selected"),
225 | },
226 | };
227 | let description_paragraph = Paragraph::new(Text::from(description))
228 | .block(description_block)
229 | .wrap(Wrap { trim: false });
230 | frame.render_widget(description_paragraph, initial_layout[1]);
231 |
232 | // Popups
233 | if self.input_popup.is_active() {
234 | self.input_popup.display(frame, layout);
235 | }
236 | if self.message_popup.is_active() {
237 | self.message_popup.display(frame, layout);
238 | }
239 | }
240 | }
241 |
242 | impl InputReceptor for TaskService {
243 | fn handle_input_key(&mut self, key_code: KeyCode) {
244 | match self.input_mode {
245 | InputMode::CommandMode => match key_code {
246 | KeyCode::Left => {
247 | self.focused_on_active = true;
248 | }
249 | KeyCode::Right => {
250 | self.focused_on_active = false;
251 | }
252 | KeyCode::Char('a') => {
253 | self.add_task_command();
254 | }
255 | KeyCode::Char('c') => {
256 | if self.focused_on_active {
257 | self.mark_selected_task_as_completed();
258 | self.update_project();
259 | }
260 | }
261 | KeyCode::Char('u') => {
262 | if !self.focused_on_active {
263 | self.mark_selected_task_as_uncompleted();
264 | self.update_project();
265 | }
266 | }
267 | KeyCode::Char('e') => {
268 | self.edit_task_description();
269 | }
270 | KeyCode::Up => {
271 | if self.focused_on_active {
272 | self.active_tasks_list.previous();
273 | } else {
274 | self.completed_tasks_list.previous();
275 | }
276 | }
277 | KeyCode::Down => {
278 | if self.focused_on_active {
279 | self.active_tasks_list.next();
280 | } else {
281 | self.completed_tasks_list.next();
282 | }
283 | }
284 | _ => {}
285 | },
286 | InputMode::WriteMode => {
287 | match key_code {
288 | _ => {
289 | if self.message_popup.is_active() {
290 | self.message_popup.handle_input_key(key_code);
291 | if self.message_popup.is_completed() {
292 | self.message_popup.set_active(false);
293 | return;
294 | }
295 | }
296 | self.input_popup.handle_input_key(key_code);
297 | if !self.input_popup.is_active() {
298 | self.input_mode = CommandMode;
299 | return;
300 | }
301 | if self.input_popup.is_completed() {
302 | match self.input_popup_type {
303 | AddName => {
304 | self.selected_project.add_task(
305 | self.input_popup.get_input_data(),
306 | String::from("Description"),
307 | );
308 | match self
309 | .selected_project
310 | .write_project_full_path(self.working_path.clone())
311 | {
312 | Ok(_) => {
313 | self.input_popup.set_active(false);
314 | self.input_mode = CommandMode;
315 | self.update_project();
316 | }
317 | Err(e) => {
318 | self.create_message_popup(e.to_string());
319 | }
320 | };
321 | }
322 | EditDescription => {
323 | let inputted_string = self.input_popup.get_input_data();
324 | match self.focused_on_active {
325 | true => {
326 | match self.active_tasks_list.state.selected() {
327 | Some(val) => {
328 | self.active_tasks_list.array[val].description =
329 | inputted_string;
330 | self.selected_project.active_tasks =
331 | self.active_tasks_list.array.clone();
332 | match self
333 | .selected_project
334 | .write_project_full_path(
335 | self.working_path.clone(),
336 | ) {
337 | Ok(_) => {
338 | self.input_popup.set_active(false);
339 | self.input_mode = CommandMode;
340 | self.update_project();
341 | }
342 | Err(e) => {
343 | self.create_message_popup(
344 | e.to_string(),
345 | );
346 | }
347 | }
348 | }
349 | None => {
350 | self.create_message_popup(String::from(
351 | "Selected task is invalid",
352 | ));
353 | }
354 | };
355 | }
356 | false => {
357 | match self.completed_tasks_list.state.selected() {
358 | Some(val) => {
359 | self.completed_tasks_list.array[val]
360 | .description = inputted_string;
361 | self.selected_project.completed_tasks =
362 | self.completed_tasks_list.array.clone();
363 | match self
364 | .selected_project
365 | .write_project_full_path(
366 | self.working_path.clone(),
367 | ) {
368 | Ok(_) => {
369 | self.input_popup.set_active(false);
370 | self.input_mode = CommandMode;
371 | self.update_project();
372 | }
373 | Err(e) => {
374 | self.create_message_popup(
375 | e.to_string(),
376 | );
377 | }
378 | }
379 | }
380 | None => {
381 | self.create_message_popup(String::from(
382 | "Selected task is invalid",
383 | ));
384 | }
385 | };
386 | }
387 | };
388 | }
389 | }
390 | }
391 | }
392 | };
393 | }
394 | };
395 | }
396 |
397 | fn get_controls_description(&self) -> String {
398 | if self.message_popup.is_active() {
399 | return self.message_popup.get_controls_description();
400 | } else if self.input_popup.is_active() {
401 | return self.input_popup.get_controls_description();
402 | } else {
403 | String::from("Navigate with arrows | C: Mark as completed | U: Mark as incomplete | A: Add task | E: Edit task description | Tab: Back To Projects")
404 | }
405 | }
406 |
407 | fn get_input_mode(&self) -> InputMode {
408 | match self.input_mode {
409 | InputMode::CommandMode => InputMode::CommandMode,
410 | InputMode::WriteMode => InputMode::WriteMode,
411 | }
412 | }
413 | }
414 |
--------------------------------------------------------------------------------
/src/structure.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 | use serde::Serialize;
3 |
4 | use std::io;
5 | use tui::backend::CrosstermBackend;
6 | use tui::Terminal;
7 |
8 | use tui::layout::{Constraint, Direction, Layout};
9 | use tui::text::Text;
10 | use tui::widgets::Paragraph;
11 |
12 | use crate::ui::{Drawable, InputMode, InputReceptor};
13 | use crate::utils;
14 |
15 | use crate::services::project_service::ProjectManagementService;
16 | use crate::services::task_service::TaskService;
17 | use crossterm::event::KeyCode;
18 | use std::ops::Add;
19 | use std::path::PathBuf;
20 |
21 | enum SelectedWindow {
22 | Project,
23 | Task,
24 | }
25 |
26 | pub struct Application<'a> {
27 | terminal: tui::Terminal>,
28 | active_folder_path: std::path::PathBuf,
29 | project_window: ProjectManagementService<'a>,
30 | task_window: TaskService,
31 | pub is_running: bool,
32 | selected_window: SelectedWindow,
33 | }
34 |
35 | impl<'a> Application<'a> {
36 | pub fn new(path: std::path::PathBuf) -> Application<'a> {
37 | let stdout = io::stdout();
38 | let backend = CrosstermBackend::new(stdout);
39 | let mut b_terminal = Terminal::new(backend).unwrap();
40 | b_terminal.clear().unwrap();
41 | let app_project_window = ProjectManagementService::new(path.clone());
42 | Application {
43 | terminal: b_terminal,
44 | active_folder_path: path,
45 | project_window: app_project_window,
46 | task_window: TaskService::default(),
47 | is_running: true,
48 | selected_window: SelectedWindow::Project,
49 | }
50 | }
51 | fn display_main_window(&mut self) {
52 | let text_active_path = Text::from(self.active_folder_path.to_str().unwrap());
53 | let project_window_ref = &mut self.project_window;
54 | self.terminal
55 | .draw(|f| {
56 | let window_layout = Layout::default()
57 | .direction(Direction::Vertical)
58 | .constraints(
59 | [
60 | Constraint::Percentage(5),
61 | Constraint::Percentage(90),
62 | Constraint::Percentage(5),
63 | ]
64 | .as_ref(),
65 | )
66 | .split(f.size());
67 | let current_project_path = Paragraph::new(text_active_path);
68 | f.render_widget(current_project_path, window_layout[0]);
69 | let controls_string =
70 | String::from(project_window_ref.get_controls_description().as_str());
71 | let controls_para = Paragraph::new(Text::from(controls_string));
72 | f.render_widget(controls_para, window_layout[2]);
73 | project_window_ref.display(f, window_layout[1]);
74 | })
75 | .unwrap();
76 | }
77 |
78 | fn display_tasks_window(&mut self) {
79 | let project_name = self
80 | .project_window
81 | .get_selected_project_path_name()
82 | .unwrap();
83 | let mut project_path = self.active_folder_path.clone();
84 | project_path = project_path.join(String::from(".").add(utils::PROJECT_FILE_EXTENSION));
85 | project_path = project_path.with_file_name(project_name);
86 | project_path.set_extension(utils::PROJECT_FILE_EXTENSION);
87 | let text_active_path = Text::from(project_path.to_str().unwrap());
88 | let task_window_ref = &mut self.task_window;
89 | self.terminal
90 | .draw(|f| {
91 | let window_layout = Layout::default()
92 | .direction(Direction::Vertical)
93 | .constraints(
94 | [
95 | Constraint::Percentage(5),
96 | Constraint::Percentage(90),
97 | Constraint::Percentage(5),
98 | ]
99 | .as_ref(),
100 | )
101 | .split(f.size());
102 | let current_project_path = Paragraph::new(text_active_path);
103 | f.render_widget(current_project_path, window_layout[0]);
104 | let controls_string =
105 | String::from(task_window_ref.get_controls_description().as_str());
106 | let controls_para = Paragraph::new(Text::from(controls_string));
107 | f.render_widget(controls_para, window_layout[2]);
108 | task_window_ref.display(f, window_layout[1]);
109 | })
110 | .unwrap();
111 | }
112 |
113 | fn switch_to_window(&mut self, new_window: SelectedWindow) {
114 | self.selected_window = new_window;
115 | match self.selected_window {
116 | SelectedWindow::Project => {
117 | self.project_window =
118 | ProjectManagementService::new(self.active_folder_path.clone());
119 | }
120 | SelectedWindow::Task => {
121 | match self.project_window.get_selected_project_path_name() {
122 | Some(project_name) => {
123 | self.task_window =
124 | TaskService::new(self.active_folder_path.clone(), project_name)
125 | }
126 | None => self.selected_window = SelectedWindow::Project,
127 | };
128 | }
129 | }
130 | }
131 |
132 | pub fn update(&mut self) {
133 | match self.selected_window {
134 | SelectedWindow::Project => {
135 | self.display_main_window();
136 | }
137 | SelectedWindow::Task => {
138 | self.display_tasks_window();
139 | }
140 | }
141 | }
142 | pub fn handle_inputs(&mut self, key_code: KeyCode) {
143 | match self.selected_window {
144 | SelectedWindow::Project => {
145 | self.project_window.handle_input_key(key_code);
146 | match self.project_window.get_input_mode() {
147 | InputMode::CommandMode => match key_code {
148 | KeyCode::Char('q') => self.quit(),
149 | KeyCode::Tab => self.switch_to_window(SelectedWindow::Task),
150 | KeyCode::Enter => self.switch_to_window(SelectedWindow::Task),
151 | _ => {}
152 | },
153 | _ => {}
154 | }
155 | }
156 | SelectedWindow::Task => {
157 | self.task_window.handle_input_key(key_code);
158 | match self.task_window.get_input_mode() {
159 | InputMode::CommandMode => match key_code {
160 | KeyCode::Char('q') => self.quit(),
161 | KeyCode::Tab => self.switch_to_window(SelectedWindow::Project),
162 | _ => {}
163 | },
164 | _ => {}
165 | }
166 | }
167 | }
168 | }
169 | pub fn quit(&mut self) {
170 | self.is_running = false;
171 | match self.terminal.flush() {
172 | Err(e) => println!("Error when exiting program: {}", e),
173 | _ => {}
174 | };
175 | }
176 | }
177 |
178 | trait InformationDisplay {
179 | fn get_description(&self) -> String;
180 | fn get_name(&self) -> String;
181 | }
182 |
183 | trait Completable {
184 | fn complete(&self);
185 | }
186 |
187 | pub trait TaskContainer {
188 | fn add_task(&mut self, task_name: String, task_description: String);
189 | }
190 |
191 | #[derive(Clone, Default, Serialize, Deserialize)]
192 | pub struct Project {
193 | pub name: String,
194 | pub description: String,
195 | pub active_tasks: Vec,
196 | pub completed_tasks: Vec,
197 | }
198 |
199 | impl Project {
200 | pub fn new(project_name: String) -> Project {
201 | Project {
202 | name: project_name,
203 | description: String::from("Sample description"),
204 | active_tasks: vec![],
205 | completed_tasks: vec![],
206 | }
207 | }
208 |
209 | pub fn write_project_full_path(&self, path_for_project: PathBuf) -> Result<(), std::io::Error> {
210 | let project_string = match serde_json::to_string(self) {
211 | Ok(p_string) => p_string,
212 | Err(e) => {
213 | return Result::Err(std::io::Error::from(e));
214 | }
215 | };
216 | match std::fs::write(path_for_project.clone(), project_string) {
217 | Ok(()) => Ok(()),
218 | Err(e) => {
219 | println!("Error: {}", path_for_project.to_str().unwrap());
220 | return Result::Err(e);
221 | }
222 | }
223 | }
224 | }
225 |
226 | impl TaskContainer for Project {
227 | fn add_task(&mut self, task_name: String, task_description: String) {
228 | let task = Task::new(task_name, task_description);
229 | self.active_tasks.push(task);
230 | }
231 | }
232 |
233 | #[derive(Clone, Default, Serialize, Deserialize)]
234 | pub struct Task {
235 | pub name: String,
236 | pub description: String,
237 | pub time_spent: i32,
238 | pub estimate: i32,
239 | pub sub_tasks: Vec,
240 | }
241 |
242 | impl Task {
243 | pub fn new(task_name: String, task_description: String) -> Task {
244 | Task {
245 | name: task_name,
246 | description: task_description,
247 | time_spent: 0,
248 | estimate: 0,
249 | sub_tasks: vec![],
250 | }
251 | }
252 | }
253 |
254 | impl InformationDisplay for Task {
255 | fn get_description(&self) -> String {
256 | self.description.clone()
257 | }
258 |
259 | fn get_name(&self) -> String {
260 | self.description.clone()
261 | }
262 | }
263 |
264 | impl TaskContainer for Task {
265 | fn add_task(&mut self, task_name: String, task_description: String) {
266 | let task = Task {
267 | name: task_name,
268 | description: task_description,
269 | time_spent: 0,
270 | estimate: 0,
271 | sub_tasks: vec![],
272 | };
273 | self.sub_tasks.push(task);
274 | }
275 | }
276 |
--------------------------------------------------------------------------------
/src/ui.rs:
--------------------------------------------------------------------------------
1 | use tui::backend::CrosstermBackend;
2 | use tui::Frame;
3 |
4 | use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
5 | use tui::text::Text;
6 | use tui::widgets::{Block, BorderType, Borders, Clear, ListState, Paragraph, Wrap};
7 |
8 | use crossterm::event::KeyCode;
9 | use std::io::Stdout;
10 | use tui::style::{Color, Style};
11 |
12 | #[derive(Default)]
13 | pub struct DisplayList {
14 | pub(crate) state: ListState,
15 | pub(crate) array: Vec,
16 | }
17 |
18 | #[derive(SmartDefault)]
19 | pub enum InputMode {
20 | #[default]
21 | CommandMode,
22 | WriteMode,
23 | }
24 |
25 | pub trait Drawable {
26 | fn display(&self, frame: &mut Frame>, layout: Rect);
27 | fn centered_rect(&self, percent_x: u16, percent_y: u16, r: Rect) -> Rect {
28 | let popup_layout = Layout::default()
29 | .direction(Direction::Vertical)
30 | .constraints(
31 | [
32 | Constraint::Percentage((100 - percent_y) / 2),
33 | Constraint::Percentage(percent_y),
34 | Constraint::Percentage((100 - percent_y) / 2),
35 | ]
36 | .as_ref(),
37 | )
38 | .split(r);
39 |
40 | Layout::default()
41 | .direction(Direction::Horizontal)
42 | .constraints(
43 | [
44 | Constraint::Percentage((100 - percent_x) / 2),
45 | Constraint::Percentage(percent_x),
46 | Constraint::Percentage((100 - percent_x) / 2),
47 | ]
48 | .as_ref(),
49 | )
50 | .split(popup_layout[1])[1]
51 | }
52 | }
53 |
54 | pub trait InputReceptor {
55 | fn handle_input_key(&mut self, key_code: KeyCode);
56 | fn get_controls_description(&self) -> String;
57 | fn get_input_mode(&self) -> InputMode;
58 | }
59 |
60 | pub trait InputReturn {
61 | fn get_input_data(&self) -> String;
62 | }
63 |
64 | pub trait Completable {
65 | fn is_completed(&self) -> bool;
66 | fn reset_completion(&mut self);
67 | fn is_active(&self) -> bool;
68 | fn set_active(&mut self, new_active: bool);
69 | }
70 |
71 | impl DisplayList {
72 | pub(crate) fn next(&mut self) {
73 | let i = match self.state.selected() {
74 | Some(i) => {
75 | if self.array.len() > 0 {
76 | if i < self.array.len() - 1 {
77 | i + 1
78 | } else {
79 | i
80 | }
81 | } else {
82 | 0
83 | }
84 | }
85 | None => 0,
86 | };
87 | if self.array.len() > 0 {
88 | self.state.select(Some(i));
89 | }
90 | }
91 | pub(crate) fn previous(&mut self) {
92 | let i = match self.state.selected() {
93 | Some(i) => {
94 | if i > 0 {
95 | i - 1
96 | } else {
97 | 0
98 | }
99 | }
100 | None => 0,
101 | };
102 | if self.array.len() > 0 {
103 | self.state.select(Some(i));
104 | }
105 | }
106 |
107 | pub fn from(content: Vec) -> DisplayList {
108 | let content_len = content.len();
109 | let mut dl = DisplayList {
110 | state: Default::default(),
111 | array: content,
112 | };
113 | if content_len > 0 {
114 | dl.state.select(Some(0));
115 | }
116 | dl
117 | }
118 | }
119 |
120 | #[derive(Default)]
121 | pub struct PopupMessageWindow {
122 | description: String,
123 | is_active: bool,
124 | is_done: bool,
125 | }
126 |
127 | // PopupMessageWindow
128 |
129 | impl PopupMessageWindow {
130 | pub fn new(popup_message: String) -> PopupMessageWindow {
131 | PopupMessageWindow {
132 | description: popup_message,
133 | is_active: true,
134 | is_done: false,
135 | }
136 | }
137 | }
138 |
139 | impl Completable for PopupMessageWindow {
140 | fn is_completed(&self) -> bool {
141 | self.is_done
142 | }
143 |
144 | fn reset_completion(&mut self) {
145 | self.is_done = false;
146 | }
147 |
148 | fn is_active(&self) -> bool {
149 | self.is_active
150 | }
151 |
152 | fn set_active(&mut self, new_active: bool) {
153 | self.is_active = new_active;
154 | }
155 | }
156 |
157 | impl Drawable for PopupMessageWindow {
158 | fn display(&self, frame: &mut Frame>, layout: Rect) {
159 | let popup_layout = self.centered_rect(50, 25, layout);
160 | frame.render_widget(Clear, popup_layout);
161 | let popup_block = Block::default().borders(Borders::ALL);
162 | frame.render_widget(popup_block, popup_layout);
163 | let main_popup_layout = Layout::default()
164 | .direction(Direction::Vertical)
165 | .constraints([Constraint::Percentage(80), Constraint::Percentage(20)])
166 | .split(popup_layout);
167 | let block = Block::default().borders(Borders::ALL);
168 | let description_paragraph = Paragraph::new(Text::from(self.description.clone()))
169 | .alignment(Alignment::Center)
170 | .block(block)
171 | .wrap(Wrap { trim: false });
172 | frame.render_widget(description_paragraph, main_popup_layout[0]);
173 | let block = Block::default().borders(Borders::ALL);
174 | let ok_message = Paragraph::new(Text::from("Ok"))
175 | .wrap(Wrap { trim: false })
176 | .alignment(Alignment::Center)
177 | .block(block);
178 | frame.render_widget(ok_message, main_popup_layout[1]);
179 | }
180 | }
181 |
182 | impl InputReceptor for PopupMessageWindow {
183 | fn handle_input_key(&mut self, key_code: KeyCode) {
184 | match key_code {
185 | KeyCode::Enter => {
186 | self.is_done = true;
187 | }
188 | _ => {}
189 | }
190 | }
191 |
192 | fn get_controls_description(&self) -> String {
193 | String::from("Press enter to continue")
194 | }
195 |
196 | fn get_input_mode(&self) -> InputMode {
197 | InputMode::CommandMode
198 | }
199 | }
200 |
201 | // PopupBinaryChoice
202 |
203 | #[derive(Default)]
204 | pub struct PopupBinaryChoice {
205 | choice_message: String,
206 | current_choice: bool,
207 | is_completed: bool,
208 | is_active: bool,
209 | }
210 |
211 | impl PopupBinaryChoice {
212 | pub fn new(message: String) -> PopupBinaryChoice {
213 | PopupBinaryChoice {
214 | choice_message: message,
215 | current_choice: false,
216 | is_completed: false,
217 | is_active: true,
218 | }
219 | }
220 |
221 | pub fn get_choice(&self) -> bool {
222 | self.current_choice
223 | }
224 | }
225 |
226 | impl InputReceptor for PopupBinaryChoice {
227 | fn handle_input_key(&mut self, key_code: KeyCode) {
228 | match key_code {
229 | KeyCode::Left => self.current_choice = true,
230 | KeyCode::Right => self.current_choice = false,
231 | KeyCode::Enter => self.is_completed = true,
232 | _ => {}
233 | }
234 | }
235 |
236 | fn get_controls_description(&self) -> String {
237 | String::from("<-: Go Left | ->: Go Right | Enter: Confirm Selection ")
238 | }
239 |
240 | fn get_input_mode(&self) -> InputMode {
241 | InputMode::CommandMode
242 | }
243 | }
244 |
245 | impl Completable for PopupBinaryChoice {
246 | fn is_completed(&self) -> bool {
247 | self.is_completed
248 | }
249 |
250 | fn reset_completion(&mut self) {
251 | self.is_completed = false;
252 | }
253 |
254 | fn is_active(&self) -> bool {
255 | self.is_active
256 | }
257 |
258 | fn set_active(&mut self, new_active: bool) {
259 | self.is_active = new_active;
260 | }
261 | }
262 |
263 | impl Drawable for PopupBinaryChoice {
264 | fn display(&self, frame: &mut Frame>, layout: Rect) {
265 | let popup_layout = self.centered_rect(50, 20, layout);
266 | frame.render_widget(Clear, popup_layout);
267 | let main_split = Layout::default()
268 | .direction(Direction::Vertical)
269 | .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
270 | .split(popup_layout);
271 | let message_block = Block::default().borders(Borders::ALL);
272 | let message_paragraph = Paragraph::new(Text::from(self.choice_message.clone()))
273 | .alignment(Alignment::Center)
274 | .wrap(Wrap { trim: false })
275 | .block(message_block);
276 | frame.render_widget(message_paragraph, main_split[0]);
277 | let choice_layout = Layout::default()
278 | .direction(Direction::Horizontal)
279 | .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
280 | .split(main_split[1]);
281 | let normal_block = Block::default().borders(Borders::ALL);
282 | let choice_block = Block::default()
283 | .borders(Borders::ALL)
284 | .border_type(BorderType::Thick)
285 | .style(Style::default().fg(Color::Red));
286 | let mut yes_paragraph = Paragraph::new(Text::from("Yes")).alignment(Alignment::Center);
287 | let mut no_paragraph = Paragraph::new(Text::from("No")).alignment(Alignment::Center);
288 | if self.current_choice {
289 | yes_paragraph = yes_paragraph.block(choice_block);
290 | no_paragraph = no_paragraph.block(normal_block);
291 | } else {
292 | no_paragraph = no_paragraph.block(choice_block);
293 | yes_paragraph = yes_paragraph.block(normal_block);
294 | }
295 | frame.render_widget(yes_paragraph, choice_layout[0]);
296 | frame.render_widget(no_paragraph, choice_layout[1]);
297 | }
298 | }
299 |
300 | #[derive(Default)]
301 | pub struct PopupInputWindow {
302 | description: String,
303 | input_string: String,
304 | is_active: bool,
305 | message_input_finished: bool,
306 | }
307 |
308 | impl PopupInputWindow {
309 | pub fn new(popup_description: String) -> PopupInputWindow {
310 | PopupInputWindow {
311 | description: popup_description,
312 | input_string: String::new(),
313 | is_active: true,
314 | message_input_finished: false,
315 | }
316 | }
317 |
318 | pub fn set_input_string(&mut self, new_input_string: String) {
319 | self.input_string = new_input_string;
320 | }
321 | }
322 |
323 | impl Drawable for PopupInputWindow {
324 | fn display(&self, frame: &mut Frame>, layout: Rect) {
325 | let popup_layout = self.centered_rect(50, 25, layout);
326 | frame.render_widget(Clear, popup_layout);
327 | let popup_block = Block::default().borders(Borders::ALL);
328 | frame.render_widget(popup_block, popup_layout);
329 | let main_popup_layout = Layout::default()
330 | .direction(Direction::Vertical)
331 | .constraints([Constraint::Percentage(20), Constraint::Percentage(80)])
332 | .split(popup_layout);
333 | let description_paragraph =
334 | Paragraph::new(Text::from(self.description.clone())).alignment(Alignment::Center);
335 | frame.render_widget(description_paragraph, main_popup_layout[0]);
336 | let input_paragraph = Paragraph::new(Text::from(self.input_string.clone()))
337 | .wrap(Wrap { trim: false })
338 | .alignment(Alignment::Center);
339 | frame.render_widget(input_paragraph, main_popup_layout[1]);
340 | }
341 | }
342 |
343 | impl Completable for PopupInputWindow {
344 | fn is_completed(&self) -> bool {
345 | self.message_input_finished
346 | }
347 |
348 | fn reset_completion(&mut self) {
349 | self.message_input_finished = false;
350 | }
351 |
352 | fn is_active(&self) -> bool {
353 | self.is_active
354 | }
355 |
356 | fn set_active(&mut self, new_active: bool) {
357 | self.is_active = new_active;
358 | }
359 | }
360 |
361 | impl InputReturn for PopupInputWindow {
362 | fn get_input_data(&self) -> String {
363 | self.input_string.clone()
364 | }
365 | }
366 |
367 | impl InputReceptor for PopupInputWindow {
368 | fn handle_input_key(&mut self, key_code: KeyCode) {
369 | match key_code {
370 | KeyCode::Char(char) => {
371 | self.input_string.push(char);
372 | }
373 | KeyCode::Backspace => {
374 | if self.input_string.len() > 0 {
375 | self.input_string.pop();
376 | }
377 | }
378 | KeyCode::Enter => {
379 | self.message_input_finished = true;
380 | }
381 | KeyCode::Esc => {
382 | self.set_active(false);
383 | }
384 | _ => {}
385 | };
386 | }
387 |
388 | fn get_controls_description(&self) -> String {
389 | String::from("esc - Cancel | Enter - Confirm entry")
390 | }
391 |
392 | fn get_input_mode(&self) -> InputMode {
393 | InputMode::CommandMode
394 | }
395 | }
396 |
--------------------------------------------------------------------------------
/src/utils.rs:
--------------------------------------------------------------------------------
1 | pub static PROJECT_FILE_EXTENSION: &str = "pman";
2 | use std::ops::Add;
3 | use std::path::{Path, PathBuf};
4 |
5 | use crate::structure::{Project, Task, TaskContainer};
6 | use std::io::Error;
7 |
8 | pub fn create_working_folder_if_not_exist() {
9 | let working_folder = get_working_folder();
10 | if !working_folder.exists() {
11 | match std::fs::create_dir(working_folder.as_path()) {
12 | Ok(_) => {}
13 | Err(e) => {
14 | panic!("Error occurred while creating the working dir: {}", e);
15 | }
16 | };
17 | }
18 | }
19 |
20 | pub fn wrap(string_to_wrap: String, wrap_at_length: u32) -> String {
21 | let words: Vec<&str> = string_to_wrap.split(" ").collect();
22 | let mut line_length: u32 = 0;
23 | let mut final_string = String::new();
24 | let shortened_wrap = wrap_at_length - 4; // arrow length
25 | for word in words.iter() {
26 | line_length += word.chars().count() as u32;
27 | line_length += 1; // accounting for the space
28 | if line_length >= shortened_wrap {
29 | final_string = final_string.add("\n");
30 | line_length = word.chars().count() as u32;
31 | } else {
32 | final_string = final_string.add(" ");
33 | }
34 | final_string = final_string.add(word);
35 | }
36 | final_string
37 | }
38 |
39 | pub fn get_working_folder() -> PathBuf {
40 | let work_path = match std::env::args().nth(1) {
41 | Some(val) => std::path::PathBuf::from(val),
42 | None => dirs::home_dir().unwrap(),
43 | };
44 | let folder_path = String::from('.').add(PROJECT_FILE_EXTENSION);
45 | work_path.join(Path::new(folder_path.as_str()))
46 | }
47 |
48 | pub fn delete_project_of_name(project_name: String, working_path: PathBuf) -> Result<(), Error> {
49 | let mut path = working_path.join(project_name);
50 | path.set_extension(PROJECT_FILE_EXTENSION);
51 | match std::fs::remove_file(path.as_path()) {
52 | Ok(()) => Ok(()),
53 | Err(e) => Result::Err(e),
54 | }
55 | }
56 |
57 | pub fn load_project_from_path(path: PathBuf) -> Result {
58 | match std::fs::read_to_string(path) {
59 | Ok(project_string) => match serde_json::from_str(project_string.as_str()) {
60 | Ok(deserialized_project) => Result::Ok(deserialized_project),
61 | Err(e) => Result::Err(Error::from(e)),
62 | },
63 | Err(e) => Result::Err(e),
64 | }
65 | }
66 |
67 | pub fn get_projects_in_path(path: PathBuf) -> Vec {
68 | let mut serialized_projects: Vec = vec![];
69 | let folder_result = std::fs::read_dir(path.as_path()).unwrap();
70 | for file in folder_result {
71 | let f = file.unwrap();
72 | if f.file_type().unwrap().is_file() {
73 | match f.path().extension() {
74 | Some(ext) => {
75 | if ext == PROJECT_FILE_EXTENSION {
76 | match match serde_json::from_str(
77 | std::fs::read_to_string(f.path()).unwrap().as_str(),
78 | ) {
79 | Ok(result) => Some(result),
80 | Err(_) => None,
81 | } {
82 | Some(project) => serialized_projects.push(project),
83 | _ => {}
84 | }
85 | }
86 | }
87 | _ => {}
88 | };
89 | }
90 | }
91 | serialized_projects
92 | }
93 |
94 | #[test]
95 | fn create_dummy_project() {
96 | create_dummy_project_with_name(String::from("project1"));
97 | create_dummy_project_with_name(String::from("project2"));
98 | create_dummy_project_with_name(String::from("project3"));
99 | }
100 | #[allow(dead_code)]
101 | fn create_dummy_project_with_name(name: String) {
102 | create_working_folder_if_not_exist();
103 | let mut p = Project::new(name.clone());
104 | p.description = p.name.clone().add(" description");
105 | p.add_task(
106 | name.clone().add("test 1"),
107 | String::from("Sample description"),
108 | );
109 | p.add_task(
110 | name.clone().add("test2 2"),
111 | String::from("Sample description"),
112 | );
113 | p.completed_tasks.push(Task::new(
114 | String::from("a completed task"),
115 | String::from("Sample description"),
116 | ));
117 | let mut project_file_path = get_working_folder().join(p.name.clone());
118 | project_file_path.set_extension(PROJECT_FILE_EXTENSION);
119 | match p.write_project_full_path(project_file_path) {
120 | Ok(_) => {}
121 | Err(e) => panic!("{}", e),
122 | }
123 | }
124 |
--------------------------------------------------------------------------------