feat: WASM web app (browser LesS/KEY) backed by the hel core
Run the real hel core in the browser via WebAssembly, replacing the plain-JS ooke.github.io/sk reimplementation so the web and the CLI share one generator. - hel core: new storage module (native fs / wasm localStorage) backs init/source/dump/correct; the pipe and pb branches are gated out of wasm; the wasm editor is completed (EditorRef) and its password prompt no longer blocks the single JS thread (synchronous hel_get_password instead of a thread::sleep poll). - helwasm: hel_load_script for bulk import, a panic hook in hel_init, dropped the ok_add / web-sys leftovers; build.sh runs wasm-bindgen into a committed pkg/ so GitHub Pages can serve the static dir with no CI. - UI: rewritten index.html + style.css in the kaizenkodo.org style — responsive, masked-but-copyable passwords with click-to-reveal, a quick-generate card plus a full command console, localStorage persistence, and paste import/export.
This commit is contained in:
+53
-32
@@ -1,10 +1,7 @@
|
||||
use regex::Regex;
|
||||
use sha1::{Digest, Sha1};
|
||||
use std::cmp::min;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::io::{BufWriter, Write};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::parser::command_parser;
|
||||
use crate::password::fix_password_recursion;
|
||||
@@ -12,6 +9,9 @@ use crate::password::{Name, Password, PasswordRef};
|
||||
use crate::repl::LKEval;
|
||||
use crate::structs::{config_get, config_set, LKOut, Radix, CORRECT_FILE, DUMP_FILE};
|
||||
use crate::utils::editor::password;
|
||||
// call_cmd_with_input / get_cmd_args_from_command / get_copy_command_from_env are
|
||||
// only used by the native (non-wasm) subprocess branches.
|
||||
#[cfg_attr(target_arch = "wasm32", allow(unused_imports))]
|
||||
use crate::utils::{call_cmd_with_input, get_cmd_args_from_command, get_copy_command_from_env, rnd};
|
||||
|
||||
impl<'a> LKEval<'a> {
|
||||
@@ -214,6 +214,14 @@ impl<'a> LKEval<'a> {
|
||||
let data = print.out.data();
|
||||
print.out.copy_err(&out);
|
||||
if data.len() > 0 {
|
||||
// Clipboard copy shells out to pbcopy/xclip/tmux: native only.
|
||||
// In the browser the page provides a Copy button instead.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
out.e("error: pb (clipboard copy) is not available in the browser; use the Copy button".to_string());
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let (copy_command, copy_cmd_args) = get_copy_command_from_env();
|
||||
match call_cmd_with_input(©_command, ©_cmd_args, &data) {
|
||||
Ok(s) if s.len() > 0 => {
|
||||
@@ -228,13 +236,23 @@ impl<'a> LKEval<'a> {
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => out.e(format!("error: failed to parse command {}: {}", command, e.to_string())),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cmd_source(&self, out: &LKOut, source: &String) -> bool {
|
||||
out.o(format!("source {}", source));
|
||||
let script = if source.trim().ends_with("|") {
|
||||
let script: String;
|
||||
if source.trim().ends_with("|") {
|
||||
// Loading from a command's output needs a subprocess: native only.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
out.e("error: pipe source is not available in the browser".to_string());
|
||||
return false;
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let (cmd, args) = match get_cmd_args_from_command(source.trim().trim_end_matches('|')) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
@@ -242,23 +260,25 @@ impl<'a> LKEval<'a> {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
match call_cmd_with_input(&cmd, &args, "") {
|
||||
script = match call_cmd_with_input(&cmd, &args, "") {
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
out.e(format!("error: failed to execute command {}: {}", cmd, e.to_string()));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
let script = shellexpand::full(source).unwrap().into_owned();
|
||||
match std::fs::read_to_string(script) {
|
||||
// File path on native; localStorage key in the browser.
|
||||
let key = shellexpand::full(source).unwrap().into_owned();
|
||||
script = match crate::storage::read(&key) {
|
||||
Ok(script) => script,
|
||||
Err(e) => {
|
||||
out.e(format!("error: failed to read file {}: {}", source, e.to_string()));
|
||||
out.e(format!("error: failed to read {}: {}", source, e.to_string()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
match command_parser::script(&script) {
|
||||
Ok(cmd_list) => {
|
||||
for cmd in cmd_list {
|
||||
@@ -321,17 +341,14 @@ impl<'a> LKEval<'a> {
|
||||
None => config_get("hel_dump").unwrap_or_else(|| DUMP_FILE.to_str().unwrap().to_string()),
|
||||
};
|
||||
let script = shellexpand::full(&script).unwrap().into_owned();
|
||||
fn save_dump(data: &HashMap<Name, PasswordRef>, script: &String) -> std::io::Result<()> {
|
||||
let file = fs::File::create(script)?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
let mut vals = data.values().map(|v| v.clone()).collect::<Vec<PasswordRef>>();
|
||||
vals.sort_by(|a, b| a.lock().borrow().name.cmp(&b.lock().borrow().name));
|
||||
for pwd in vals {
|
||||
writeln!(writer, "add {}", pwd.lock().borrow().to_string())?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
if script.trim().starts_with("|") {
|
||||
// Piping the dump to a command needs a subprocess: native only.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
out.e("error: pipe dump is not available in the browser".to_string());
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let (cmd, args) = match get_cmd_args_from_command(script.trim().trim_start_matches('|')) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
@@ -355,6 +372,7 @@ impl<'a> LKEval<'a> {
|
||||
} else {
|
||||
out.o(format!("Passwords saved to command {}", cmd));
|
||||
}
|
||||
}
|
||||
} else if script.trim() == "-" {
|
||||
let mut vals = (&self.state.lock().borrow().db).values().map(|v| v.clone()).collect::<Vec<PasswordRef>>();
|
||||
vals.sort_by(|a, b| a.lock().borrow().name.cmp(&b.lock().borrow().name));
|
||||
@@ -362,14 +380,15 @@ impl<'a> LKEval<'a> {
|
||||
out.o(format!("add {}", pwd.lock().borrow().to_string()))
|
||||
}
|
||||
} else {
|
||||
// File path on native; localStorage key in the browser.
|
||||
let data = self.serialize_db();
|
||||
self.show_dump_diff(out, &data);
|
||||
// Bind first so the immutable borrow drops before the borrow_mut below.
|
||||
let res = save_dump(&self.state.lock().borrow().db, &script);
|
||||
match res {
|
||||
// Trailing newline to match the historical file format (writeln per line).
|
||||
let body = if data.is_empty() { String::new() } else { format!("{}\n", data) };
|
||||
match crate::storage::write(&script, &body) {
|
||||
Ok(()) => {
|
||||
self.state.lock().borrow_mut().last_dump = Some(data);
|
||||
out.o(format!("Passwords saved to file {}", script));
|
||||
out.o(format!("Passwords saved to {}", script));
|
||||
}
|
||||
Err(e) => out.e(format!("error: failed to dump passwords to {}: {}", script, e.to_string())),
|
||||
};
|
||||
@@ -434,11 +453,13 @@ impl<'a> LKEval<'a> {
|
||||
None => return,
|
||||
};
|
||||
fn load_lines() -> std::io::Result<HashSet<String>> {
|
||||
let file = fs::File::open(CORRECT_FILE.to_str().unwrap())?;
|
||||
let reader = BufReader::new(file);
|
||||
let content = crate::storage::read(CORRECT_FILE.to_str().unwrap())?;
|
||||
let mut lines = HashSet::new();
|
||||
for line in reader.lines() {
|
||||
lines.insert(line?.trim().to_owned());
|
||||
for line in content.lines() {
|
||||
let line = line.trim();
|
||||
if !line.is_empty() {
|
||||
lines.insert(line.to_owned());
|
||||
}
|
||||
}
|
||||
Ok(lines)
|
||||
}
|
||||
@@ -469,12 +490,12 @@ impl<'a> LKEval<'a> {
|
||||
data.remove(&encpwd);
|
||||
}
|
||||
fn save_lines(data: &HashSet<String>) -> std::io::Result<()> {
|
||||
let file = fs::File::create(CORRECT_FILE.to_str().unwrap())?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
let mut content = String::new();
|
||||
for entry in data {
|
||||
writeln!(writer, "{}", entry)?;
|
||||
content.push_str(entry);
|
||||
content.push('\n');
|
||||
}
|
||||
Ok(())
|
||||
crate::storage::write(CORRECT_FILE.to_str().unwrap(), &content)
|
||||
}
|
||||
match save_lines(&data) {
|
||||
Ok(()) => out.o(format!(
|
||||
|
||||
@@ -11,5 +11,6 @@ pub mod parser;
|
||||
pub mod password;
|
||||
pub mod repl;
|
||||
pub mod skey;
|
||||
pub mod storage;
|
||||
pub mod structs;
|
||||
pub mod utils;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
//! Key -> value persistence with a per-target backend.
|
||||
//!
|
||||
//! - Native: the key is a filesystem path; backed by `std::fs`.
|
||||
//! - WASM: the key is a localStorage key; backed by JS imports
|
||||
//! `hel_storage_get` / `hel_storage_set` (provided by the host page).
|
||||
//!
|
||||
//! This lets the same command code (`init`, `source`, `dump`/`save`, `correct`)
|
||||
//! persist to files on the CLI and to the browser's localStorage on the web,
|
||||
//! with no per-call-site branching.
|
||||
|
||||
use std::io;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn read(key: &str) -> io::Result<String> {
|
||||
std::fs::read_to_string(key)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn write(key: &str, data: &str) -> io::Result<()> {
|
||||
std::fs::write(key, data)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod imp {
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_name = hel_storage_get)]
|
||||
pub fn get(key: &str) -> Option<String>;
|
||||
#[wasm_bindgen(js_name = hel_storage_set)]
|
||||
pub fn set(key: &str, val: &str);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn read(key: &str) -> io::Result<String> {
|
||||
match imp::get(key) {
|
||||
Some(v) => Ok(v),
|
||||
None => Err(io::Error::new(io::ErrorKind::NotFound, "key not found in localStorage")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn write(key: &str, data: &str) -> io::Result<()> {
|
||||
imp::set(key, data);
|
||||
Ok(())
|
||||
}
|
||||
+1
-1
@@ -392,7 +392,7 @@ pub fn init() -> Option<LKRead> {
|
||||
let lk = Arc::new(ReentrantMutex::new(RefCell::new(LK::new())));
|
||||
let editor = Editor::new();
|
||||
|
||||
match std::fs::read_to_string(INIT_FILE.to_str().unwrap()) {
|
||||
match crate::storage::read(INIT_FILE.to_str().unwrap()) {
|
||||
Ok(script) => match command_parser::script(&script) {
|
||||
Ok(cmd_list) => {
|
||||
for cmd in cmd_list {
|
||||
|
||||
+15
-14
@@ -150,25 +150,32 @@ pub mod editor {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod editor {
|
||||
use crate::structs::LKErr;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
// Mirror the unix editor's contract so repl.rs (which holds an `EditorRef`
|
||||
// and calls `.lock()`) compiles unchanged under wasm.
|
||||
pub type EditorRef = Arc<Mutex<Editor>>;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_name = hel_read_password)]
|
||||
fn extern_read_password(prompt: &str);
|
||||
|
||||
#[wasm_bindgen(js_name = hel_current_password)]
|
||||
fn extern_current_password() -> Option<String>;
|
||||
// Synchronous: the host page returns the current master-password value.
|
||||
// (The old read/poll pair used thread::sleep, which deadlocks the single
|
||||
// browser thread — never use blocking polling under wasm.)
|
||||
#[wasm_bindgen(js_name = hel_get_password)]
|
||||
fn extern_get_password(prompt: &str) -> String;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Editor {
|
||||
#[allow(dead_code)]
|
||||
history: Vec<String>,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn new() -> Self {
|
||||
Self { history: vec![] }
|
||||
pub fn new() -> EditorRef {
|
||||
Arc::new(Mutex::new(Self { history: vec![] }))
|
||||
}
|
||||
|
||||
pub fn clear_history(&mut self) {
|
||||
@@ -193,13 +200,7 @@ pub mod editor {
|
||||
}
|
||||
|
||||
pub fn password(prompt: String) -> std::io::Result<String> {
|
||||
extern_read_password(&prompt);
|
||||
loop {
|
||||
match extern_current_password() {
|
||||
Some(p) => return Ok(p),
|
||||
None => std::thread::sleep(std::time::Duration::from_millis(100)),
|
||||
}
|
||||
}
|
||||
Ok(extern_get_password(&prompt))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -16,7 +16,4 @@ hel = { version = "0.1.0", path = "../hel" }
|
||||
lazy_static = "1.4.0"
|
||||
wasm-bindgen = "0.2.83"
|
||||
parking_lot = "0.12.1"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.4"
|
||||
features = [ 'Document' ]
|
||||
console_error_panic_hook = "0.1"
|
||||
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
# Build the helwasm web bundle into helwasm/pkg/ (committed so GitHub Pages can
|
||||
# serve the static dir directly, no CI). Requires the wasm32 target and a
|
||||
# wasm-bindgen CLI matching the wasm-bindgen crate version:
|
||||
# rustup target add wasm32-unknown-unknown
|
||||
# cargo install wasm-bindgen-cli --version <crate version>
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
cargo build --target wasm32-unknown-unknown -p helwasm --release
|
||||
wasm-bindgen --target web --out-dir helwasm/pkg \
|
||||
target/wasm32-unknown-unknown/release/helwasm.wasm
|
||||
echo "built helwasm/pkg/ (helwasm.js + helwasm_bg.wasm)"
|
||||
+243
-158
@@ -1,172 +1,257 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: #000;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
color: #fff;
|
||||
overflow-y: scroll;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: #0f0;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.input {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: #fff;
|
||||
outline: none;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
label {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.terminal p,
|
||||
.prompt,
|
||||
.input {
|
||||
font-family: monospace;
|
||||
font-size: 16px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#passwordPrompt {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
tranform: translate(-50%, -50%);
|
||||
backrground-color: white;
|
||||
border: 1px solid black;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#1b4161" />
|
||||
<title>LesS/KEY — password generator</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<header class="site">
|
||||
<div class="wrap nav">
|
||||
<a class="brand" href="./">
|
||||
<span class="enso" aria-hidden="true"></span>
|
||||
<span class="wordmark">LesS/KEY</span>
|
||||
</a>
|
||||
<span class="nav-spacer"></span>
|
||||
<button class="btn ghost" id="importBtn">Import</button>
|
||||
<button class="btn ghost" id="exportBtn">Export</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="wrap">
|
||||
<!-- Quick generate -->
|
||||
<section class="card">
|
||||
<div class="eyebrow">Generate</div>
|
||||
<h2>Quick password</h2>
|
||||
<p class="hint">
|
||||
Deterministic — the same name + master always regenerate the same
|
||||
password. Nothing secret is stored; this entry is not added to your
|
||||
catalog.
|
||||
</p>
|
||||
<div class="grid2">
|
||||
<div class="field">
|
||||
<label for="qname">Name (+ optional rules)</label>
|
||||
<input type="text" id="qname" placeholder="exa91 or exa91 20R 99 2020-01-01" autocomplete="off" spellcheck="false" autofocus />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="master">Master password</label>
|
||||
<input type="password" id="master" placeholder="your master phrase" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button class="btn" id="show">Show</button>
|
||||
<button class="btn ghost" id="copy">Copy</button>
|
||||
</div>
|
||||
<div class="secret-line">
|
||||
<span class="tag">result</span>
|
||||
<span id="qsecret"><span class="secret"></span></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Console -->
|
||||
<section class="card">
|
||||
<div class="eyebrow">Console</div>
|
||||
<h2>All commands</h2>
|
||||
<p class="hint">
|
||||
The full hel command line — <code>ls</code>, <code>add</code>,
|
||||
<code>enc</code>, <code>gen</code>, <code>comment</code>,
|
||||
<code>correct</code>, <code>help</code>, … Your catalog persists in this
|
||||
browser (localStorage). <code>enc</code>/<code>gen</code> output is
|
||||
masked — click to reveal.
|
||||
</p>
|
||||
<div class="console-out" id="cout"></div>
|
||||
<div class="console-in">
|
||||
<span class="prompt">></span>
|
||||
<input type="text" id="cin" placeholder="type a command and press Enter" autocomplete="off" spellcheck="false" />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="foot wrap">
|
||||
Runs the real hel core compiled to WebAssembly — same generator as the CLI.
|
||||
</footer>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<!-- Import modal -->
|
||||
<div class="modal-bg" id="importModal">
|
||||
<div class="modal">
|
||||
<h3>Import catalog</h3>
|
||||
<p class="hint">Paste your catalog (e.g. the text of the Notion page). Existing entries with the same name are replaced.</p>
|
||||
<textarea id="importText" placeholder="add ..."></textarea>
|
||||
<div class="row">
|
||||
<button class="btn" id="importDo">Import</button>
|
||||
<button class="btn ghost" id="importCancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Export modal -->
|
||||
<div class="modal-bg" id="exportModal">
|
||||
<div class="modal">
|
||||
<h3>Export catalog</h3>
|
||||
<p class="hint">Copy this and paste it back into your Notion page.</p>
|
||||
<textarea id="exportText" readonly></textarea>
|
||||
<div class="row">
|
||||
<button class="btn" id="exportCopy">Copy</button>
|
||||
<button class="btn ghost" id="exportClose">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import init, { hel_init, hel_command } from "./pkg/helwasm.js";
|
||||
init().then(() => {
|
||||
window.hel = {
|
||||
hel_init: hel_init,
|
||||
hel_command: hel_command,
|
||||
import init, { hel_init, hel_command, hel_load_script } from "./pkg/helwasm.js";
|
||||
|
||||
const $ = (s) => document.querySelector(s);
|
||||
const masterEl = () => document.getElementById("master");
|
||||
const CATALOG_KEY = "hel_catalog";
|
||||
|
||||
// ---- host imports (wasm calls these by bare name → must be globals) ----
|
||||
window.hel_get_password = () => (masterEl() ? masterEl().value : "") || "";
|
||||
window.hel_rnd_range = (s, e) => {
|
||||
if (e <= s) return s;
|
||||
const r = crypto.getRandomValues(new Uint32Array(1))[0] / 4294967296;
|
||||
return s + Math.floor(r * (e - s));
|
||||
};
|
||||
window.hel_storage_get = (k) => localStorage.getItem("hel:" + k);
|
||||
window.hel_storage_set = (k, v) => localStorage.setItem("hel:" + k, v);
|
||||
|
||||
// ---- helpers ----
|
||||
const persist = () => hel_command("save " + CATALOG_KEY);
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const isSecretCmd = (cmd) => {
|
||||
const v = cmd.trim().split(/\s+/)[0];
|
||||
return v === "enc" || v === "gen";
|
||||
};
|
||||
|
||||
let toastT;
|
||||
function toast(msg) {
|
||||
const t = $("#toast");
|
||||
t.textContent = msg;
|
||||
t.classList.add("show");
|
||||
clearTimeout(toastT);
|
||||
toastT = setTimeout(() => t.classList.remove("show"), 1600);
|
||||
}
|
||||
|
||||
async function copyText(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
toast("Copied");
|
||||
} catch {
|
||||
toast("Copy failed");
|
||||
}
|
||||
}
|
||||
|
||||
function secretSpan(text) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "secret";
|
||||
s.textContent = text;
|
||||
s.title = "click to reveal";
|
||||
s.addEventListener("click", () => s.classList.toggle("revealed"));
|
||||
return s;
|
||||
}
|
||||
|
||||
// ---- quick generate (stateless: never pollutes the stored catalog) ----
|
||||
let lastSecret = "";
|
||||
function quickShow() {
|
||||
const spec = $("#qname").value.trim();
|
||||
if (!spec) return toast("Enter a name");
|
||||
if (!masterEl().value) return toast("Enter the master password");
|
||||
const name = spec.split(/\s+/)[0];
|
||||
let out = hel_command("enc " + name);
|
||||
if (/not found/.test(out)) {
|
||||
hel_command("add " + spec);
|
||||
out = hel_command("enc " + name);
|
||||
hel_command("rm " + name);
|
||||
}
|
||||
const pw = out.split("\n").filter((l) => l && !/^(warning|error):/.test(l)).pop() || "";
|
||||
lastSecret = pw;
|
||||
const slot = $("#qsecret");
|
||||
slot.innerHTML = "";
|
||||
slot.appendChild(secretSpan(pw));
|
||||
if (!pw) toast("No output");
|
||||
}
|
||||
|
||||
// ---- console ----
|
||||
function appendLine(html, cls) {
|
||||
const out = $("#cout");
|
||||
const div = document.createElement("div");
|
||||
if (cls) div.className = cls;
|
||||
div.innerHTML = html;
|
||||
out.appendChild(div);
|
||||
out.scrollTop = out.scrollHeight;
|
||||
}
|
||||
function appendSecret(text) {
|
||||
const out = $("#cout");
|
||||
const div = document.createElement("div");
|
||||
div.appendChild(secretSpan(text));
|
||||
out.appendChild(div);
|
||||
out.scrollTop = out.scrollHeight;
|
||||
}
|
||||
function consoleRun(cmd) {
|
||||
appendLine('<span class="cmd">> ' + esc(cmd) + "</span>");
|
||||
const out = hel_command(cmd);
|
||||
persist();
|
||||
if (out) {
|
||||
const secret = isSecretCmd(cmd);
|
||||
for (const line of out.split("\n")) {
|
||||
if (/^(warning|error):/.test(line)) appendLine(esc(line), "err");
|
||||
else if (secret && line.trim() && !line.startsWith("add ")) appendSecret(line);
|
||||
else appendLine(esc(line) || " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- boot ----
|
||||
async function boot() {
|
||||
await init();
|
||||
hel_init();
|
||||
if (localStorage.getItem("hel:" + CATALOG_KEY)) hel_command("source " + CATALOG_KEY);
|
||||
|
||||
$("#show").onclick = quickShow;
|
||||
$("#copy").onclick = () => (lastSecret ? copyText(lastSecret) : toast("Nothing to copy"));
|
||||
$("#qname").addEventListener("keydown", (e) => e.key === "Enter" && quickShow());
|
||||
|
||||
const ci = $("#cin");
|
||||
ci.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
const v = ci.value;
|
||||
if (v.trim()) consoleRun(v);
|
||||
ci.value = "";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script type="text/babel">
|
||||
function Terminal() {
|
||||
const prompt = "> ";
|
||||
const [input, setInput] = React.useState("");
|
||||
const [output, setOutput] = React.useState([]);
|
||||
const [passwordPrompt, setPasswordPrompt] = React.useState(undefined);
|
||||
const [passwordInput, setPasswordInput] = React.useState("");
|
||||
const outputRef = React.useRef(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
outputRef.current.scrollIntoView({ behaviour: "smooth" });
|
||||
}, [output]);
|
||||
|
||||
function handleInput(event) {
|
||||
setInput(event.target.value);
|
||||
$("#importBtn").onclick = () => $("#importModal").classList.add("show");
|
||||
$("#importCancel").onclick = () => $("#importModal").classList.remove("show");
|
||||
$("#importDo").onclick = () => {
|
||||
const txt = $("#importText").value;
|
||||
if (txt.trim()) {
|
||||
hel_load_script(txt);
|
||||
persist();
|
||||
toast("Imported");
|
||||
appendLine('<span class="muted"># imported catalog</span>');
|
||||
}
|
||||
$("#importModal").classList.remove("show");
|
||||
$("#importText").value = "";
|
||||
};
|
||||
$("#exportBtn").onclick = () => {
|
||||
$("#exportText").value = hel_command("dump -");
|
||||
$("#exportModal").classList.add("show");
|
||||
};
|
||||
$("#exportClose").onclick = () => $("#exportModal").classList.remove("show");
|
||||
$("#exportCopy").onclick = () => copyText($("#exportText").value);
|
||||
|
||||
function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
const result = executeCommand(input);
|
||||
setOutput([...output, { prompt: prompt, command: input }, { prompt: "", command: result }]);
|
||||
setInput("");
|
||||
appendLine('<span class="muted"># LesS/KEY ready — runs the real hel core via WASM. Try: help</span>');
|
||||
}
|
||||
|
||||
function handlePasswordInput(event) {
|
||||
setPasswordInput(event.target.value);
|
||||
}
|
||||
|
||||
function handlePasswordSubmit(event) {
|
||||
window.hel_current_password_value = passwordInput;
|
||||
setPasswordPrompt(undefined);
|
||||
setPasswordInput("");
|
||||
}
|
||||
|
||||
function helReadPassword(prompt) {
|
||||
window.hel_current_password_value = null;
|
||||
setPasswordPrompt(prompt);
|
||||
}
|
||||
window.hel_read_password = helReadPassword;
|
||||
|
||||
function helCurrentPassword(prompt) {
|
||||
return window.hel_current_password_value;
|
||||
}
|
||||
window.hel_current_password = helCurrentPassword;
|
||||
|
||||
function executeCommand(command) {
|
||||
return window.hel.hel_command(command);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="terminal">
|
||||
{output.map(({ prompt, command }, index) => (<p key={index}>{prompt && (<span className="prompt">{prompt}</span>)}{command}</p>))}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label>
|
||||
<span className="prompt" ref={outputRef}>{prompt}</span>
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={handleInput}
|
||||
className="input"
|
||||
autoFocus
|
||||
/>
|
||||
</label>
|
||||
</form>
|
||||
{passwordPrompt !== undefined && (<form onSubmit={handlePasswordSubmit}><label><span className="prompt">{passwordPrompt}</span><input type="password" value={passwordInput} onChange={handlePasswordInput} className="input" autoFocus /></label></form>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const container = document.getElementById('root');
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<Terminal />);
|
||||
boot();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Vendored
+56
@@ -0,0 +1,56 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* Run a single hel command line and return its combined output.
|
||||
*/
|
||||
export function hel_command(cmd: string): string;
|
||||
|
||||
/**
|
||||
* Call once at page load: routes Rust panics to the browser console with a
|
||||
* readable message + stack instead of an opaque "unreachable" trap.
|
||||
*/
|
||||
export function hel_init(): void;
|
||||
|
||||
/**
|
||||
* Run a whole multi-line script (every `add …` line, `set …`, etc.) against the
|
||||
* shared state in one call. Used to bulk-import a pasted catalog (e.g. the text
|
||||
* of the Notion page) and to load the persisted catalog from localStorage.
|
||||
*/
|
||||
export function hel_load_script(script: string): string;
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly hel_command: (a: number, b: number) => [number, number];
|
||||
readonly hel_init: () => void;
|
||||
readonly hel_load_script: (a: number, b: number) => [number, number];
|
||||
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
readonly __wbindgen_externrefs: WebAssembly.Table;
|
||||
readonly __wbindgen_start: () => void;
|
||||
}
|
||||
|
||||
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||
|
||||
/**
|
||||
* Instantiates the given `module`, which can either be bytes or
|
||||
* a precompiled `WebAssembly.Module`.
|
||||
*
|
||||
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {InitOutput}
|
||||
*/
|
||||
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||
|
||||
/**
|
||||
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||
*
|
||||
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {Promise<InitOutput>}
|
||||
*/
|
||||
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
||||
@@ -0,0 +1,320 @@
|
||||
/* @ts-self-types="./helwasm.d.ts" */
|
||||
|
||||
/**
|
||||
* Run a single hel command line and return its combined output.
|
||||
* @param {string} cmd
|
||||
* @returns {string}
|
||||
*/
|
||||
export function hel_command(cmd) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(cmd, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.hel_command(ptr0, len0);
|
||||
deferred2_0 = ret[0];
|
||||
deferred2_1 = ret[1];
|
||||
return getStringFromWasm0(ret[0], ret[1]);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call once at page load: routes Rust panics to the browser console with a
|
||||
* readable message + stack instead of an opaque "unreachable" trap.
|
||||
*/
|
||||
export function hel_init() {
|
||||
wasm.hel_init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a whole multi-line script (every `add …` line, `set …`, etc.) against the
|
||||
* shared state in one call. Used to bulk-import a pasted catalog (e.g. the text
|
||||
* of the Notion page) and to load the persisted catalog from localStorage.
|
||||
* @param {string} script
|
||||
* @returns {string}
|
||||
*/
|
||||
export function hel_load_script(script) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(script, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.hel_load_script(ptr0, len0);
|
||||
deferred2_0 = ret[0];
|
||||
deferred2_1 = ret[1];
|
||||
return getStringFromWasm0(ret[0], ret[1]);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
function __wbg_get_imports() {
|
||||
const import0 = {
|
||||
__proto__: null,
|
||||
__wbg___wbindgen_throw_1506f2235d1bdba0: function(arg0, arg1) {
|
||||
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||
},
|
||||
__wbg_error_a6fa202b58aa1cd3: function(arg0, arg1) {
|
||||
let deferred0_0;
|
||||
let deferred0_1;
|
||||
try {
|
||||
deferred0_0 = arg0;
|
||||
deferred0_1 = arg1;
|
||||
console.error(getStringFromWasm0(arg0, arg1));
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
|
||||
}
|
||||
},
|
||||
__wbg_getTime_00b3f7db575e4ef5: function(arg0) {
|
||||
const ret = arg0.getTime();
|
||||
return ret;
|
||||
},
|
||||
__wbg_getTimezoneOffset_08e2892156231088: function(arg0) {
|
||||
const ret = arg0.getTimezoneOffset();
|
||||
return ret;
|
||||
},
|
||||
__wbg_hel_get_password_271a2beac04c29db: function(arg0, arg1, arg2) {
|
||||
const ret = hel_get_password(getStringFromWasm0(arg1, arg2));
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
},
|
||||
__wbg_hel_rnd_range_d01857a65a482c6d: function(arg0, arg1) {
|
||||
const ret = hel_rnd_range(arg0 >>> 0, arg1 >>> 0);
|
||||
return ret;
|
||||
},
|
||||
__wbg_hel_storage_get_9915f302e24bdacb: function(arg0, arg1, arg2) {
|
||||
const ret = hel_storage_get(getStringFromWasm0(arg1, arg2));
|
||||
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
},
|
||||
__wbg_hel_storage_set_78052521669832fa: function(arg0, arg1, arg2, arg3) {
|
||||
hel_storage_set(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3));
|
||||
},
|
||||
__wbg_new_0_445c13a750296eb6: function() {
|
||||
const ret = new Date();
|
||||
return ret;
|
||||
},
|
||||
__wbg_new_227d7c05414eb861: function() {
|
||||
const ret = new Error();
|
||||
return ret;
|
||||
},
|
||||
__wbg_new_6d75fd236f920a62: function(arg0) {
|
||||
const ret = new Date(arg0);
|
||||
return ret;
|
||||
},
|
||||
__wbg_stack_3b0d974bbf31e44f: function(arg0, arg1) {
|
||||
const ret = arg1.stack;
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
},
|
||||
__wbindgen_cast_0000000000000001: function(arg0) {
|
||||
// Cast intrinsic for `F64 -> Externref`.
|
||||
const ret = arg0;
|
||||
return ret;
|
||||
},
|
||||
__wbindgen_init_externref_table: function() {
|
||||
const table = wasm.__wbindgen_externrefs;
|
||||
const offset = table.grow(4);
|
||||
table.set(0, undefined);
|
||||
table.set(offset + 0, undefined);
|
||||
table.set(offset + 1, null);
|
||||
table.set(offset + 2, true);
|
||||
table.set(offset + 3, false);
|
||||
},
|
||||
};
|
||||
return {
|
||||
__proto__: null,
|
||||
"./helwasm_bg.js": import0,
|
||||
};
|
||||
}
|
||||
|
||||
let cachedDataViewMemory0 = null;
|
||||
function getDataViewMemory0() {
|
||||
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
|
||||
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
||||
}
|
||||
return cachedDataViewMemory0;
|
||||
}
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
return decodeText(ptr >>> 0, len);
|
||||
}
|
||||
|
||||
let cachedUint8ArrayMemory0 = null;
|
||||
function getUint8ArrayMemory0() {
|
||||
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachedUint8ArrayMemory0;
|
||||
}
|
||||
|
||||
function isLikeNone(x) {
|
||||
return x === undefined || x === null;
|
||||
}
|
||||
|
||||
function passStringToWasm0(arg, malloc, realloc) {
|
||||
if (realloc === undefined) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
const ptr = malloc(buf.length, 1) >>> 0;
|
||||
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||
WASM_VECTOR_LEN = buf.length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let len = arg.length;
|
||||
let ptr = malloc(len, 1) >>> 0;
|
||||
|
||||
const mem = getUint8ArrayMemory0();
|
||||
|
||||
let offset = 0;
|
||||
|
||||
for (; offset < len; offset++) {
|
||||
const code = arg.charCodeAt(offset);
|
||||
if (code > 0x7F) break;
|
||||
mem[ptr + offset] = code;
|
||||
}
|
||||
if (offset !== len) {
|
||||
if (offset !== 0) {
|
||||
arg = arg.slice(offset);
|
||||
}
|
||||
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||
const ret = cachedTextEncoder.encodeInto(arg, view);
|
||||
|
||||
offset += ret.written;
|
||||
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||
}
|
||||
|
||||
WASM_VECTOR_LEN = offset;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||
cachedTextDecoder.decode();
|
||||
const MAX_SAFARI_DECODE_BYTES = 2146435072;
|
||||
let numBytesDecoded = 0;
|
||||
function decodeText(ptr, len) {
|
||||
numBytesDecoded += len;
|
||||
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
|
||||
cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||
cachedTextDecoder.decode();
|
||||
numBytesDecoded = len;
|
||||
}
|
||||
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
const cachedTextEncoder = new TextEncoder();
|
||||
|
||||
if (!('encodeInto' in cachedTextEncoder)) {
|
||||
cachedTextEncoder.encodeInto = function (arg, view) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
view.set(buf);
|
||||
return {
|
||||
read: arg.length,
|
||||
written: buf.length
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
let WASM_VECTOR_LEN = 0;
|
||||
|
||||
let wasmModule, wasmInstance, wasm;
|
||||
function __wbg_finalize_init(instance, module) {
|
||||
wasmInstance = instance;
|
||||
wasm = instance.exports;
|
||||
wasmModule = module;
|
||||
cachedDataViewMemory0 = null;
|
||||
cachedUint8ArrayMemory0 = null;
|
||||
wasm.__wbindgen_start();
|
||||
return wasm;
|
||||
}
|
||||
|
||||
async function __wbg_load(module, imports) {
|
||||
if (typeof Response === 'function' && module instanceof Response) {
|
||||
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||
try {
|
||||
return await WebAssembly.instantiateStreaming(module, imports);
|
||||
} catch (e) {
|
||||
const validResponse = module.ok && expectedResponseType(module.type);
|
||||
|
||||
if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
|
||||
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
||||
|
||||
} else { throw e; }
|
||||
}
|
||||
}
|
||||
|
||||
const bytes = await module.arrayBuffer();
|
||||
return await WebAssembly.instantiate(bytes, imports);
|
||||
} else {
|
||||
const instance = await WebAssembly.instantiate(module, imports);
|
||||
|
||||
if (instance instanceof WebAssembly.Instance) {
|
||||
return { instance, module };
|
||||
} else {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
function expectedResponseType(type) {
|
||||
switch (type) {
|
||||
case 'basic': case 'cors': case 'default': return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function initSync(module) {
|
||||
if (wasm !== undefined) return wasm;
|
||||
|
||||
|
||||
if (module !== undefined) {
|
||||
if (Object.getPrototypeOf(module) === Object.prototype) {
|
||||
({module} = module)
|
||||
} else {
|
||||
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
|
||||
}
|
||||
}
|
||||
|
||||
const imports = __wbg_get_imports();
|
||||
if (!(module instanceof WebAssembly.Module)) {
|
||||
module = new WebAssembly.Module(module);
|
||||
}
|
||||
const instance = new WebAssembly.Instance(module, imports);
|
||||
return __wbg_finalize_init(instance, module);
|
||||
}
|
||||
|
||||
async function __wbg_init(module_or_path) {
|
||||
if (wasm !== undefined) return wasm;
|
||||
|
||||
|
||||
if (module_or_path !== undefined) {
|
||||
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
|
||||
({module_or_path} = module_or_path)
|
||||
} else {
|
||||
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
|
||||
}
|
||||
}
|
||||
|
||||
if (module_or_path === undefined) {
|
||||
module_or_path = new URL('helwasm_bg.wasm', import.meta.url);
|
||||
}
|
||||
const imports = __wbg_get_imports();
|
||||
|
||||
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
|
||||
module_or_path = fetch(module_or_path);
|
||||
}
|
||||
|
||||
const { instance, module } = await __wbg_load(await module_or_path, imports);
|
||||
|
||||
return __wbg_finalize_init(instance, module);
|
||||
}
|
||||
|
||||
export { initSync, __wbg_init as default };
|
||||
Binary file not shown.
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const memory: WebAssembly.Memory;
|
||||
export const hel_command: (a: number, b: number) => [number, number];
|
||||
export const hel_init: () => void;
|
||||
export const hel_load_script: (a: number, b: number) => [number, number];
|
||||
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
export const __wbindgen_externrefs: WebAssembly.Table;
|
||||
export const __wbindgen_start: () => void;
|
||||
@@ -1,22 +1,42 @@
|
||||
use hel::lk::{LK, LKRef};
|
||||
use hel::repl::LKRead;
|
||||
use hel::utils::editor::Editor;
|
||||
use std::sync::Arc;
|
||||
use hel::parser::command_parser;
|
||||
use hel::repl::{LKEval, LKRead};
|
||||
use hel::structs::LKOut;
|
||||
use hel::utils::editor::{password, Editor};
|
||||
use parking_lot::ReentrantMutex;
|
||||
use std::cell::RefCell;
|
||||
use std::sync::Arc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
lazy_static! {
|
||||
static ref STATE: LKRef = Arc::new(ReentrantMutex::new(RefCell::new(LK::new())));
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Run a single hel command line and return its combined output.
|
||||
#[wasm_bindgen]
|
||||
pub fn hel_command(cmd: String) -> String {
|
||||
let editor = Editor::new();
|
||||
let mut lkread = LKRead::new(editor, "> ".to_string(), STATE.clone());
|
||||
lkread.input = Some(cmd.to_string());
|
||||
lkread.input = Some(cmd);
|
||||
let lkeval = lkread.read();
|
||||
let lkprint = lkeval.eval();
|
||||
lkprint.out.output().join("\n")
|
||||
}
|
||||
|
||||
/// Run a whole multi-line script (every `add …` line, `set …`, etc.) against the
|
||||
/// shared state in one call. Used to bulk-import a pasted catalog (e.g. the text
|
||||
/// of the Notion page) and to load the persisted catalog from localStorage.
|
||||
#[wasm_bindgen]
|
||||
pub fn hel_load_script(script: String) -> String {
|
||||
let out = LKOut::new();
|
||||
match command_parser::script(&script) {
|
||||
Ok(cmds) => {
|
||||
for cmd in cmds {
|
||||
let print = LKEval::new(Editor::new(), cmd, STATE.clone(), password).eval();
|
||||
print.out.copy(&out);
|
||||
}
|
||||
}
|
||||
Err(e) => out.e(format!("error: {}", e)),
|
||||
}
|
||||
out.output().join("\n")
|
||||
}
|
||||
|
||||
+3
-5
@@ -4,13 +4,11 @@ extern crate hel;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn ok_add(a: i32, b: i32) -> i32 {
|
||||
a + b + 1
|
||||
}
|
||||
|
||||
mod hel_state;
|
||||
|
||||
/// Call once at page load: routes Rust panics to the browser console with a
|
||||
/// readable message + stack instead of an opaque "unreachable" trap.
|
||||
#[wasm_bindgen]
|
||||
pub fn hel_init() {
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/* LesS/KEY web app — visual language mirrored from kaizenkodo.org
|
||||
(~/Repos/Identity/website/main.css): washi-paper cream, ocean blue, hanko red,
|
||||
Poppins / Inter / JetBrains Mono. Light, responsive, restrained. */
|
||||
|
||||
:root {
|
||||
--cream: #f4f0e8;
|
||||
--paper: #fbf8f2;
|
||||
--ink: #15181b;
|
||||
--muted: #6a675e;
|
||||
--line: #ddd6c6;
|
||||
--ocean: #1b4161;
|
||||
--ocean-2: #2f6286;
|
||||
--deep: #0a2540;
|
||||
--seal: #e23b2e;
|
||||
--disp: "Poppins", system-ui, sans-serif;
|
||||
--body: "Inter", system-ui, sans-serif;
|
||||
--mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
||||
--maxw: 880px;
|
||||
--radius: 14px;
|
||||
}
|
||||
|
||||
/* Masked-secret font: dots glyphs so the real chars stay selectable/copyable.
|
||||
WebKit/Blink use -webkit-text-security; Firefox falls back to this font. */
|
||||
@font-face {
|
||||
font-family: "text-security-disc";
|
||||
src: url("https://cdn.jsdelivr.net/npm/text-security/dist/text-security-disc.woff2") format("woff2"),
|
||||
url("https://cdn.jsdelivr.net/npm/text-security/dist/text-security-disc.woff") format("woff");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html { height: 100%; }
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
font-family: var(--body);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: var(--ink);
|
||||
background-color: var(--cream);
|
||||
background-image:
|
||||
linear-gradient(rgba(27, 65, 97, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(27, 65, 97, 0.05) 1px, transparent 1px);
|
||||
background-size: 48px 48px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 20px; }
|
||||
|
||||
/* Header */
|
||||
.site {
|
||||
position: sticky; top: 0; z-index: 50;
|
||||
background: rgba(244, 240, 232, 0.82);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.nav { display: flex; align-items: center; gap: 14px; height: 64px; }
|
||||
.brand { display: inline-flex; align-items: center; gap: 11px; text-decoration: none; }
|
||||
.enso {
|
||||
width: 34px; height: 34px; border-radius: 50%;
|
||||
border: 3px solid var(--ocean); border-right-color: transparent;
|
||||
transform: rotate(-20deg);
|
||||
}
|
||||
.wordmark { font-family: var(--disp); font-weight: 700; font-size: 21px; color: var(--ocean); letter-spacing: -0.01em; }
|
||||
.nav-spacer { flex: 1; }
|
||||
.nav .ghost { font-family: var(--mono); font-size: 12px; }
|
||||
|
||||
/* Eyebrow label */
|
||||
.eyebrow {
|
||||
font-family: var(--mono); font-size: 12px; letter-spacing: 0.16em;
|
||||
text-transform: uppercase; color: var(--ocean);
|
||||
display: inline-flex; align-items: center; gap: 9px; margin-bottom: 14px;
|
||||
}
|
||||
.eyebrow::before {
|
||||
content: ""; width: 7px; height: 7px; border-radius: 50%;
|
||||
background: var(--seal); box-shadow: 0 0 0 3px rgba(226, 59, 46, 0.16);
|
||||
}
|
||||
|
||||
main { padding: 30px 0 64px; display: grid; gap: 22px; }
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: var(--paper); border: 1px solid var(--line);
|
||||
border-radius: var(--radius); padding: 22px;
|
||||
}
|
||||
.card h2 { font-family: var(--disp); font-weight: 600; font-size: clamp(20px, 3vw, 26px); color: var(--ink); margin-bottom: 4px; }
|
||||
.card .hint { color: var(--muted); font-size: 14px; margin-bottom: 16px; }
|
||||
|
||||
/* Form fields */
|
||||
.field { display: flex; flex-direction: column; gap: 6px; }
|
||||
.field label { font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted); }
|
||||
input[type="text"], input[type="password"] {
|
||||
font-family: var(--mono); font-size: 15px; color: var(--ink);
|
||||
background: #fff; border: 1px solid var(--line); border-radius: 10px;
|
||||
padding: 11px 13px; width: 100%; outline: none;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
input:focus { border-color: var(--ocean); box-shadow: 0 0 0 3px rgba(27, 65, 97, 0.12); }
|
||||
|
||||
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
||||
.row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-top: 16px; }
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
|
||||
font-family: var(--disp); font-weight: 600; font-size: 14px;
|
||||
background: var(--ocean); color: var(--cream);
|
||||
border: 1px solid var(--ocean); border-radius: 999px; padding: 10px 20px;
|
||||
transition: transform .15s, background .15s;
|
||||
}
|
||||
.btn:hover { transform: translateY(-1px); background: var(--deep); border-color: var(--deep); }
|
||||
.btn.ghost { background: transparent; color: var(--ocean); }
|
||||
.btn.ghost:hover { background: rgba(27, 65, 97, 0.06); color: var(--ocean); }
|
||||
.btn:active { transform: none; }
|
||||
|
||||
/* Secret rendering: real text in the DOM, visually masked. Click toggles. */
|
||||
.secret-line { margin-top: 16px; display: flex; align-items: baseline; gap: 12px; min-height: 28px; }
|
||||
.secret-line .tag { font-family: var(--mono); font-size: 11px; color: var(--muted); white-space: nowrap; }
|
||||
.secret {
|
||||
font-family: "text-security-disc", var(--mono);
|
||||
-webkit-text-security: disc;
|
||||
font-size: 19px; letter-spacing: 0.06em; color: var(--ink);
|
||||
cursor: pointer; word-break: break-all; user-select: text;
|
||||
border-bottom: 1px dashed var(--line);
|
||||
}
|
||||
.secret.revealed { font-family: var(--mono); -webkit-text-security: none; color: var(--ocean); }
|
||||
.secret:empty::before { content: "—"; color: var(--muted); -webkit-text-security: none; }
|
||||
|
||||
/* Console */
|
||||
.console-out {
|
||||
font-family: var(--mono); font-size: 13.5px; line-height: 1.55;
|
||||
background: #fff; border: 1px solid var(--line); border-radius: 10px;
|
||||
padding: 14px; height: 320px; overflow-y: auto; white-space: pre-wrap; word-break: break-word;
|
||||
}
|
||||
.console-out .cmd { color: var(--ocean); font-weight: 500; }
|
||||
.console-out .err { color: var(--seal); }
|
||||
.console-out .muted { color: var(--muted); }
|
||||
.console-out .secret { font-size: 13.5px; }
|
||||
.console-in { display: flex; align-items: center; gap: 8px; margin-top: 10px;
|
||||
background: #fff; border: 1px solid var(--line); border-radius: 10px; padding: 4px 12px; }
|
||||
.console-in .prompt { font-family: var(--mono); color: var(--ocean); font-weight: 600; }
|
||||
.console-in input { border: none; box-shadow: none; padding: 8px 0; background: transparent; }
|
||||
.console-in input:focus { box-shadow: none; }
|
||||
|
||||
.toast {
|
||||
position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%);
|
||||
background: var(--deep); color: var(--cream); font-family: var(--mono); font-size: 13px;
|
||||
padding: 10px 18px; border-radius: 999px; box-shadow: 0 8px 24px rgba(10, 16, 24, 0.35);
|
||||
opacity: 0; pointer-events: none; transition: opacity .2s; z-index: 80;
|
||||
}
|
||||
.toast.show { opacity: 1; }
|
||||
|
||||
/* Modal (import/export) */
|
||||
.modal-bg {
|
||||
position: fixed; inset: 0; background: rgba(10, 16, 24, 0.45);
|
||||
display: none; align-items: center; justify-content: center; padding: 20px; z-index: 90;
|
||||
}
|
||||
.modal-bg.show { display: flex; }
|
||||
.modal { background: var(--paper); border-radius: 16px; border: 1px solid var(--line);
|
||||
width: 100%; max-width: 620px; padding: 22px; }
|
||||
.modal h3 { font-family: var(--disp); font-weight: 600; margin-bottom: 10px; }
|
||||
.modal textarea {
|
||||
width: 100%; height: 240px; resize: vertical; font-family: var(--mono); font-size: 13px;
|
||||
border: 1px solid var(--line); border-radius: 10px; padding: 12px; outline: none;
|
||||
}
|
||||
.modal textarea:focus { border-color: var(--ocean); }
|
||||
|
||||
.foot { color: var(--muted); font-family: var(--mono); font-size: 12px; text-align: center; padding: 8px 0 40px; }
|
||||
.foot a { color: var(--ocean-2); }
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.grid2 { grid-template-columns: 1fr; }
|
||||
.nav { gap: 8px; }
|
||||
.console-out { height: 260px; }
|
||||
body { font-size: 15px; }
|
||||
}
|
||||
Reference in New Issue
Block a user