diff --git a/Cargo.lock b/Cargo.lock index 86940b5..ef98ccc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,6 +266,15 @@ dependencies = [ "cipher", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -540,6 +549,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "once_cell" version = "1.19.0" @@ -630,6 +645,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -910,6 +931,7 @@ dependencies = [ "rusqlite", "sha3", "thiserror", + "time", "unicode-width", "wl-clipboard-rs", ] @@ -946,6 +968,25 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + [[package]] name = "tree_magic_mini" version = "3.1.5" diff --git a/Cargo.toml b/Cargo.toml index 3f03dde..578cfb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,5 +18,6 @@ ratatui = "0.26.2" rusqlite = { version = "0.31.0", features = ["bundled"] } sha3 = "0.10.8" thiserror = "1.0.60" +time = "0.3.36" unicode-width = "0.1.13" wl-clipboard-rs = "0.9.0" diff --git a/export.sh b/script/export-pass.sh similarity index 86% rename from export.sh rename to script/export-pass.sh index 66769a4..17c602b 100644 --- a/export.sh +++ b/script/export-pass.sh @@ -1,4 +1,6 @@ #!/usr/bin/env bash +# pass: the standard unix password manager +# https://www.passwordstore.org/ # export passwords to external file shopt -s nullglob globstar diff --git a/src/tui/app.rs b/src/tui/app.rs index 88168c9..d86cdbd 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -1,8 +1,4 @@ -use std::{ - path::Path, - rc::Rc, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::{path::Path, rc::Rc}; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use ratatui::{ @@ -23,7 +19,7 @@ use super::{ module::{ draw_account_table, draw_confirm, draw_form, draw_view, AccountTable, Confirm, Form, View, }, - util::copy_content, + util::{copy_content, current_millis}, }; enum AppMode { @@ -67,8 +63,10 @@ impl App { form: Form::default(), to_del: Confirm::default().with_content("To delete the selected account?"), }; - let accounts = app.account_repo.all()?; - app.account_table.load(accounts); + + app.change_mode(AppMode::Table); + app.load_accounts()?; + // app.change_mode(mode); Ok(app) } @@ -83,8 +81,6 @@ impl App { return Ok(()); } match self.mode { - // AppMode::Login => self.login_on_key_event(key_event)?, - // AppMode::Reg => self.reg_on_key_envent(key_event)?, AppMode::Table => self.table_on_key_envent(key_event)?, AppMode::View => self.view_on_key_event(key_event)?, AppMode::Add => self.add_on_key_event(key_event)?, @@ -149,6 +145,7 @@ impl App { } } KeyCode::Char('a') => { + self.form.reset(); self.change_mode(AppMode::Add); } KeyCode::Char('e') => { @@ -203,7 +200,7 @@ impl App { .. } => { if self.form.validate() { - let current = current_timestamp(); + let current = current_millis() as usize; let acc = Account { id: 0, url: self.form.url().to_string(), @@ -220,7 +217,7 @@ impl App { }; self.pwd_repo.add(&pwd)?; - self.account_table.load(self.account_repo.all()?); + self.load_accounts()?; self.account_table.select_by_aid(aid); self.form.reset(); @@ -249,7 +246,7 @@ impl App { .. } => { if self.form.validate() { - let current = current_timestamp(); + let current = current_millis() as usize; if let Some(selected) = self.account_table.selected() { let aid = selected.id; let acc = Account { @@ -268,7 +265,8 @@ impl App { }; self.pwd_repo.add(&pwd)?; - self.account_table.load(self.account_repo.all()?); + // self.account_table.load(self.account_repo.all()?); + self.load_accounts()?; self.account_table.select_by_aid(aid); self.form.reset(); @@ -312,13 +310,23 @@ impl App { fn change_mode(&mut self, mode: AppMode) { self.mode = mode; match self.mode { - // AppMode::Login => self.help_text = "login".to_owned(), - // AppMode::Reg => self.help_text = "reg".to_owned(), - AppMode::Table => self.help_text = "table".to_owned(), - AppMode::View => self.help_text = "view".to_owned(), - AppMode::Add => self.help_text = "add".to_owned(), - AppMode::Del => self.help_text = "delete".to_owned(), - AppMode::Edit => self.help_text = "edit".to_owned(), + AppMode::Table => { + self.help_text = + "/: filter, a: add, e: edit, d: delete, c: copy password, j: next, k: prev, l/enter: view, ctrl-c: quit" + .to_owned() + } + AppMode::View => { + self.help_text = "View Account - c: copy, x: show/hide passwords, q/esc: back".to_owned() + } + AppMode::Add => { + self.help_text = + "Eidt Account - ctrl-x: show/hide passwords, ctrl-v: paste, esc: back".to_owned() + } + AppMode::Del => self.help_text = "Delete Account - esc: back".to_owned(), + AppMode::Edit => { + self.help_text = + "Edit Account - ctrl-x: show/hide passwords, ctrl-v: paste, esc: back".to_owned() + } } } @@ -331,17 +339,23 @@ impl App { } Ok(()) } + + fn load_accounts(&mut self) -> TecResult<()> { + let accounts = self.account_repo.all()?; + self.account_table.load(accounts); + Ok(()) + } } pub fn draw_app(f: &mut Frame, app: &mut App) { - let [main_area, search_area, help_area] = Layout::vertical([ + let [main_area, help_area] = Layout::vertical([ Constraint::Min(3), - Constraint::Length(1), + // Constraint::Length(1), Constraint::Length(1), ]) .areas(f.size()); - draw_account_table(f, &mut app.account_table, main_area, search_area); + draw_account_table(f, &mut app.account_table, main_area); let pop_rect = centered_rect(60, 30, main_area); match app.mode { @@ -388,10 +402,10 @@ fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { .split(popup_layout[1])[1] // Return the middle chunk } -fn current_timestamp() -> usize { - let now = SystemTime::now(); - let since_the_epoch = now - .duration_since(UNIX_EPOCH) - .expect("Clock may have gone backwards"); - since_the_epoch.as_millis() as usize -} +// fn current_timestamp() -> usize { +// let now = SystemTime::now(); +// let since_the_epoch = now +// .duration_since(UNIX_EPOCH) +// .expect("Clock may have gone backwards"); +// since_the_epoch.as_millis() as usize +// } diff --git a/src/tui/auth.rs b/src/tui/auth.rs index fda6fc1..c56faeb 100644 --- a/src/tui/auth.rs +++ b/src/tui/auth.rs @@ -25,7 +25,7 @@ pub struct Auth { impl Auth { pub fn build(config_path: impl AsRef) -> TecResult { - let key_path = config_path.as_ref().join("tecpass.key"); + let key_path = config_path.as_ref().join("tecpass.pub.key"); let key_store = KeyStore::new(key_path); let mode = { @@ -36,7 +36,6 @@ impl Auth { } }; let mut auth = Self { - // account_repo: AccountRepo::new(conn), key_store, mode: AuthMode::Login, quiting: false, @@ -47,7 +46,7 @@ impl Auth { .with_min(8) .with_max(32) .with_active(), - reg: ConfirmPassword::default(), + reg: ConfirmPassword::default().with_title("Register password"), }; auth.change_mode(mode); Ok(auth) @@ -86,7 +85,6 @@ impl Auth { let res = self.key_store.get_key(pwd.as_bytes()); if let Ok(key) = res { self.key = Some(key); - // self.change_mode(AppMode::Table); } else { self.login.set_msg("wrong password"); } @@ -115,9 +113,7 @@ pub fn draw_auth(f: &mut Frame, auth: &mut Auth) { let area = f.size(); match auth.mode { - AuthMode::Login => { - draw_input(f, &auth.login, area); - } + AuthMode::Login => draw_input(f, &auth.login, area), AuthMode::Reg => draw_confirm_password(f, &auth.reg, area), } } diff --git a/src/tui/module/account_table.rs b/src/tui/module/account_table.rs index 4fdfa7e..a741dc1 100644 --- a/src/tui/module/account_table.rs +++ b/src/tui/module/account_table.rs @@ -1,6 +1,6 @@ use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; use ratatui::{ - layout::{Constraint, Rect}, + layout::{Constraint, Layout, Rect}, style::{Style, Stylize}, widgets::{Block, Row, Table, TableState}, Frame, @@ -206,13 +206,13 @@ impl AccountTable { // Ok(()) // } - pub fn help_text(&self) -> String { - if !self.query.is_active() { - "Down, j: next; Up, k: prev; /: filter".to_owned() - } else { - "Esc, Enter: quit filter".to_owned() - } - } + // pub fn help_text(&self) -> String { + // if !self.query.is_active() { + // "Down, j: next; Up, k: prev; /: filter".to_owned() + // } else { + // "Esc, Enter: quit filter".to_owned() + // } + // } pub(crate) fn selected(&mut self) -> Option<&Account> { // println!("xxxxxxxxxxxxxxxxxx {:?}", self.state.selected()); @@ -229,12 +229,9 @@ impl AccountTable { } } -pub fn draw_account_table( - f: &mut Frame, - at: &mut AccountTable, - main_area: Rect, - search_area: Rect, -) { +pub fn draw_account_table(f: &mut Frame, at: &mut AccountTable, area: Rect) { + let [main_area, search_area] = + Layout::vertical([Constraint::Min(3), Constraint::Length(1)]).areas(area); let rows: Vec = at .items .iter() diff --git a/src/tui/module/confirm_password.rs b/src/tui/module/confirm_password.rs index 25019d1..6350f70 100644 --- a/src/tui/module/confirm_password.rs +++ b/src/tui/module/confirm_password.rs @@ -1,8 +1,9 @@ +use std::u16; + use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; use ratatui::{ layout::{Constraint, Layout, Rect}, - style::{Style, Stylize}, - text::Line, + widgets::{Block, Borders}, Frame, }; @@ -11,14 +12,16 @@ use crate::common::TecResult; use super::{draw_input, Input}; pub struct ConfirmPassword { + title: String, password: Input, confirm: Input, - msg: String, + // msg: String, } impl Default for ConfirmPassword { fn default() -> Self { Self { + title: "".to_string(), password: Input::default() .with_mask() .with_label("password: ") @@ -30,12 +33,16 @@ impl Default for ConfirmPassword { .with_label("confirm: ") .with_min(8) .with_max(32), - msg: "".to_owned(), } } } impl ConfirmPassword { + pub fn with_title(mut self, title: impl Into) -> Self { + self.title = title.into(); + self + } + pub fn on_key_event(&mut self, key_event: KeyEvent) -> TecResult<()> { // press `Tab` to switch input if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Tab { @@ -55,9 +62,9 @@ impl ConfirmPassword { } }; curr.on_key_event(key_event)?; - if !self.msg.is_empty() { - self.msg = "".to_owned(); - } + // if !self.msg.is_empty() { + // self.msg = "".to_owned(); + // } Ok(()) } @@ -66,14 +73,15 @@ impl ConfirmPassword { return false; } if self.password.content().eq(self.confirm.content()) { - if !self.msg.is_empty() { - self.msg = "".to_owned(); - } + // if !self.msg.is_empty() { + // self.msg = "".to_owned(); + // } return true; } else { - if self.msg.is_empty() { - self.msg = "Not match".to_owned(); - } + self.confirm.set_msg("Not match"); + // if self.msg.is_empty() { + // self.msg = "Not match".to_owned(); + // } return false; } } @@ -84,13 +92,19 @@ impl ConfirmPassword { } pub fn draw_confirm_password(f: &mut Frame, state: &ConfirmPassword, area: Rect) { - let [pwd_area, confirm_area, msg_area] = Layout::vertical([ - Constraint::Max(2), - Constraint::Max(2), - Constraint::Length(1), + let block = Block::new() + .title(state.title.as_str()) + .borders(Borders::ALL); + let inner_area = block.inner(area); + f.render_widget(block, area); + + let pwd_height = state.password.width().div_ceil(area.width as usize) as u16; + let confirm_height = state.confirm.width().div_ceil(area.width as usize) as u16; + let [pwd_area, confirm_area] = Layout::vertical([ + Constraint::Length(pwd_height), + Constraint::Length(confirm_height), ]) - .areas(area); + .areas(inner_area); draw_input(f, &state.password, pwd_area); draw_input(f, &state.confirm, confirm_area); - f.render_widget(Line::styled(&state.msg[..], Style::new().red()), msg_area); } diff --git a/src/tui/module/form.rs b/src/tui/module/form.rs index 79b9d17..5e9bcce 100644 --- a/src/tui/module/form.rs +++ b/src/tui/module/form.rs @@ -1,5 +1,4 @@ use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; -use crypto_common::rand_core::block; use ratatui::{ layout::{Constraint, Layout, Rect}, style::{Color, Style}, @@ -27,7 +26,6 @@ pub struct Form { username: Input, password: Input, confirm: Input, - msg: String, } impl Default for Form { @@ -49,7 +47,6 @@ impl Default for Form { .with_label("confirm: ") .with_min(8) .with_max(32), - msg: "".to_owned(), } } } @@ -179,11 +176,12 @@ pub fn draw_form(f: &mut Frame, form: &Form, area: Rect) { let inner_area = block.inner(area); f.render_widget(block, area); + let line_width = inner_area.width; let [url_area, username_area, password_area, confirm_area] = Layout::vertical([ - Constraint::Length(2), - Constraint::Length(1), - Constraint::Length(1), - Constraint::Length(1), + Constraint::Length(form.url.width().div_ceil(line_width as usize) as u16), + Constraint::Length(form.username.width().div_ceil(line_width as usize) as u16), + Constraint::Length(form.password.width().div_ceil(line_width as usize) as u16), + Constraint::Length(form.confirm.width().div_ceil(line_width as usize) as u16), ]) .areas(inner_area); diff --git a/src/tui/module/input.rs b/src/tui/module/input.rs index ce209b1..586d323 100644 --- a/src/tui/module/input.rs +++ b/src/tui/module/input.rs @@ -81,9 +81,9 @@ impl Input { &self.content } - pub fn label(&self) -> &str { - &self.label - } + // pub fn label(&self) -> &str { + // &self.label + // } pub fn reset(&mut self) { self.content = "".into(); @@ -211,6 +211,10 @@ impl Input { pub(crate) fn set_content(&mut self, content: impl Into) { self.content = content.into(); } + + pub fn width(&self) -> usize { + self.content.width() + self.label.width() + } } pub fn draw_input(f: &mut Frame, state: &Input, area: Rect) { diff --git a/src/tui/module/view.rs b/src/tui/module/view.rs index a3b1843..fa1e151 100644 --- a/src/tui/module/view.rs +++ b/src/tui/module/view.rs @@ -1,18 +1,16 @@ -use std::time; - use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; use ratatui::{ layout::Rect, style::{Color, Modifier, Style, Stylize}, text::{Line, Span}, - widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap}, + widgets::{Block, Borders, Clear, List, ListItem, ListState}, Frame, }; use crate::{ common::TecResult, model::{Account, Pwd}, - tui::util::copy_content, + tui::util::{copy_content, millis2string}, }; pub struct View { @@ -45,12 +43,12 @@ impl View { self.pwds = Some(pwds); } - pub fn show_pwds(&mut self) { - self.is_masked = false; - } - pub fn hide_pwds(&mut self) { - self.is_masked = true; - } + // pub fn show_pwds(&mut self) { + // self.is_masked = false; + // } + // pub fn hide_pwds(&mut self) { + // self.is_masked = true; + // } pub(crate) fn on_key_event(&mut self, key_event: KeyEvent) -> TecResult<()> { self.symbol = "❯".into(); @@ -99,6 +97,8 @@ impl View { } } } + Some(3) => copy_content(millis2string(account.created as u64).as_bytes())?, + Some(4) => copy_content(millis2string(account.changed as u64).as_bytes())?, _ => {} } if self.state.selected().is_some() { @@ -132,14 +132,15 @@ pub fn draw_view(f: &mut Frame, view: &mut View, area: Rect) { Span::raw(&account.username), ]) .into(); + let created_item: ListItem = Line::from(vec![ Span::styled("created: ", Style::default().bold()), - Span::raw(account.created.to_string()), + Span::raw(millis2string(account.created as u64)), ]) .into(); let changed_item: ListItem = Line::from(vec![ Span::styled("changed: ", Style::default().bold()), - Span::raw(account.changed.to_string()), + Span::raw(millis2string(account.changed as u64)), ]) .into(); diff --git a/src/tui/util/mod.rs b/src/tui/util/mod.rs index a726279..1a6351f 100644 --- a/src/tui/util/mod.rs +++ b/src/tui/util/mod.rs @@ -1,3 +1,5 @@ mod clipboard; +mod time; pub use clipboard::{copy_content, get_pasted_content}; +pub use time::{current_millis, millis2string}; diff --git a/src/tui/util/time.rs b/src/tui/util/time.rs new file mode 100644 index 0000000..42de9a5 --- /dev/null +++ b/src/tui/util/time.rs @@ -0,0 +1,36 @@ +use std::{ + ops::Add, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use time::OffsetDateTime; + +pub fn time2millis(t: SystemTime) -> u128 { + let since_the_epoch = t + .duration_since(UNIX_EPOCH) + .expect("Clock may have gone blockwards"); + since_the_epoch.as_millis() +} + +pub fn millis2time(m: u64) -> SystemTime { + let dur = Duration::from_millis(m); + UNIX_EPOCH.add(dur) +} + +pub fn time2string(st: SystemTime) -> String { + let t: OffsetDateTime = st.into(); + format!("{t}") +} + +pub fn millis2string(m: u64) -> String { + let dur = Duration::from_millis(m); + let t: OffsetDateTime = UNIX_EPOCH.add(dur).into(); + format!("{t}") +} + +pub fn current_millis() -> u128 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Clock may have gone backwards") + .as_millis() +}