1pub mod config;
43pub mod display_options;
44pub mod storage;
45mod users;
46
47use chrono::{DateTime, Local};
48use serde::{Deserialize, Serialize};
49use std::{
50 collections::HashMap,
51 fmt::Display,
52 io,
53 path::{Path, PathBuf},
54};
55pub use users::{User, Users};
57
58pub const VERSION: &str = env!("CARGO_PKG_VERSION");
59pub const DB_NAME: &str = "db.json";
60
61#[derive(Debug)]
62pub enum Error {
63 DbError(storage::Error),
64 SomeError,
65 NotFound,
66 Io(io::Error),
67}
68
69impl Display for Error {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 match self {
72 Error::DbError(e) => write!(f, "db error: {}", e),
73 Error::SomeError => write!(f, "some error. please retry."),
74 Error::NotFound => write!(f, "not found"),
75 Error::Io(error) => write!(f, "io error: {}", error),
76 }
77 }
78}
79
80impl Error {
81 pub fn is_file_is_zero(&self) -> bool {
82 match self {
83 Error::DbError(e) => matches!(e, storage::Error::FileIsZero),
84 _ => false,
85 }
86 }
87}
88
89#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
90pub struct Message {
91 message: String,
92 show: bool,
93 created_at: DateTime<Local>,
94 author: User,
95}
96impl Message {
97 pub fn new<S: AsRef<str>, U: Into<User>>(message: S, show: bool, author: U) -> Self {
98 Self {
99 message: message.as_ref().to_string(),
100 show,
101 created_at: Local::now(),
102 author: author.into(),
103 }
104 }
105 fn hide(&mut self) {
106 self.show = false;
107 }
108 fn show(&mut self) {
109 self.show = true
110 }
111}
112
113impl Display for Message {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 if self.show {
116 writeln!(f, "{}\n\t{}\n", self.created_at.to_rfc2822(), self.message)
117 } else {
118 write!(f, "")
119 }
120 }
121}
122
123trait ManageMessage {
124 fn new() -> Self
125 where
126 Self: Sized;
127 fn hide_message_by_id(&mut self, id: u64);
128 fn show_message_by_id(&mut self, id: u64);
129 fn add_message_to(&mut self, new_message: Message);
130 fn rm_message(&mut self, id: u64);
131}
132
133#[derive(Debug, Default, Serialize, Deserialize, Clone)]
134pub struct Messages(Vec<Message>);
135
136impl Messages {
137 fn count_messages(&self) -> i32 {
138 self.0.len() as i32
139 }
140}
141
142impl ManageMessage for Messages {
143 fn new() -> Self
144 where
145 Self: Sized,
146 {
147 Self(Vec::new())
148 }
149
150 fn hide_message_by_id(&mut self, id: u64) {
151 if let Some(f) = self.0.get_mut(id as usize) {
152 f.hide()
153 }
154 }
155
156 fn show_message_by_id(&mut self, id: u64) {
157 if let Some(f) = self.0.get_mut(id as usize) {
158 f.show()
159 }
160 }
161
162 fn add_message_to(&mut self, new_message: Message) {
163 self.0.push(new_message);
165 }
166
167 fn rm_message(&mut self, id: u64) {
169 self.0.remove(id as usize);
170 }
171}
172
173impl Display for Messages {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 write!(
181 f,
182 "{}",
183 self.0
184 .iter()
185 .map(|f| { format!("{}", f) })
186 .collect::<String>()
187 )
188 }
189}
190
191pub trait StatusT {
192 fn is_opened(&self) -> bool;
193 fn to_emoji(&self) -> String;
194}
195
196#[derive(Debug, Serialize, Deserialize, Clone)]
197enum Status {
198 Open,
199 Closed(Closed),
200}
201
202impl StatusT for Status {
203 fn is_opened(&self) -> bool {
204 matches!(self, Status::Open)
205 }
206
207 fn to_emoji(&self) -> String {
208 match self {
209 Status::Open => "🟢",
210 Status::Closed(closed) => match closed {
211 Closed::Resolved => "✅🔴",
212 Closed::UnResolved => "❌🔴",
213 },
214 }
215 .to_string()
216 }
217}
218
219impl Display for Status {
220 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221 match self {
222 Status::Open => write!(f, "Open"),
223 Status::Closed(closed) => match closed {
224 Closed::Resolved => write!(f, "Resolved Closed"),
225 Closed::UnResolved => write!(f, "UnResolved Closed"),
226 },
227 }
228 }
229}
230
231#[derive(Debug, Serialize, Deserialize, Clone)]
232enum Closed {
233 Resolved,
234 UnResolved,
235}
236
237trait IssueTrait {
238 fn update(&mut self);
239 fn comment<S: AsRef<str>, U: Into<User>>(&mut self, msg_str: S, author: U);
240 fn rename<S: AsRef<str>>(&mut self, new_title: S);
241 fn edit_due(&mut self, new_due: DateTime<Local>);
242 fn rm_comment(&mut self, id: u64);
243 fn hide_message(&mut self, id: u64);
244 fn show_message(&mut self, id: u64);
245 fn search<S: AsRef<str>>(&self, target_title: S) -> Option<u64>;
246 fn search_list<S: AsRef<str>>(&self, target_title: S) -> Option<Vec<u64>>;
247 fn close(&mut self, is_resolved: bool);
248 fn open(&mut self);
249 fn is_opened(&self) -> bool;
250 fn get_message(&self) -> &Messages;
251 fn change_author<U: Into<User>>(&mut self, new_author: U);
252 fn assign_user<U: Into<User>>(&mut self, user: U);
253}
254
255#[derive(Debug, Serialize, Deserialize, Clone)]
256pub struct Issue {
257 name: String,
258 messages: Messages,
259 status: Status,
260 created_at: DateTime<Local>,
261 updated_at: DateTime<Local>,
262 due_date: Option<DateTime<Local>>,
263 author: User,
264 assigned_member: Option<Users>,
265}
266
267impl Issue {
268 pub fn new<S: AsRef<str>, U: Into<User>>(name: S, author: U) -> Self {
269 Self {
270 name: name.as_ref().to_string(),
271 messages: Messages::new(),
272 status: Status::Open,
273 created_at: Local::now(),
274 updated_at: Local::now(),
275 due_date: None,
276 author: author.into(),
277 assigned_member: None,
278 }
279 }
280 pub fn count_message(&self) -> i32 {
281 self.messages.count_messages()
282 }
283}
284
285impl IssueTrait for Issue {
286 fn update(&mut self) {
287 self.updated_at = Local::now();
288 }
289
290 fn comment<S: AsRef<str>, U: Into<User>>(&mut self, msg_str: S, author: U) {
291 self.update();
292 self.messages
293 .add_message_to(Message::new(msg_str, true, author));
294 }
295
296 fn rename<S: AsRef<str>>(&mut self, new_title: S) {
297 self.update();
298 self.name = new_title.as_ref().to_string();
299 }
300
301 fn edit_due(&mut self, new_due: DateTime<Local>) {
303 self.update();
304 self.due_date = Some(new_due);
305 }
306
307 fn rm_comment(&mut self, id: u64) {
310 self.update();
311 self.messages.rm_message(id);
312 }
313
314 fn hide_message(&mut self, id: u64) {
315 self.update();
316 self.messages.hide_message_by_id(id);
317 }
318
319 fn show_message(&mut self, id: u64) {
320 self.update();
321 self.messages.show_message_by_id(id);
322 }
323
324 fn search<S: AsRef<str>>(&self, target_title: S) -> Option<u64> {
326 self.messages
327 .0
328 .iter()
329 .position(|f| f.message == *target_title.as_ref())
330 .map(|f| f as u64)
331 }
332
333 fn search_list<S: AsRef<str>>(&self, target_title: S) -> Option<Vec<u64>> {
334 let a = self
335 .messages
336 .0
337 .iter()
338 .enumerate()
339 .filter(|f| f.1.message == *target_title.as_ref())
340 .map(|f| f.0 as u64)
341 .collect::<Vec<u64>>();
342 if a.is_empty() { Some(a) } else { None }
343 }
344
345 fn close(&mut self, is_resolved: bool) {
346 self.update();
347 if is_resolved {
348 self.status = Status::Closed(Closed::Resolved);
349 } else {
350 self.status = Status::Closed(Closed::UnResolved);
351 }
352 }
353
354 fn open(&mut self) {
355 self.update();
356 self.status = Status::Open;
357 }
358
359 fn is_opened(&self) -> bool {
360 self.status.is_opened()
361 }
362
363 fn get_message(&self) -> &Messages {
364 &self.messages
365 }
366
367 fn change_author<U: Into<User>>(&mut self, new_author: U) {
368 self.update();
369 self.author = new_author.into();
370 }
371
372 fn assign_user<U: Into<User>>(&mut self, user: U) {
373 self.update();
374 match &mut self.assigned_member {
375 Some(v) => v.add_user(user),
376 None => {
377 self.assigned_member = Some({
378 let mut n = Users::new();
379 n.add_user(user);
380 n
381 })
382 }
383 }
384 }
385}
386
387impl Display for Issue {
388 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
389 let issue_info = format!(
390 "issue: {}\n status:\t{}\n created:\t{}\n update_at:\t{}\n due date:\t{}",
391 self.name,
392 self.status,
393 self.created_at.to_rfc2822(),
394 self.updated_at.to_rfc2822(),
395 self.due_date
396 .map(|f| f.to_rfc2822())
397 .unwrap_or("None".to_string())
398 );
399 write!(f, "{}\n\n{}\n", issue_info, self.messages)
400 }
401}
402
403impl Issue {
404 pub fn fmt_only_open(&self) -> String {
405 if self.is_opened() {
406 let issue_info = format!(
407 "issue:\t\t{}\nstatus:\t\t{}\ncreated:\t{}\nupdate_at:\t{}\ndue date:\t{}",
408 self.name,
409 self.status,
410 self.created_at.to_rfc2822(),
411 self.updated_at.to_rfc2822(),
412 self.due_date
413 .map(|f| f.to_rfc2822())
414 .unwrap_or("None".to_string())
415 );
416 format!("{}\n\n{}\n", issue_info, self.messages)
417 } else {
418 String::new()
419 }
420 }
421 pub fn fmt_only_prop(&self) -> String {
422 let issue_info = format!(
423 "issue: {}\n status:\t{}\n created:\t{}\n update_at:\t{}\n due date:\t{}",
424 self.name,
425 self.status,
426 self.created_at.to_rfc2822(),
427 self.updated_at.to_rfc2822(),
428 self.due_date
429 .map(|f| f.to_rfc2822())
430 .unwrap_or("None".to_string())
431 );
432 format!("{}\n\n", issue_info)
433 }
434
435 pub fn fmt_only_open_prop(&self) -> String {
436 if self.is_opened() {
437 let issue_info = format!(
438 "issue:\t\t{}\nstatus:\t\t{}\ncreated:\t{}\nupdate_at:\t{}\ndue date:\t{}",
439 self.name,
440 self.status,
441 self.created_at.to_rfc2822(),
442 self.updated_at.to_rfc2822(),
443 self.due_date
444 .map(|f| f.to_rfc2822())
445 .unwrap_or("None".to_string())
446 );
447 format!("{}\n\n", issue_info)
448 } else {
449 String::new()
450 }
451 }
452}
453
454pub trait ProjectManager {
455 fn new<S: AsRef<str>, P: AsRef<Path>>(name: S, project_path: P) -> Self;
456 fn open<P: AsRef<Path>>(project_path: P) -> Result<Self, Error>
457 where
458 Self: Sized;
459 fn open_or_create<P: AsRef<Path>, S: AsRef<str>>(
460 project_path: P,
461 name: S,
462 ) -> Result<Self, Error>
464 where
465 Self: Sized;
466 fn save(&self) -> Result<(), Error>;
467 fn update(&mut self);
468}
469
470pub fn storage_path<P: AsRef<Path>>(path: &P) -> PathBuf {
472 path.as_ref().join(".local_issue")
473}
474
475#[derive(Debug, Serialize, Deserialize, Clone)]
476pub struct Project {
477 project_name: String,
478 issues: HashMap<u64, Issue>,
479 current_id: u64,
480 created_at: DateTime<Local>,
481 updated_at: DateTime<Local>,
482 project_path: PathBuf,
483 storage_path: PathBuf,
484 db_path: PathBuf,
485 users: Users,
486}
487
488impl ProjectManager for Project {
489 fn new<S: AsRef<str>, P: AsRef<Path>>(name: S, project_path: P) -> Self {
492 let storage_path = storage_path(&project_path);
493 let db_path = storage_path.join(DB_NAME);
494
495 Self {
496 project_name: name.as_ref().to_string(),
497 issues: HashMap::new(),
498 current_id: 0,
499 created_at: Local::now(),
500 updated_at: Local::now(),
501 project_path: project_path.as_ref().to_path_buf(),
502 storage_path,
503 db_path,
504 users: Users::new(),
505 }
506 }
507
508 fn open<P: AsRef<Path>>(project_path: P) -> Result<Self, Error>
510 where
511 Self: Sized,
512 {
513 let storage_path = storage_path(&project_path);
514 let db_path = storage_path.join(DB_NAME);
515
516 storage::load::<Project, _>(db_path, false).map_err(Error::DbError)
517 }
518
519 fn open_or_create<P: AsRef<Path>, S: AsRef<str>>(
521 project_path: P,
522 name: S,
523 ) -> Result<Self, Error>
524 where
525 Self: Sized,
526 {
527 let storage_path = storage_path(&project_path);
528 let db_path = storage_path.join(DB_NAME);
529
530 storage::load::<Project, _>(db_path, true)
531 .or_else(|e| {
533 if e.is_file_is_zero() {
534 Ok(Project::new(name, project_path))
535 } else {
536 Err(e)
537 }
538 })
539 .map_err(Error::DbError)
540 }
541
542 fn save(&self) -> Result<(), Error> {
544 storage::save(self, &self.db_path).map_err(Error::DbError)
545 }
546
547 fn update(&mut self) {
548 self.updated_at = Local::now();
549 }
550}
551
552impl Project {
554 pub fn add_user<U: Into<User>>(&mut self, new_user: U) {
555 self.update();
556 self.users.add_user(new_user);
557 }
558 pub fn list_users(&self) -> Vec<String> {
559 self.users.users_list()
560 }
561
562 pub fn change_author_of_issue<U: Into<User>>(&mut self, new_author: U, issue_id: u64) {
563 if let Some(f) = self.issues.get_mut(&issue_id) {
564 f.change_author(new_author);
565 self.update();
566 }
567 }
568
569 pub fn assign_new_user_to_issue<U: Into<User>>(&mut self, new_user: U, issue_id: u64) {
570 if let Some(f) = self.issues.get_mut(&issue_id) {
571 f.assign_user(new_user);
572 self.update();
573 }
574 }
575}
576
577pub trait SearchIssue {
578 fn search_issue<S: AsRef<str>>(&self, target_title: S) -> Option<u64>;
579 fn search_issues<S: AsRef<str>>(&self, target_title: S) -> Option<Vec<u64>>;
580}
581
582impl SearchIssue for Project {
583 fn search_issue<S: AsRef<str>>(&self, target_title: S) -> Option<u64> {
584 self.issues
585 .iter()
586 .find(|f| f.1.name == *target_title.as_ref())
587 .map(|f| *f.0)
588 }
589
590 fn search_issues<S: AsRef<str>>(&self, target_title: S) -> Option<Vec<u64>> {
591 let a = self
592 .issues
593 .iter()
594 .filter(|f| f.1.name == *target_title.as_ref())
595 .map(|f| *f.0)
596 .collect::<Vec<u64>>();
597 if a.is_empty() { Some(a) } else { None }
598 }
599}
600
601impl Project {
602 pub fn count_issues(&self) -> i32 {
603 self.issues.iter().len() as i32
604 }
605 pub fn count_comments(&self, issue_id: u64) -> Option<i32> {
606 self.issues.get(&issue_id).map(|f| f.count_message())
607 }
608}
609
610impl Project {
612 pub fn rename<T: AsRef<str>>(&mut self, title: T) {
613 self.update();
614 self.project_name = title.as_ref().to_string();
615 }
616}
617
618impl Project {
619 pub fn add_issue<T: AsRef<str>, U: Into<User>>(&mut self, new_name: T, author: U) {
620 self.update();
621 self.current_id += 1;
622 self.issues
623 .insert(self.current_id, Issue::new(new_name, author));
624 }
625
626 pub fn rename_issue<T: AsRef<str>>(&mut self, issue_id: u64, new_name: T) {
627 self.update();
628 if let Some(f) = self.issues.get_mut(&issue_id) {
629 f.rename(new_name);
630 }
631 }
632
633 pub fn edit_issue_due(&mut self, id: u64, due: DateTime<Local>) {
634 self.update();
635 if let Some(f) = self.issues.get_mut(&id) {
636 f.edit_due(due);
637 }
638 }
639
640 pub fn to_open_issue(&mut self, issue_id: u64) {
641 if let Some(f) = self.issues.get_mut(&issue_id) {
642 f.open();
643 self.update();
644 }
645 }
646
647 pub fn to_close_issue(&mut self, issue_id: u64, is_resolved: bool) {
648 if let Some(f) = self.issues.get_mut(&issue_id) {
649 f.close(is_resolved);
650 self.update();
651 }
652 }
653
654 pub fn is_opened_issue(&self, issue_id: u64) -> Option<bool> {
655 self.issues.get(&issue_id).map(|f| f.is_opened())
656 }
657
658 pub fn exist(&self, issue_id: u64) -> bool {
659 self.is_opened_issue(issue_id).is_some()
661 }
662
663 pub fn remove_issue(&mut self, issue_id: u64) {
664 self.update();
665 self.issues.remove(&issue_id);
666 }
667}
668
669pub trait SearchComment {
670 fn search_comment_position<T: AsRef<str>>(&self, issue_id: u64, target_title: T)
671 -> Option<u64>;
672 fn search_comments<T: AsRef<str>>(&self, issue_id: u64, target_title: T) -> Option<&Messages>;
673 fn search_comments_positions<T: AsRef<str>>(
674 &self,
675 issue_id: u64,
676 target_title: T,
677 ) -> Option<Vec<u64>>;
678}
679
680impl SearchComment for Project {
681 fn search_comment_position<T: AsRef<str>>(
682 &self,
683 issue_id: u64,
684 target_title: T,
685 ) -> Option<u64> {
686 self.issues
687 .get(&issue_id)
688 .and_then(|f| f.search(target_title))
689 }
690
691 fn search_comments<T: AsRef<str>>(&self, issue_id: u64, target_title: T) -> Option<&Messages> {
692 self.search_comment_position(issue_id, target_title)
693 .and_then(|f| self.issues.get(&f).map(|f| f.get_message()))
694 }
695
696 fn search_comments_positions<T: AsRef<str>>(
697 &self,
698 issue_id: u64,
699 target_title: T,
700 ) -> Option<Vec<u64>> {
701 self.issues
702 .get(&issue_id)
703 .and_then(|f| f.search_list(target_title))
704 }
705}
706
707impl Project {
709 pub fn add_comment<T: AsRef<str>, U: Into<User>>(
710 &mut self,
711 issue_id: u64,
712 comment: T,
713 author: U,
714 ) {
715 if let Some(f) = self.issues.get_mut(&issue_id) {
716 f.comment(comment, author);
717 self.update();
718 }
719 }
720
721 pub fn rm_comment(&mut self, issue_id: u64, comment_id: u64) {
724 if let Some(f) = self.issues.get_mut(&issue_id) {
725 f.rm_comment(comment_id);
726 self.update();
727 }
728 }
729
730 pub fn set_comment_as_visible(&mut self, comment_id: u64, issue_id: u64) {
731 if let Some(f) = self.issues.get_mut(&issue_id) {
732 f.show_message(comment_id);
733 self.update();
734 }
735 }
736
737 pub fn set_comment_as_hidden(&mut self, comment_id: u64, issue_id: u64) {
738 if let Some(f) = self.issues.get_mut(&issue_id) {
739 f.hide_message(comment_id);
740 self.update();
741 }
742 }
743}
744
745impl Display for Project {
746 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
747 let sub_prop = format!(
748 " created at: {}\n updated at: {}\n Path: {}",
749 self.created_at.to_rfc2822(),
750 self.updated_at.to_rfc2822(),
751 self.project_path.to_string_lossy(),
752 );
753 let iss = self
754 .issues
755 .iter()
756 .map(|f| (f.0, f.1))
757 .collect::<Vec<(&u64, &Issue)>>()
758 .iter()
759 .map(|f| format!("#{}: {}", f.0, f.1))
760 .collect::<String>();
761
762 let r = format!("Project: {}\n{}\n\n{}", self.project_name, sub_prop, iss);
763 write!(f, "{}", r)
764 }
765}
766
767impl Project {
768 pub fn fmt_only_prop(&self) -> String {
769 self.issues.iter().map(|f| f.1.fmt_only_prop()).collect()
770 }
771 pub fn fmt_only_open(&self) -> String {
772 self.issues.iter().map(|f| f.1.fmt_only_open()).collect()
773 }
774 pub fn fmt_only_open_prop(&self) -> String {
775 self.issues
776 .iter()
777 .map(|f| f.1.fmt_only_open_prop())
778 .collect()
779 }
780}
781
782pub fn db_path(work_path: PathBuf) -> PathBuf {
783 storage_path(&work_path).join(DB_NAME)
784 }