bit-twiddler 0.2.1

Cross-platform developer toolbox: bit manipulation, hashing, YAML/JSON/SQL, QR, Markdown, cron, and 40+ more tools — Tauri v2, no Node.js
// Shared utilities (global - no ES module exports for Tauri static serving)
window.escHtml = (s) => String(s)
  .replace(/&/g, '&')
  .replace(/</g, '&lt;')
  .replace(/>/g, '&gt;')
  .replace(/"/g, '&quot;');

window.copyToClipboard = async (text, $btnElement) => {
  try {
    await navigator.clipboard.writeText(text);
    const originalHtml = $btnElement.html();
    $btnElement.text("Copied!");
    setTimeout(() => { $btnElement.html(originalHtml); }, 1500);
  } catch (err) {
    console.error('Failed to copy', err);
  }
};

// Per-tool input history, persisted in localStorage. ↑/↓ recall + a
// clickable dropdown, similar to a shell's history.
class ToolHistory {
  constructor(toolId, maxEntries = 10) {
    this.key = 'bt-history-' + toolId;
    this.maxEntries = maxEntries;
    this.cursor = -1; // -1 = not browsing (on the in-progress draft)
    this.draft = '';
    try {
      this.entries = JSON.parse(localStorage.getItem(this.key) || '[]');
    } catch (e) {
      this.entries = [];
    }
  }

  push(value) {
    if (!value || !value.trim()) return;
    this.entries = this.entries.filter(e => e !== value);
    this.entries.unshift(value);
    if (this.entries.length > this.maxEntries) this.entries.length = this.maxEntries;
    localStorage.setItem(this.key, JSON.stringify(this.entries));
    this.cursor = -1;
  }

  prev(currentDraft) {
    if (this.entries.length === 0) return null;
    if (this.cursor === -1) this.draft = currentDraft;
    if (this.cursor < this.entries.length - 1) this.cursor++;
    return this.entries[this.cursor];
  }

  next() {
    if (this.cursor === -1) return null; // not browsing, nothing to move forward to
    if (this.cursor === 0) { this.cursor = -1; return this.draft; }
    this.cursor--;
    return this.entries[this.cursor];
  }

  getAll() { return this.entries; }

  clear() {
    this.entries = [];
    this.cursor = -1;
    localStorage.removeItem(this.key);
  }
}
window.ToolHistory = ToolHistory;

// Wires a ToolHistory instance to an <input>/<textarea>: ↑/↓ recall
// (only at the caret boundary for multi-line fields, so normal cursor
// movement isn't hijacked), a 🕐 dropdown of past entries, and
// auto-save on blur. Returns the ToolHistory instance.
window.attachToolHistory = (inputEl, toolId, opts = {}) => {
  const $input = $(inputEl);
  const history = new ToolHistory(toolId);
  const getValue = opts.getValue || (() => $input.val());
  const setValue = opts.setValue || ((v) => { $input.val(v).trigger('input'); });

  if (!$input.parent().hasClass('tool-history-input-wrap')) {
    $input.wrap('<div class="tool-history-input-wrap relative flex-1 flex flex-col min-h-0"></div>');
  }
  const $wrap = $input.parent();

  const $btn = $(`<button type="button" class="tool-history-btn absolute top-2 right-2 z-10 text-gray-600 hover:text-gray-300 bg-gray-900/70 rounded p-1 transition-colors" title="Input history"><svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg></button>`);
  const $dropdown = $('<div class="tool-history-dropdown hidden absolute top-9 right-2 z-20 w-64 max-h-56 overflow-y-auto bg-gray-900 border border-gray-700 rounded-lg shadow-2xl text-xs"></div>');
  $wrap.append($btn, $dropdown);

  const renderDropdown = () => {
    const items = history.getAll();
    if (!items.length) {
      $dropdown.html('<div class="px-3 py-2 text-gray-600 italic">No history yet</div>');
      return;
    }
    $dropdown.html(
      items.map((v, i) => `<div class="tool-history-item px-3 py-2 hover:bg-gray-800 cursor-pointer text-gray-300 truncate border-b border-gray-800" data-idx="${i}" title="${window.escHtml(v)}">${window.escHtml(v.slice(0, 80))}</div>`).join('') +
      '<div class="tool-history-clear px-3 py-2 hover:bg-gray-800 cursor-pointer text-rose-400 font-semibold">Clear history</div>'
    );
  };

  $btn.on('click', (e) => {
    e.stopPropagation();
    renderDropdown();
    $dropdown.toggleClass('hidden');
  });

  $dropdown.on('click', function(e) { e.stopPropagation(); });

  $dropdown.on('click', '.tool-history-item', function() {
    setValue(history.getAll()[$(this).data('idx')]);
    $dropdown.addClass('hidden');
  });

  $dropdown.on('click', '.tool-history-clear', function() {
    history.clear();
    renderDropdown();
  });

  $(document).on('click', () => $dropdown.addClass('hidden'));

  $input.on('keydown', function(e) {
    const el = this;
    const isTextarea = el.tagName === 'TEXTAREA';
    const atStart = el.selectionStart === 0 && el.selectionEnd === 0;
    const atEnd = el.selectionStart === el.value.length && el.selectionEnd === el.value.length;

    if (e.key === 'ArrowUp' && (!isTextarea || atStart)) {
      const val = history.prev(getValue());
      if (val !== null) { e.preventDefault(); setValue(val); }
    } else if (e.key === 'ArrowDown' && (!isTextarea || atEnd)) {
      const val = history.next();
      if (val !== null) { e.preventDefault(); setValue(val); }
    }
  });

  $input.on('blur', () => history.push(getValue()));

  return history;
};