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.
This commit is contained in:
Oleksandr Kozachuk
2026-06-08 16:53:23 +02:00
parent 38e7df982f
commit 74d3296882
11 changed files with 550 additions and 19 deletions
+33
View File
@@ -3,6 +3,7 @@ use num_integer::Integer;
use parking_lot::Mutex;
use parking_lot::ReentrantMutex;
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt;
use std::path::Path;
use std::sync::Arc;
@@ -44,6 +45,33 @@ lazy_static! {
_ => home::dir().join(".hel_dump").into_boxed_path(),
}
};
/// Runtime configuration set via the `set <key> <value>` command (e.g. from
/// `~/.helrc`). Lets the user keep settings like `hel_dump` and
/// `hel_notion_token` in the init script instead of environment variables.
/// Keys are stored lowercase; `config_get` falls back to the uppercased
/// environment variable, and `config_envs` exports the map (uppercased) into
/// child processes spawned by the pipe/source plumbing.
pub static ref CONFIG: Mutex<HashMap<String, String>> = Mutex::new(HashMap::new());
}
/// Store a runtime config value under its lowercased key.
pub fn config_set(key: &str, value: &str) {
CONFIG.lock().insert(key.to_lowercase(), value.to_string());
}
/// Look up a config value: runtime `set` map first, then the uppercased env var.
pub fn config_get(key: &str) -> Option<String> {
if let Some(v) = CONFIG.lock().get(&key.to_lowercase()) {
return Some(v.clone());
}
std::env::var(key.to_uppercase()).ok()
}
/// The runtime config as `(UPPERCASE_KEY, value)` pairs, for injection into the
/// environment of child processes (so `set hel_notion_token …` reaches a spawned
/// `hel store`/`hel load`).
pub fn config_envs() -> Vec<(String, String)> {
CONFIG.lock().iter().map(|(k, v)| (k.to_uppercase(), v.clone())).collect()
}
#[derive(thiserror::Error, Debug, PartialEq)]
@@ -75,6 +103,7 @@ pub enum Command<'a> {
PasteBuffer(String),
Source(String),
Dump(Option<String>),
Set(String, String),
Comment(Name, Comment),
Error(LKErr<'a>),
Noop,
@@ -100,6 +129,7 @@ impl<'a> PartialEq for Command<'a> {
(Command::PasteBuffer(s), Command::PasteBuffer(o)) => s == o,
(Command::Source(s), Command::Source(o)) => s == o,
(Command::Dump(s), Command::Dump(o)) => s == o,
(Command::Set(a, b), Command::Set(x, y)) => a == x && b == y,
(Command::Comment(a, b), Command::Comment(x, y)) => a == x && b == y,
(Command::Error(s), Command::Error(o)) => s == o,
(Command::Noop, Command::Noop) => true,
@@ -130,6 +160,9 @@ impl<'a> std::fmt::Display for Command<'a> {
Command::Source(s) => write!(f, "source {}", s),
Command::Dump(None) => write!(f, "dump"),
Command::Dump(Some(s)) => write!(f, "dump {}", s),
// Value redacted: a `set` may carry a secret (e.g. hel_notion_token)
// and this Display feeds the history entry.
Command::Set(a, _) => write!(f, "set {} ***", a),
Command::Comment(a, None) => write!(f, "comment {}", a),
Command::Comment(a, Some(b)) => write!(f, "comment {} {}", a, b),
Command::Error(s) => write!(f, "error {}", s),