From 75367d0a777f5d8d685b5747b39b3a0f5469a490 Mon Sep 17 00:00:00 2001 From: Iurii Kondrakov <55366304+Deezzir@users.noreply.github.com> Date: Mon, 6 Feb 2023 23:15:39 -0500 Subject: [PATCH 1/3] utils module & porper ctrlc handling --- Cargo.toml | 1 + TODO | 2 +- src/main.rs | 56 +++++--------------------------------------- src/mods.rs | 1 + src/mods/utils.rs | 59 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 51 deletions(-) create mode 100644 src/mods/utils.rs diff --git a/Cargo.toml b/Cargo.toml index 96ba963..b9ee840 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,4 @@ edition = "2021" regex = "1.3.9" chrono = "0.4.23" ncurses = { version = "5.101.0", features = ["wide"] } +ctrlc = "3.2.5" diff --git a/TODO b/TODO index a59be67..05177e7 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,4 @@ TODO(): Add subtodos -TODO(): Add proper Ctrl+c handling TODO(): Fix autoresizing TODO(): Finish a Rust TODO app TODO(): Finish a Game of Life with Rust with GUI(SDL?) @@ -11,3 +10,4 @@ DONE(2023-02-04 19:12:32.253412 -05:00): Add editing option to TODO app DONE(2023-02-04 19:17:54.422294 -05:00): Add inserting option to TODO app DONE(2023-02-06 13:47:25.683700140 -05:00): Add buffered stdout write DONE(2023-02-06 16:00:00.876958453 -05:00): Add autoresizing of the TODO app UI(tree?) +DONE(2023-02-06 23:09:36.666041 -05:00): Add proper Ctrl+c handling diff --git a/src/main.rs b/src/main.rs index ef3df41..efdb3ab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,13 +2,12 @@ extern crate regex; mod mods; use chrono::Local; -use std::env::args; -use std::process::exit; - use ncurses::*; +use std::sync::atomic::Ordering; use mods::todo::*; use mods::ui::*; +use mods::utils::*; const SELECTED_PAIR: i16 = 1; const UNSELECTED_PAIR: i16 = 2; @@ -40,6 +39,7 @@ Author: Iurii Kondrakov const FILE_PATH: &str = "TODO"; fn main() { + set_sig_handler(); let file_path: String = get_args(); ncurses_init(); @@ -49,7 +49,7 @@ fn main() { let mut ui = UI::new(); app.parse(&file_path); - loop { + while !QUIT.load(Ordering::SeqCst) { erase(); let date = Local::now().format("%Y %a %b %d %H:%M:%S"); let mut w = 0; @@ -193,7 +193,8 @@ fn main() { } } else { match key as u8 as char { - '\n' | '\u{1b}' => { + '\n' => { + //'\u{1b}' editing = app.finish_edit(); editing_cursor = if editing { editing_cursor } else { 0 }; } @@ -206,48 +207,3 @@ fn main() { endwin(); app.save(&file_path).unwrap(); } - -fn ncurses_init() { - setlocale(LcCategory::all, ""); - // Init ncurses - initscr(); - raw(); - // Allow for extended keyboard (like F1). - noecho(); - keypad(stdscr(), true); - curs_set(CURSOR_VISIBILITY::CURSOR_INVISIBLE); - // Set timeout and esc delay - timeout(16); - set_escdelay(0); - // Set colors - use_default_colors(); - start_color(); - init_pair(HIGHLIGHT_PAIR, COLOR_BLACK, COLOR_GREEN); - init_pair(SELECTED_PAIR, COLOR_BLACK, COLOR_CYAN); - init_pair(UNSELECTED_PAIR, COLOR_BLACK, COLOR_WHITE); - init_pair(UI_PAIR, COLOR_WHITE, COLOR_BLACK); -} - -fn get_args() -> String { - let mut args = args().skip(1); - - match args.next() { - Some(arg) => match arg.as_str() { - "-f" | "--file" => args.next().unwrap_or_else(|| { - eprintln!("[ERROR]: No file given for '{arg}'."); - eprintln!("{USAGE}"); - exit(1); - }), - "-h" | "--help" => { - println!("{HELP}\n{USAGE}"); - exit(0); - } - _ => { - eprintln!("[ERROR]: Unknown argument: '{arg}'."); - eprintln!("{USAGE}"); - exit(1); - } - }, - None => FILE_PATH.to_string(), - } -} diff --git a/src/mods.rs b/src/mods.rs index 8826314..8560ff2 100644 --- a/src/mods.rs +++ b/src/mods.rs @@ -1,2 +1,3 @@ pub mod todo; pub mod ui; +pub mod utils; diff --git a/src/mods/utils.rs b/src/mods/utils.rs new file mode 100644 index 0000000..b4c678d --- /dev/null +++ b/src/mods/utils.rs @@ -0,0 +1,59 @@ +use std::env::args; +use std::process::exit; +use std::sync::atomic::{AtomicBool, Ordering}; + +use ncurses::*; + +use crate::{FILE_PATH, HELP, HIGHLIGHT_PAIR, SELECTED_PAIR, UI_PAIR, UNSELECTED_PAIR, USAGE}; + +pub static QUIT: AtomicBool = AtomicBool::new(false); + +pub fn set_sig_handler() { + ctrlc::set_handler(move || QUIT.store(true, Ordering::SeqCst)) + .expect("[ERROR]: Failed to set Ctrl-C handler"); +} + +pub fn ncurses_init() { + setlocale(LcCategory::all, ""); + // Init ncurses + initscr(); + raw(); + // Allow for extended keyboard (like F1). + noecho(); + keypad(stdscr(), true); + curs_set(CURSOR_VISIBILITY::CURSOR_INVISIBLE); + // Set timeout and esc delay + timeout(16); + set_escdelay(0); + // Set colors + use_default_colors(); + start_color(); + init_pair(HIGHLIGHT_PAIR, COLOR_BLACK, COLOR_GREEN); + init_pair(SELECTED_PAIR, COLOR_BLACK, COLOR_CYAN); + init_pair(UNSELECTED_PAIR, COLOR_BLACK, COLOR_WHITE); + init_pair(UI_PAIR, COLOR_WHITE, COLOR_BLACK); +} + +pub fn get_args() -> String { + let mut args = args().skip(1); + + match args.next() { + Some(arg) => match arg.as_str() { + "-f" | "--file" => args.next().unwrap_or_else(|| { + eprintln!("[ERROR]: No file given for '{arg}'."); + eprintln!("{USAGE}"); + exit(1); + }), + "-h" | "--help" => { + println!("{HELP}\n{USAGE}"); + exit(0); + } + _ => { + eprintln!("[ERROR]: Unknown argument: '{arg}'."); + eprintln!("{USAGE}"); + exit(1); + } + }, + None => FILE_PATH.to_string(), + } +} From 41337ccf31bd8bd7dd9c6931003c29d8c7cc4e36 Mon Sep 17 00:00:00 2001 From: Iurii Kondrakov <55366304+Deezzir@users.noreply.github.com> Date: Mon, 6 Feb 2023 23:38:48 -0500 Subject: [PATCH 2/3] more styling --- src/main.rs | 23 ++++++++++++++++------- src/mods/ui.rs | 12 +++++++++--- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/main.rs b/src/main.rs index efdb3ab..20fad58 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,15 +69,20 @@ fn main() { app.get_dones_n() ), UI_PAIR, + Some(A_BOLD()), + ); + ui.label_styled( + &format!("[MESSAGE]: {}", app.get_message()), + UI_PAIR, + Some(A_BOLD()), ); - ui.label_styled(&format!("[MESSAGE]: {}", app.get_message()), UI_PAIR); } ui.end_layout(); ui.begin_layout(LayoutKind::Vert); { - ui.label_styled(&format!("[DATE]: {date}"), UI_PAIR); - ui.label_styled(&format!("[FILE]: {file_path}"), UI_PAIR); + ui.label_styled(&format!("[DATE]: {date}"), UI_PAIR, Some(A_BOLD())); + ui.label_styled(&format!("[FILE]: {file_path}"), UI_PAIR, Some(A_BOLD())); } ui.end_layout(); } @@ -91,9 +96,9 @@ fn main() { ui.begin_layout(LayoutKind::Vert); { if app.is_in_todo_panel() { - ui.label_styled("[TODO]", HIGHLIGHT_PAIR); + ui.label_styled("[TODO]", HIGHLIGHT_PAIR, None); } else { - ui.label_styled(" TODO ", UNSELECTED_PAIR); + ui.label_styled(" TODO ", UNSELECTED_PAIR, None); } ui.hl(); for todo in app.get_todos() { @@ -109,12 +114,14 @@ fn main() { ui.label_styled( &format!("- [ ] {}", todo.get_text()), SELECTED_PAIR, + None, ); } } else { ui.label_styled( &format!("- [ ] {}", todo.get_text()), UNSELECTED_PAIR, + None, ); } } else { @@ -127,9 +134,9 @@ fn main() { ui.begin_layout(LayoutKind::Vert); { if app.is_in_done_panel() { - ui.label_styled("[DONE]", HIGHLIGHT_PAIR); + ui.label_styled("[DONE]", HIGHLIGHT_PAIR, None); } else { - ui.label_styled(" DONE ", UNSELECTED_PAIR); + ui.label_styled(" DONE ", UNSELECTED_PAIR, None); } ui.hl(); for done in app.get_dones() { @@ -145,12 +152,14 @@ fn main() { ui.label_styled( &format!("- [X] ({}) {}", done.get_date(), done.get_text()), SELECTED_PAIR, + None, ); } } else { ui.label_styled( &format!("- [X]|{}| {}", done.get_date(), done.get_text()), UNSELECTED_PAIR, + None, ); } } else { diff --git a/src/mods/ui.rs b/src/mods/ui.rs index 56ff1d5..1c318a4 100644 --- a/src/mods/ui.rs +++ b/src/mods/ui.rs @@ -217,10 +217,16 @@ impl UI { .add_widget(Vec2::new(text.len() as i32, 1)); } - pub fn label_styled(&mut self, text: &str, pair: i16) { - attr_on(COLOR_PAIR(pair)); + pub fn label_styled(&mut self, text: &str, color_pair: i16, style: Option) { + if let Some(s) = style { + attr_on(s); + } + attr_on(COLOR_PAIR(color_pair)); self.label(text); - attr_off(COLOR_PAIR(pair)); + attr_off(COLOR_PAIR(color_pair)); + if let Some(s) = style { + attr_off(s); + } } pub fn edit_label(&mut self, text: &String, cur: usize, prefix: String) { From 8335614763141661804d94dc99a2020ccdc9ec4d Mon Sep 17 00:00:00 2001 From: Iurii Kondrakov <55366304+Deezzir@users.noreply.github.com> Date: Tue, 7 Feb 2023 14:43:45 -0500 Subject: [PATCH 3/3] improvements --- src/main.rs | 87 ++++++++++++++++++++++++++++++------------------ src/mods/todo.rs | 34 ++++++++++++++----- src/mods/ui.rs | 4 ++- 3 files changed, 83 insertions(+), 42 deletions(-) diff --git a/src/main.rs b/src/main.rs index 20fad58..49faed8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,12 +38,18 @@ Author: Iurii Kondrakov const FILE_PATH: &str = "TODO"; +#[derive(PartialEq)] +enum Mode { + Edit, + Normal, +} + fn main() { set_sig_handler(); let file_path: String = get_args(); ncurses_init(); - let mut editing: bool = false; + let mut mode: Mode = Mode::Normal; let mut editing_cursor: usize = 0; let mut app: TodoApp = TodoApp::new(); let mut ui = UI::new(); @@ -104,7 +110,7 @@ fn main() { for todo in app.get_todos() { if app.is_cur_todo(todo) { if app.is_in_todo_panel() { - if editing { + if mode == Mode::Edit { ui.edit_label( todo.get_text(), editing_cursor, @@ -142,7 +148,7 @@ fn main() { for done in app.get_dones() { if app.is_cur_done(done) { if app.is_in_done_panel() { - if editing { + if mode == Mode::Edit { ui.edit_label( done.get_text(), editing_cursor, @@ -176,38 +182,55 @@ fn main() { refresh(); let key = getch(); if key != ERR { - if !editing { - app.clear_message(); - match char::from_u32(key as u32).unwrap() { - 'k' | '\u{103}' => app.go_up(), - 'j' | '\u{102}' => app.go_down(), - 'g' => app.go_top(), - 'G' => app.go_bottom(), - 'K' => app.drag_up(), - 'J' => app.drag_down(), - '\n' => app.transfer_item(), - 'd' => app.delete_item(), - 'i' => { - editing_cursor = app.insert_item(); - editing = editing_cursor == 0; - } - 'r' => { - editing_cursor = app.edit_item(); - editing = editing_cursor > 0; + match mode { + Mode::Normal => { + app.clear_message(); + match char::from_u32(key as u32).unwrap() { + 'k' | '\u{103}' => app.go_up(), + 'j' | '\u{102}' => app.go_down(), + 'g' => app.go_top(), + 'G' => app.go_bottom(), + 'K' => app.drag_up(), + 'J' => app.drag_down(), + '\n' => app.transfer_item(), + 'd' => app.delete_item(), + 'i' => { + if let Some(cur) = app.insert_item() { + editing_cursor = cur; + mode = Mode::Edit; + } + } + 'a' => { + if let Some(cur) = app.append_item() { + editing_cursor = cur; + mode = Mode::Edit; + } + } + 'r' => { + if let Some(cur) = app.edit_item() { + editing_cursor = cur; + mode = Mode::Edit; + } + } + 'u' => app.undo(), + '\t' => app.toggle_panel(), + 'q' | '\u{3}' => break, + _ => {} } - 'u' => app.undo(), - '\t' => app.toggle_panel(), - 'q' | '\u{3}' => break, - _ => {} } - } else { - match key as u8 as char { - '\n' => { - //'\u{1b}' - editing = app.finish_edit(); - editing_cursor = if editing { editing_cursor } else { 0 }; + Mode::Edit => { + match key as u8 as char { + '\n' => { + //'\u{1b}' + mode = if app.finish_edit() { + editing_cursor = 0; + Mode::Normal + } else { + Mode::Edit + }; + } + _ => app.edit_item_with(&mut editing_cursor, key), } - _ => app.edit_item_with(&mut editing_cursor, key), } } } diff --git a/src/mods/todo.rs b/src/mods/todo.rs index 7de1779..b6a2868 100644 --- a/src/mods/todo.rs +++ b/src/mods/todo.rs @@ -64,11 +64,18 @@ impl Operation { pub struct Item { text: String, date: DateTime, + sub_items: Vec, + dones_num: usize, } impl Item { fn new(text: String, date: DateTime) -> Self { - Self { text, date } + Self { + text, + date, + sub_items: Vec::new(), + dones_num: 0, + } } pub fn get_text(&self) -> &String { @@ -528,13 +535,13 @@ impl TodoApp { } } - pub fn insert_item(&mut self) -> usize { + pub fn insert_item(&mut self) -> Option { assert!( !self.is_in_edit(), "insert_item() called in already running edit mode." ); - let mut editing_cursor = 1; + let mut editing_cursor = None; match self.panel { Panel::Todo => { @@ -542,7 +549,7 @@ impl TodoApp { self.operation_stack .push(Operation::new(Action::Insert, Panel::Todo)); self.todos.insert(); - editing_cursor = 0; + editing_cursor = Some(0); self.operation_stack .push(Operation::new(Action::InEdit, self.panel)); @@ -556,7 +563,15 @@ impl TodoApp { editing_cursor } - pub fn edit_item(&mut self) -> usize { + pub fn append_item(&mut self) -> Option { + assert!( + !self.is_in_edit(), + "append_item() called in already running edit mode." + ); + todo!("append_item() not implemented yet."); + } + + pub fn edit_item(&mut self) -> Option { assert!( !self.is_in_edit(), "edit_item() called in already running edit mode." @@ -586,9 +601,10 @@ impl TodoApp { self.operation_stack .push(Operation::new(Action::InEdit, self.panel)); self.message.push_str("Editing current item."); - } - editing_cursor + return Some(editing_cursor); + } + None } pub fn edit_item_with(&mut self, cur: &mut usize, key: i32) { @@ -615,7 +631,7 @@ impl TodoApp { Panel::Todo => { if self.todos.get_item().text.is_empty() { self.message.push_str("TODO item can't be empty."); - return true; + return false; } } Panel::Done => { @@ -626,7 +642,7 @@ impl TodoApp { } self.operation_stack.pop(); - false + true } fn is_in_edit(&self) -> bool { diff --git a/src/mods/ui.rs b/src/mods/ui.rs index 1c318a4..55b8be2 100644 --- a/src/mods/ui.rs +++ b/src/mods/ui.rs @@ -235,11 +235,13 @@ impl UI { .last_mut() .expect("Tried to render edit mode outside of any layout"); let pos = layout.borrow().available_pos(); + let space_fill = + " ".repeat((layout.borrow().max_size.x as usize).saturating_sub(text.len())); // Buffer { mv(pos.y, pos.x); - addstr(&format!("{prefix}{text}")); + addstr(&format!("{prefix}{text}{space_fill}")); layout .borrow_mut() .add_widget(Vec2::new(text.len() as i32, 1));