Skip to content

Commit 48f99c2

Browse files
committed
Add changes for v0.1.4
* Update README * Prevent panic when clipboard creation fails (fixes #6) * Replace unwrap calls * Refactor password table scrollbar * Fix clippy warning * Improve help instructions
1 parent 2819bad commit 48f99c2

9 files changed

+93
-77
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "passepartui"
33
description = "A TUI for pass"
4-
version = "0.1.3"
4+
version = "0.1.4"
55
edition = "2021"
66
authors = ["Karl Felix Schewe"]
77
readme = "README.md"

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ A TUI for pass
99
I started this project as a way to practice programming in Rust while reading the [Rust Book](https://doc.rust-lang.org/stable/book/title-page.html).
1010
Therefore this project is still in an alpha version, however user interaction is mostly finished.
1111

12-
`passepartui` relies for all decryption operations on [pass](https://www.passwordstore.org/), one-time passwords (OTP) are handled by [`pass-otp`](https://github.com/tadfisher/pass-otp).
12+
`passepartui` relies for all decryption operations on [pass](https://www.passwordstore.org/), one-time passwords (OTP) are handled by [pass-otp](https://github.com/tadfisher/pass-otp).
1313
Currently no functionality for manipulating the password store, e.g. adding or deleting a password, is implemented. For those operations use `pass` directly from your terminal (refer to `man pass`).
1414
More on the current state of development can be found below.
1515

@@ -36,7 +36,7 @@ The name `passepartui` is a combination of "passepartout", French for "master ke
3636
`passepartui` can be found on crates.io [here](https://crates.io/crates/passepartui).
3737

3838
```sh
39-
cargo install passepartui
39+
cargo install passepartui --locked
4040
```
4141

4242
Type `passepartui` to run the app (provided that `~/.cargo/bin` has been added to `$PATH`).

src/app.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ impl<'a> App<'a> {
4444
}
4545

4646
fn handle_events(&mut self) -> Result<()> {
47-
if event::poll(self.tick_rate).unwrap() {
47+
if event::poll(self.tick_rate)? {
4848
if let Ok(terminal_event) = event::read() {
4949
match terminal_event {
5050
TerminalEvent::Key(event) if event.kind == KeyEventKind::Press => {

src/components/help_popup.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ impl<'a> Widget for &mut HelpPopup<'a> {
6262
let text = vec![
6363
Line::from("Navigation".fg(theme.debug).italic()),
6464
Line::default(),
65-
Line::from("(↓), (↑), (j), (k) Select list entry".fg(theme.standard_fg)),
66-
Line::from("(⇣), (⇡), (b), (f) Skip list entries".fg(theme.standard_fg)),
67-
Line::from("(⇱), (g) Select first entry in list".fg(theme.standard_fg)),
68-
Line::from("(⇲), (G) Select last entry in list".fg(theme.standard_fg)),
65+
Line::from("(↓) (↑) (j) (k) Select list entry".fg(theme.standard_fg)),
66+
Line::from("(⇣) (⇡) (f) (b) Skip list entries".fg(theme.standard_fg)),
67+
Line::from("(⇱) (g) Select first entry in list".fg(theme.standard_fg)),
68+
Line::from("(⇲) (G) Select last entry in list".fg(theme.standard_fg)),
6969
Line::default(),
7070
Line::from("(←) (h) (→) (l) (↵) Switch between view modes".fg(theme.standard_fg)),
7171
Line::from("for password list, preview and secrets".fg(theme.standard_fg)),
@@ -78,7 +78,7 @@ impl<'a> Widget for &mut HelpPopup<'a> {
7878
Line::default(),
7979
Line::from("Search".fg(theme.debug).italic()),
8080
Line::default(),
81-
Line::from("(Esc), (↵) Suspend search".fg(theme.standard_fg)),
81+
Line::from("(Esc) (↵) Suspend search".fg(theme.standard_fg)),
8282
Line::from(
8383
"Pressing (Esc) a second time clears the search and resets the filter."
8484
.fg(theme.standard_fg),

src/components/password_details.rs

+25-21
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use ratatui::{
22
buffer::Buffer,
33
crossterm::event::MouseEvent,
44
layout::{Alignment, Constraint, Direction, Flex, Layout, Rect},
5-
style::{Modifier, Style, Stylize},
5+
style::{Style, Stylize},
66
symbols,
77
text::Line,
88
widgets::{Block, Borders, Paragraph, Widget, Wrap},
@@ -39,9 +39,10 @@ impl PasswordDetails<'_> {
3939
let theme = Theme::new();
4040
let pass_id_field = DetailsField::new(Line::from(vec![
4141
"Password file"
42-
.add_modifier(Modifier::ITALIC | Modifier::UNDERLINED)
43-
.fg(theme.details_field_fg)
44-
.bold(),
42+
.underlined()
43+
.italic()
44+
.bold()
45+
.fg(theme.details_field_fg),
4546
" 🗐".fg(theme.details_field_fg),
4647
]))
4748
.button(
@@ -53,9 +54,10 @@ impl PasswordDetails<'_> {
5354
);
5455
let lines_field = DetailsField::new(Line::from(vec![
5556
"Number of lines"
56-
.add_modifier(Modifier::ITALIC | Modifier::UNDERLINED)
57-
.fg(theme.details_field_fg)
58-
.bold(),
57+
.underlined()
58+
.italic()
59+
.bold()
60+
.fg(theme.details_field_fg),
5961
" 🗟".fg(theme.details_field_fg),
6062
]))
6163
.button(
@@ -67,9 +69,10 @@ impl PasswordDetails<'_> {
6769
);
6870
let password_field = DetailsField::new(Line::from(vec![
6971
"Password"
70-
.add_modifier(Modifier::ITALIC | Modifier::UNDERLINED)
71-
.fg(theme.details_field_fg)
72-
.bold(),
72+
.underlined()
73+
.italic()
74+
.bold()
75+
.fg(theme.details_field_fg),
7376
" 🗝".fg(theme.details_field_fg),
7477
]))
7578
.placeholder("********")
@@ -82,9 +85,10 @@ impl PasswordDetails<'_> {
8285
);
8386
let otp_field = DetailsField::new(Line::from(vec![
8487
"One-time password (OTP)"
85-
.add_modifier(Modifier::ITALIC | Modifier::UNDERLINED)
86-
.fg(theme.details_field_fg)
87-
.bold(),
88+
.underlined()
89+
.italic()
90+
.bold()
91+
.fg(theme.details_field_fg),
8892
" 🕰".fg(theme.details_field_fg),
8993
]))
9094
.placeholder("******")
@@ -104,9 +108,10 @@ impl PasswordDetails<'_> {
104108
);
105109
let login_field = DetailsField::new(Line::from(vec![
106110
"Login"
107-
.add_modifier(Modifier::ITALIC | Modifier::UNDERLINED)
108-
.fg(theme.details_field_fg)
109-
.bold(),
111+
.underlined()
112+
.italic()
113+
.bold()
114+
.fg(theme.details_field_fg),
110115
" 🨂".fg(theme.details_field_fg),
111116
]))
112117
.button(
@@ -241,17 +246,16 @@ impl Widget for &mut PasswordDetails<'_> {
241246
}
242247

243248
// One-time password field
244-
if self.one_time_password.is_some() {
249+
if let Some(ref otp) = self.one_time_password {
245250
let field_area = right_areas.next().expect("counted before");
246-
self.otp_field
247-
.set_content(self.one_time_password.as_ref().unwrap());
251+
self.otp_field.set_content(otp);
248252
self.otp_field.render(*field_area, buf);
249253
}
250254

251255
// Login field
252-
if self.login.is_some() {
256+
if let Some(ref login) = self.login {
253257
let field_area = right_areas.next().expect("counted before");
254-
self.login_field.set_content(self.login.as_ref().unwrap());
258+
self.login_field.set_content(login);
255259
self.login_field.render(*field_area, buf);
256260
}
257261
}

src/components/password_store.rs

+39-26
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ pub struct PasswordStore<'a> {
4545
status_bar: StatusBar,
4646
event_tx: Sender<ChannelEvent>,
4747
ops_map: HashMap<&'a str, (JoinHandle<()>, String)>,
48-
clipboard: Clipboard,
48+
clipboard: Option<Clipboard>,
4949
pub app_state: AppState,
5050
render_details: bool,
5151
}
@@ -73,7 +73,7 @@ impl<'a> PasswordStore<'a> {
7373
status_bar: StatusBar::new(),
7474
event_tx,
7575
ops_map: HashMap::new(),
76-
clipboard: Clipboard::new().unwrap(),
76+
clipboard: Clipboard::new().ok(),
7777
app_state: AppState::default(),
7878
render_details: true,
7979
};
@@ -182,7 +182,7 @@ impl<'a> PasswordStore<'a> {
182182
}
183183

184184
pub fn get_selected_info(&self) -> Option<&PasswordInfo> {
185-
if self.password_subset.len() > 0 {
185+
if !self.password_subset.is_empty() {
186186
return match self.password_table.selected() {
187187
Some(index) => self.passwords.get(self.password_subset[index]),
188188
None => None,
@@ -194,15 +194,20 @@ impl<'a> PasswordStore<'a> {
194194
fn copy_pass_id(&mut self) {
195195
if let Some(info) = self.get_selected_info() {
196196
let pass_id = info.pass_id();
197-
match self.clipboard.set_text(pass_id) {
198-
Ok(()) => {
199-
let status_text = "Password file identifier copied to clipboard".into();
200-
self.status_bar.display_message(status_text);
201-
}
202-
Err(e) => {
203-
let status_text = format!("Failed to copy password file identifier: {e:?}");
204-
self.status_bar.display_message(status_text);
197+
if let Some(ref mut clipboard) = self.clipboard {
198+
match clipboard.set_text(pass_id) {
199+
Ok(()) => {
200+
let status_text = "Password file identifier copied to clipboard".into();
201+
self.status_bar.display_message(status_text);
202+
}
203+
Err(e) => {
204+
let status_text = format!("Failed to copy password file identifier: {e:?}");
205+
self.status_bar.display_message(status_text);
206+
}
205207
}
208+
} else {
209+
let status_text = String::from("✗ Clipboard not available");
210+
self.status_bar.display_message(status_text);
206211
}
207212
} else {
208213
let status_text = String::from("No entry selected");
@@ -229,7 +234,7 @@ impl<'a> PasswordStore<'a> {
229234
format!("(pass) {status}")
230235
};
231236
let status_event = ChannelEvent::Status(message);
232-
tx.send(status_event).unwrap();
237+
tx.send(status_event).expect("receiver deallocated");
233238
}
234239

235240
if run_once(
@@ -266,7 +271,7 @@ impl<'a> PasswordStore<'a> {
266271
format!("✗ (pass) {status}")
267272
};
268273
let status_event = ChannelEvent::Status(message);
269-
tx.send(status_event).unwrap();
274+
tx.send(status_event).expect("receiver deallocated");
270275
}
271276

272277
if run_once(
@@ -305,7 +310,7 @@ impl<'a> PasswordStore<'a> {
305310
format!("✗ (pass) {status}")
306311
};
307312
let status_event = ChannelEvent::Status(message);
308-
tx.send(status_event).unwrap();
313+
tx.send(status_event).expect("receiver deallocated");
309314
}
310315

311316
if run_once(
@@ -341,11 +346,13 @@ impl<'a> PasswordStore<'a> {
341346
pass_id,
342347
one_time_password,
343348
})
344-
.unwrap();
345-
tx.send(ChannelEvent::ResetStatus).unwrap();
349+
.expect("receiver deallocated");
350+
tx.send(ChannelEvent::ResetStatus)
351+
.expect("receiver deallocated");
346352
} else {
347353
let message = format!("✗ (pass) {}", String::from_utf8_lossy(&output.stderr));
348-
tx.send(ChannelEvent::Status(message)).unwrap();
354+
tx.send(ChannelEvent::Status(message))
355+
.expect("receiver deallocated");
349356
}
350357
}
351358

@@ -380,11 +387,13 @@ impl<'a> PasswordStore<'a> {
380387
pass_id,
381388
file_contents,
382389
})
383-
.unwrap();
384-
tx.send(ChannelEvent::ResetStatus).unwrap();
390+
.expect("receiver deallocated");
391+
tx.send(ChannelEvent::ResetStatus)
392+
.expect("receiver deallocated");
385393
} else {
386394
let message = format!("✗ (pass) {}", String::from_utf8_lossy(&output.stderr));
387-
tx.send(ChannelEvent::Status(message)).unwrap();
395+
tx.send(ChannelEvent::Status(message))
396+
.expect("receiver deallocated");
388397
};
389398
}
390399

@@ -444,8 +453,9 @@ impl<'a> PasswordStore<'a> {
444453
return;
445454
}
446455

447-
if pass_id != self.get_selected_info().unwrap().pass_id {
448-
return;
456+
match self.get_selected_info() {
457+
Some(info) if pass_id == info.pass_id => (),
458+
_ => return,
449459
}
450460

451461
self.file_popup.set_content(&pass_id, &message.clone());
@@ -735,10 +745,13 @@ impl<'a> Component for PasswordStore<'a> {
735745
Action::DisplayOneTimePassword {
736746
pass_id,
737747
one_time_password,
738-
} if pass_id == self.get_selected_info().unwrap().pass_id => {
739-
self.password_details.one_time_password = Some(one_time_password);
740-
None
741-
}
748+
} => match self.get_selected_info() {
749+
Some(info) if pass_id == info.pass_id => {
750+
self.password_details.one_time_password = Some(one_time_password);
751+
None
752+
}
753+
_ => None,
754+
},
742755
_ => None,
743756
};
744757
Ok(action)

src/components/password_table.rs

+18-19
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ pub struct PasswordTable<'a> {
2424
length: usize,
2525
table_state: TableState,
2626
pub highlight_pattern: Option<String>,
27-
scrollbar: Scrollbar<'a>,
2827
scrollbar_state: ScrollbarState,
2928
area: Option<Rect>,
3029
mouse_content_area: Option<Rect>,
@@ -38,24 +37,12 @@ impl<'a> PasswordTable<'a> {
3837
let length = rows.len();
3938
let table = Self::build_table(rows, &theme);
4039
let scrollbar_state = ScrollbarState::new(length);
41-
let scrollbar = Scrollbar::default()
42-
.orientation(ScrollbarOrientation::VerticalRight)
43-
.begin_style(Style::new().bg(theme.table_header_bg))
44-
.track_style(
45-
Style::new()
46-
.fg(theme.table_track_fg)
47-
.bg(theme.table_track_bg),
48-
)
49-
.thumb_style(Style::new().fg(theme.standard_fg).bg(theme.standard_bg))
50-
.begin_symbol(Some(" "))
51-
.end_symbol(None);
5240
Self {
5341
theme,
5442
table,
5543
length,
5644
table_state: TableState::new(),
5745
highlight_pattern: None,
58-
scrollbar,
5946
scrollbar_state,
6047
area: None,
6148
mouse_content_area: None,
@@ -131,7 +118,7 @@ impl<'a> PasswordTable<'a> {
131118
Cell::from(Line::from(pass_id_parts)),
132119
Cell::from(info.last_modified()),
133120
])
134-
.style(Style::default().bg(bg_color))
121+
.style(Style::default().fg(self.theme.table_row_fg).bg(bg_color))
135122
})
136123
.collect()
137124
} else {
@@ -207,8 +194,10 @@ impl<'a> PasswordTable<'a> {
207194
impl<'a> Widget for &mut PasswordTable<'a> {
208195
fn render(self, area: Rect, buf: &mut Buffer) {
209196
self.area = Some(area);
197+
let theme = self.theme;
210198

211-
let layout = Layout::horizontal([Constraint::Min(1), Constraint::Length(1)]).split(area);
199+
let [table_area, s_area] =
200+
Layout::horizontal([Constraint::Min(1), Constraint::Length(1)]).areas(area);
212201

213202
// Calculate areas for mouse interaction
214203
let mouse_content_area = Rect {
@@ -225,10 +214,20 @@ impl<'a> Widget for &mut PasswordTable<'a> {
225214
self.mouse_content_area = Some(mouse_content_area);
226215
self.mouse_scrollbar_area = Some(mouse_scrollbar_area);
227216

228-
StatefulWidget::render(&self.table, layout[0], buf, &mut self.table_state);
229-
self.scrollbar
230-
.clone()
231-
.render(layout[1], buf, &mut self.scrollbar_state);
217+
StatefulWidget::render(&self.table, table_area, buf, &mut self.table_state);
218+
219+
Scrollbar::default()
220+
.orientation(ScrollbarOrientation::VerticalRight)
221+
.begin_style(Style::new().bg(theme.table_header_bg))
222+
.track_style(
223+
Style::new()
224+
.fg(theme.table_track_fg)
225+
.bg(theme.table_track_bg),
226+
)
227+
.thumb_style(Style::new().fg(theme.standard_fg).bg(theme.standard_bg))
228+
.begin_symbol(Some(" "))
229+
.end_symbol(None)
230+
.render(s_area, buf, &mut self.scrollbar_state);
232231
}
233232
}
234233

0 commit comments

Comments
 (0)