Files
HEL/hel/src/lk.rs
T
Oleksandr Kozachuk 74d3296882 feat: Notion backend for the catalog + set command + ls/source/save UX
Persist the entry catalog to a Notion page instead of an external script.

- `hel store notion:<ref>` reads the dump from stdin and writes it into a
  single code block on the page; `hel load notion:<ref>` prints it back.
  `<ref>` is a page id (UUID) or a page title (case-insensitive search).
  Token from HEL_NOTION_TOKEN. Networking lives in helcli (ureq + serde_json);
  the core lib and the wasm build are untouched. argv is dispatched before the
  REPL starts, so the subcommands never read ~/.helrc or prompt for a master.

- `set <key> <value>`: runtime config (e.g. hel_dump, hel_notion_token) from
  ~/.helrc instead of env vars. The map is exported (uppercased) into spawned
  pipe/source children, so `set hel_notion_token ...` reaches `hel store`.
  Values are redacted in history.

- `ls` now matches case-insensitively.
- `source` echoes `source <target>`.
- `save` prints a `< removed` / `> added` diff against the last load/save
  snapshot so removals are noticed before they persist.
2026-06-08 16:53:23 +02:00

84 lines
2.8 KiB
Rust

use crate::password::{fix_password_recursion, Name, PasswordRef};
use parking_lot::ReentrantMutex;
use regex::{Captures, Regex};
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
pub type LKRef = Arc<ReentrantMutex<RefCell<LK>>>;
#[derive(Debug)]
pub struct LK {
pub db: HashMap<Name, PasswordRef>,
pub ls: HashMap<String, PasswordRef>,
pub secrets: HashMap<Name, String>,
/// Serialized dump as of the last load (`source`) or save (`dump`). Used to
/// show a `< removed` / `> added` diff on save so removals are noticed.
pub last_dump: Option<String>,
}
impl LK {
pub fn new() -> Self {
Self {
db: HashMap::new(),
ls: HashMap::new(),
secrets: HashMap::new(),
last_dump: None,
}
}
pub fn fix_hierarchy(&self) {
lazy_static! {
static ref RE: Regex = Regex::new(r"\s*\^([!-~]+)").unwrap();
}
for db in vec![&self.db, &self.ls] {
for (_, name) in db {
let comment = name.lock().borrow().comment.clone();
match comment {
Some(comment) => {
let mut changed = false;
let new = RE
.replace(comment.as_str(), |c: &Captures| {
let folder = c[1].to_string();
match self.db.get(&folder) {
Some(entry) => {
name.lock().borrow_mut().parent = Some(entry.clone());
changed = true;
}
None => (),
}
""
})
.trim()
.to_string();
if changed && new != comment {
name.lock().borrow_mut().comment = if new.len() > 0 { Some(new) } else { None }
}
}
None => (),
}
fix_password_recursion(name.clone());
}
}
}
}
impl PartialEq for LK {
fn eq(&self, other: &Self) -> bool {
if self.db.len() != other.db.len() || self.ls.len() != other.ls.len() || self.secrets != other.secrets {
return false;
}
for (k, v) in &self.db {
if !other.db.contains_key(k) || *other.db[k].lock() != *v.lock() {
return false;
}
}
for (k, v) in &self.ls {
if !other.ls.contains_key(k) || *other.ls[k].lock() != *v.lock() {
return false;
}
}
true
}
}