Skip to content

Commit 70f24d8

Browse files
committed
Add pass plugin
1 parent 3ad6383 commit 70f24d8

File tree

3 files changed

+193
-1
lines changed

3 files changed

+193
-1
lines changed

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ name = "vonalc"
1212
path = "src/vonal_client/main.rs"
1313

1414
[features]
15-
default = ["launcher_plugin", "math_plugin"]
15+
default = ["launcher_plugin", "math_plugin", "pass_plugin"]
1616
launcher_plugin = ["freedesktop-desktop-entry", "regex"]
1717
math_plugin = []
18+
pass_plugin = []
1819

1920
[dependencies]
2021
egui = "0.20.1"

src/vonal_daemon/plugins/mod.rs

+6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use crate::{
99
mod launcher;
1010
#[cfg(feature = "math_plugin")]
1111
mod math;
12+
#[cfg(feature = "pass_plugin")]
13+
mod pass;
1214

1315
pub enum PluginFlowControl {
1416
/// check other plugins as well
@@ -79,6 +81,8 @@ impl PluginManager {
7981
"math_plugin".to_string(),
8082
#[cfg(feature = "launcher_plugin")]
8183
"launcher_plugin".to_string(),
84+
#[cfg(feature = "pass_plugin")]
85+
"pass_plugin".to_string(),
8286
],
8387
)?;
8488

@@ -89,6 +93,8 @@ impl PluginManager {
8993
match plugin.as_str() {
9094
#[cfg(feature = "math_plugin")]
9195
"math_plugin" => self.plugins.push(Box::new(math::Math::new())),
96+
#[cfg(feature = "pass_plugin")]
97+
"pass_plugin" => self.plugins.push(Box::new(pass::Pass::new())),
9298
#[cfg(feature = "launcher_plugin")]
9399
"launcher_plugin" => self.plugins.push(Box::new(launcher::Launcher::new())),
94100
plugin_name => return Err(ConfigError::BadEntryError {

src/vonal_daemon/plugins/pass/mod.rs

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
use std::{
2+
error::Error,
3+
fmt::Display,
4+
io::{BufRead, BufReader},
5+
process::{Command, Stdio},
6+
};
7+
8+
use crate::{
9+
config::{ConfigBuilder, ConfigError},
10+
theme::list::{CreateList, ListState},
11+
};
12+
13+
use super::{Plugin, PluginFlowControl};
14+
15+
#[derive(Default)]
16+
pub struct Pass {
17+
list_state: ListState,
18+
config_command_list_passwords: String,
19+
config_command_copy_password: String,
20+
config_command_type_password: String,
21+
config_prefix: String,
22+
}
23+
24+
impl Pass {
25+
pub fn new() -> Self {
26+
Default::default()
27+
}
28+
29+
fn list_passwords(&self) -> Result<Vec<String>, Box<dyn Error>> {
30+
let call = Command::new("bash")
31+
.stdout(Stdio::piped())
32+
.stderr(Stdio::piped())
33+
.arg("-c")
34+
.arg(&self.config_command_list_passwords)
35+
.spawn()?
36+
.wait_with_output()?;
37+
38+
let stderr = String::from_utf8_lossy(&call.stderr);
39+
if !stderr.is_empty() {
40+
return Err(Box::new(PassError(stderr.to_string())));
41+
}
42+
43+
let stdout = String::from_utf8_lossy(&call.stdout);
44+
let passwords = stdout.lines().map(ToString::to_string).collect();
45+
Ok(passwords)
46+
}
47+
48+
fn copy_password(&self, pw: &str) -> Result<(), std::io::Error> {
49+
let stdout = Command::new("sh")
50+
.stdout(Stdio::piped())
51+
.arg("-c")
52+
.arg(self.config_command_copy_password.replace("{name}", &pw))
53+
.spawn()?
54+
.stdout;
55+
if let Some(stdout) = stdout {
56+
let reader = BufReader::new(stdout);
57+
if let Some(result) = reader.lines().next() {
58+
Command::new("notify-send").arg(result?).spawn()?;
59+
}
60+
}
61+
Ok(())
62+
}
63+
64+
fn type_password(&self, pw: &str) -> Result<(), std::io::Error> {
65+
Command::new("bash")
66+
.stdout(Stdio::piped())
67+
.arg("-c")
68+
.arg(self.config_command_type_password.replace("{name}", &pw))
69+
.spawn()?;
70+
Ok(())
71+
}
72+
73+
fn render_copy_button(
74+
&mut self,
75+
ui: &mut crate::theme::list::RowUi,
76+
gl_window: &crate::windowing::GlutinWindowContext,
77+
pw: &String,
78+
query: &mut String,
79+
) {
80+
if ui.secondary_action("Copy").activated {
81+
gl_window.window.set_visible(false);
82+
if let Err(e) = self.copy_password(pw) {
83+
gl_window.window.set_visible(true);
84+
ui.label(&e.to_string())
85+
} else {
86+
*query = "".into();
87+
}
88+
}
89+
}
90+
91+
fn render_type_button(
92+
&mut self,
93+
mut ui: crate::theme::list::RowUi,
94+
gl_window: &crate::windowing::GlutinWindowContext,
95+
pw: String,
96+
query: &mut String,
97+
) {
98+
if ui.secondary_action("Type").activated {
99+
gl_window.window.set_visible(false);
100+
if let Err(e) = self.type_password(&pw) {
101+
gl_window.window.set_visible(true);
102+
ui.label(&e.to_string())
103+
} else {
104+
*query = "".into();
105+
}
106+
}
107+
}
108+
}
109+
110+
const DEFAULT_PREFIX: &str = "pass_plugin";
111+
const DEFAULT_LIST_PASSWORDS_COMMAND: &str = r#"
112+
shopt -s nullglob globstar
113+
prefix=${PASSWORD_STORE_DIR-~/.password-store}
114+
password_files=( "$prefix"/**/*.gpg )
115+
password_files=( "${password_files[@]#"$prefix"/}" )
116+
password_files=( "${password_files[@]%.gpg}" )
117+
printf '%s\n' "${password_files[@]}"
118+
"#;
119+
const DEFAULT_COPY_PASSWORD_COMMAND: &str = r#"pass show -c {name}"#;
120+
const DEFAULT_TYPE_PASSWORD_COMMAND: &str =
121+
r#"pass show {name} | { IFS= read -r pass; printf %s "$pass"; } | xdotool"#;
122+
123+
impl Plugin for Pass {
124+
fn search(
125+
&mut self,
126+
query: &mut String,
127+
ui: &mut egui::Ui,
128+
gl_window: &crate::windowing::GlutinWindowContext,
129+
) -> PluginFlowControl {
130+
if !query.starts_with("pass_plugin") {
131+
return PluginFlowControl::Continue;
132+
}
133+
match self.list_passwords() {
134+
Ok(passwords) => {
135+
const NUMBER_OF_BUTTONS: usize = 2;
136+
self.list_state.update(ui.ctx(), passwords.len(), |_| NUMBER_OF_BUTTONS);
137+
ui.list(self.list_state, |mut ui| {
138+
for pw in passwords {
139+
ui.row(|mut ui| {
140+
ui.label(&pw);
141+
self.render_copy_button(&mut ui, gl_window, &pw, query);
142+
self.render_type_button(ui, gl_window, pw, query);
143+
})
144+
}
145+
});
146+
}
147+
Err(err) => {
148+
ui.label(err.to_string());
149+
}
150+
}
151+
PluginFlowControl::Break
152+
}
153+
154+
fn configure(&mut self, mut builder: ConfigBuilder) -> Result<ConfigBuilder, ConfigError> {
155+
builder.group("pass_plugin", |builder| {
156+
self.config_prefix = builder.get_or_create("prefix", DEFAULT_PREFIX.into())?;
157+
self.config_command_list_passwords = builder.get_or_create(
158+
"command_list_password",
159+
DEFAULT_LIST_PASSWORDS_COMMAND.trim_start().into(),
160+
)?;
161+
self.config_command_copy_password = builder.get_or_create(
162+
"command_copy_password",
163+
DEFAULT_COPY_PASSWORD_COMMAND.into(),
164+
)?;
165+
self.config_command_type_password = builder.get_or_create(
166+
"command_type_password",
167+
DEFAULT_TYPE_PASSWORD_COMMAND.into(),
168+
)?;
169+
Ok(())
170+
})?;
171+
172+
Ok(builder)
173+
}
174+
}
175+
176+
#[derive(Debug)]
177+
struct PassError(String);
178+
179+
impl Display for PassError {
180+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181+
f.write_str(&self.0)
182+
}
183+
}
184+
185+
impl Error for PassError {}

0 commit comments

Comments
 (0)