Skip to content

Commit f93a89f

Browse files
committed
[icon] showing icons in emacs
1 parent 4cf05d9 commit f93a89f

File tree

5 files changed

+141
-9
lines changed

5 files changed

+141
-9
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ keepass = { path = "../libkeepass-rs" }
2121
lexpr = "0.2.6"
2222
libreauth = "0.15.0"
2323
log = "0.4.17"
24+
once_cell = "1.16.0"
2425
rpassword = "7.1.0"
2526
rustyline = "10.0.0"
2627
shadow-rs = "0.17.1"

TODOs.org

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ Can be a proxy to call =keepassxc-cli= by the provided password.
1212
* TODO TOTP generation
1313
only some types are supported now (SHA1/6-digit code)
1414

15+
* TODO Tag search
16+
Need to modify =keepass-rs= to decrypt =Tags=
17+
1518
* TODO Emacs integration
1619

1720
** DONE hydra/transient
@@ -21,7 +24,8 @@ CLOSED: [2022-11-25 Fri 16:03]
2124
CLOSED: [2022-11-25 Fri 16:03]
2225
use built-in completing-read
2326

24-
** TODO SVG icon?
27+
** DONE SVG icon?
28+
CLOSED: [2022-12-01 Thu 17:14]
2529
Is it possible to render SVG icons in =completing-read= buffer?
2630

2731
** TODO Clear kill-ring

keepass.el

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,17 @@
2323
"Face for title."
2424
:group 'keepass-faces)
2525

26-
(cl-defstruct keepass-entry "Keepass Entry without password" id title username url note has-otp)
26+
(defcustom keepass-icon-width 16
27+
"icon width"
28+
:type 'number
29+
:group 'keepass)
30+
31+
(defcustom keepass-icon-height 16
32+
"icon height"
33+
:type 'number
34+
:group 'keepass)
35+
36+
(cl-defstruct keepass-entry "Keepass Entry without password" id title username url note has-otp icon)
2737

2838
(defvar keepass~all-entries nil "All entries (served as a cache)")
2939

@@ -229,7 +239,7 @@ debuggable (backtrace) error."
229239
(db-pw (password-read
230240
(format "Password for %s: " keepass-database)
231241
keepass-database))
232-
)
242+
(args (cons "--icon" args)))
233243
(keepass-log 'misc "%S" args)
234244
(setq keepass~all-entries nil)
235245
(clrhash keepass~entry-map)
@@ -330,7 +340,7 @@ debuggable (backtrace) error."
330340
(defun keepass-list ()
331341
"List all entries and cache them in `keepass~all-entries'"
332342
(interactive)
333-
(unless keepass~all-entries (keepass~call "ls -f id title username url note has-otp")))
343+
(unless keepass~all-entries (keepass~call "ls -f id title username url note has-otp icon")))
334344

335345

336346
(defun keepass--get (id field &optional show copy)
@@ -350,6 +360,29 @@ debuggable (backtrace) error."
350360
(interactive)
351361
(keepass--get keepass-current-selected-id field show copy))
352362

363+
(defun keepass--format-icon (icon-path)
364+
(if icon-path (propertize "<"
365+
'display
366+
`(image
367+
:type imagemagick
368+
:file ,icon-path
369+
;; :scale 1
370+
:width ,keepass-icon-width
371+
:height ,keepass-icon-height
372+
:format nil
373+
:transform-smoothing t
374+
;; :relief 1
375+
:ascent center
376+
)
377+
;; 'rear-nonsticky
378+
;; '(display)
379+
;; 'front-sticky
380+
;; '(read-only)
381+
;; 'fontified
382+
;; t
383+
)
384+
" "))
385+
353386
(defun keepass--format-entry (entry)
354387
(let* (
355388
(id (keepass-entry-id entry))
@@ -358,10 +391,12 @@ debuggable (backtrace) error."
358391
(url (keepass-entry-url entry))
359392
(note (keepass-entry-note entry))
360393
(has-otp (keepass-entry-has-otp entry))
361-
(item-str (format "Title: %s\nUsername: %s\nURL: %s\nNote: %s\nHas-OTP: %s"
394+
(icon (keepass-entry-icon entry))
395+
(item-str (format "Title: %s\nUsername: %s\nURL: %s%2s\nNote: %s\nHas-OTP: %s"
362396
(propertize title 'face 'font-lock-type-face)
363397
(propertize username 'face 'font-lock-function-name-face)
364398
(propertize url 'face 'font-lock-variable-name-face)
399+
(keepass--format-icon icon)
365400
note
366401
(propertize (if has-otp "Yes" "No") 'face 'font-lock-warning-face)
367402
)))
@@ -384,7 +419,9 @@ debuggable (backtrace) error."
384419
(url (keepass-entry-url entry))
385420
(url (truncate-string-to-width url 20 0 ?\s t))
386421
(note (keepass-entry-note entry))
387-
(item-str (format "%-20s\t%-20s\t%-20s\t%s"
422+
(icon (keepass-entry-icon entry))
423+
(item-str (format "%s %-20s\t%-20s\t%-20s\t%s"
424+
(keepass--format-icon icon)
388425
(propertize title 'face 'font-lock-type-face)
389426
;; title
390427
(propertize username 'face 'font-lock-function-name-face)

src/main.rs

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,69 @@
33
use anyhow::{anyhow, bail, Result};
44
use clap::Parser;
55
use clap_verbosity_flag::Verbosity;
6+
use directories::{BaseDirs, ProjectDirs, UserDirs};
67
use is_terminal::IsTerminal;
7-
use keepass::{Database, Entry, NodeRef};
8+
use keepass::{Database, Entry, Icon, NodeRef};
89
use lexpr::{print, sexp, Value};
910
use libreauth::oath::TOTPBuilder;
11+
use once_cell::sync::OnceCell;
1012
use rustyline::error::ReadlineError;
1113
use rustyline::Editor;
1214
use std::char;
1315
use std::collections::BTreeMap;
16+
use std::fs;
1417
use std::fs::File;
1518
use std::io::Read;
1619
use std::io::{self, Write};
17-
use std::path::Path;
20+
use std::path::{Path, PathBuf};
1821
use std::time::SystemTime;
1922
use tracing::{debug, info};
2023
use tracing_core::Level;
2124
use tracing_subscriber::{filter, prelude::*};
2225
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
26+
2327
use url::Url;
2428
shadow_rs::shadow!(build);
2529

2630
const COOKIE_PRE: char = 254 as char;
2731
const COOKIE_POST: char = 255 as char;
2832

33+
/// project directories
34+
static PROJECT_DIRS: OnceCell<ProjectDirs> = OnceCell::new();
35+
36+
fn get_project_dirs() -> &'static ProjectDirs {
37+
&PROJECT_DIRS.get().unwrap()
38+
}
39+
40+
fn get_data_dir() -> &'static Path {
41+
&PROJECT_DIRS.get().unwrap().data_dir()
42+
}
43+
44+
fn get_custom_icon_dir() -> PathBuf {
45+
let data_dir = get_data_dir();
46+
let custom_icon_path = data_dir.join("custom_icons");
47+
custom_icon_path
48+
}
49+
50+
fn get_custom_icon_path(uuid: &String) -> PathBuf {
51+
let custom_icon_dir = get_custom_icon_dir();
52+
let bytes = base64::decode(uuid).unwrap();
53+
let new_id = base58::ToBase58::to_base58(bytes.as_slice());
54+
custom_icon_dir.join(new_id)
55+
}
56+
57+
fn get_builtin_icon_dir() -> PathBuf {
58+
let data_dir = get_data_dir();
59+
let icon_path = data_dir.join("icons");
60+
icon_path
61+
}
62+
63+
fn get_builtin_icon_path(id: u8) -> PathBuf {
64+
let builtin_icon_dir = get_builtin_icon_dir();
65+
let icon_file = format!("{:02}.{}", id, "svg");
66+
builtin_icon_dir.join(icon_file)
67+
}
68+
2969
#[derive(Parser, Debug, Clone)]
3070
#[clap(author, version, about)]
3171
pub struct Args {
@@ -51,6 +91,10 @@ pub struct Args {
5191
#[clap(long = "database", short = 'd')]
5292
database: Option<String>,
5393

94+
/// icons
95+
#[clap(long = "icon", short = 'i', default_value = "false", global = true)]
96+
icon: bool,
97+
5498
#[clap(flatten)]
5599
verbose: clap_verbosity_flag::Verbosity,
56100
}
@@ -140,6 +184,7 @@ pub struct ParsedEntry {
140184
pub id: u64,
141185
pub fields: BTreeMap<String, Field>,
142186
pub has_otp: bool,
187+
pub icon: Icon,
143188
}
144189

145190
impl ParsedEntry {
@@ -154,6 +199,20 @@ impl ParsedEntry {
154199
values.push(Value::string(self.get_otp_code()?));
155200
} else if field == "has-otp" {
156201
values.push(Value::Bool(self.has_otp));
202+
} else if field == "icon" {
203+
let value = match &self.icon {
204+
Icon::IconID(id) => {
205+
Value::string(get_builtin_icon_path(*id).display().to_string())
206+
}
207+
Icon::CustomIcon(uuid) => {
208+
Value::string(get_custom_icon_path(uuid).display().to_string())
209+
}
210+
Icon::None => {
211+
// NOTE: 0 as default icon for keepassxc
212+
Value::string(get_builtin_icon_path(0).display().to_string())
213+
}
214+
};
215+
values.push(value);
157216
} else {
158217
let val = self.fields.get(field);
159218
if let Some(v) = val {
@@ -290,9 +349,11 @@ impl TryFrom<&Entry> for ParsedEntry {
290349
fields.insert(f.index(), f);
291350
}
292351
}
352+
let mut icon = e.icon.clone();
293353
Ok(Self {
294354
fields,
295355
has_otp,
356+
icon,
296357
..Default::default()
297358
})
298359
}
@@ -384,6 +445,25 @@ impl<'a, 'b> KPClient<'a> {
384445
}
385446
}
386447

448+
pub fn load_icons(&'a self) -> Result<()> {
449+
let custom_icon_path = get_custom_icon_dir();
450+
debug!("cicon {custom_icon_path:#?}");
451+
fs::create_dir_all(&custom_icon_path)?;
452+
for (uuid, icon_data) in self.db_manager.db.meta.custom_icons.iter() {
453+
// uuid is base64, which is not good!
454+
// transform it to base58 instead; can also be base62
455+
let bytes = base64::decode(uuid).unwrap();
456+
let new_id = base58::ToBase58::to_base58(bytes.as_slice());
457+
debug!("old {uuid} new {new_id}");
458+
let image_path = custom_icon_path.join(&new_id);
459+
if !image_path.try_exists()? {
460+
let icon_bytes = base64::decode(icon_data).unwrap();
461+
fs::write(image_path, icon_bytes);
462+
}
463+
}
464+
Ok(())
465+
}
466+
387467
pub fn do_list(
388468
&'a self,
389469
fields: &Option<Vec<String>>,
@@ -445,7 +525,7 @@ pub struct DatabaseManager<'a> {
445525
impl<'a> DatabaseManager<'a> {
446526
pub fn new(path: &'a Path, password: String) -> Result<Self> {
447527
let db = Database::open(&mut File::open(path)?, Some(password.as_str()), None)?;
448-
debug!("{:#?}", &db);
528+
// debug!("{:#?}", &db);
449529
// std::process::exit(0);
450530
Ok(Self { path, password, db })
451531
}
@@ -462,6 +542,10 @@ impl<'a> DatabaseManager<'a> {
462542
}
463543

464544
fn main() -> Result<()> {
545+
if let Some(proj_dirs) = ProjectDirs::from("", "", "keepass-cli") {
546+
PROJECT_DIRS.set(proj_dirs).unwrap();
547+
}
548+
465549
let args = Args::parse();
466550

467551
let filter = filter::Targets::new()
@@ -499,6 +583,11 @@ fn main() -> Result<()> {
499583
db_manager.reload();
500584
let mut kp_client = KPClient::new(&db_manager)?;
501585

586+
if args.icon {
587+
kp_client.load_icons();
588+
// return Ok(());
589+
}
590+
502591
match &args.action {
503592
Action::List { fields } => {
504593
kp_client.reload();

0 commit comments

Comments
 (0)