mobux 0.5.0

A touch-friendly tmux web UI for unhinged people who run terminal sessions from their phone while walking the dog
import { useEffect, useRef } from 'preact/hooks';
import { signal } from '@preact/signals';
import { localGet, localFetch } from '../../lib/api.js';

// Shell integration installer. Ports shell-integration.js: reads
// GET /api/shell-integration/status, drives Install/Uninstall via
// POST /api/shell-integration/{install,uninstall} with a {shell} body. Acts on
// the host that served the page (host-pinned helpers). The OSC-133 snippets are
// static display content (verbatim from the Rust template).

const SHELLS = [
  {
    id: 'bash',
    rc: '~/.bashrc',
    snippet: `if [ -n "$TMUX" ]; then
    PS0='\\ePtmux;\\e\\e]133;C\\a\\e\\\\'
    PS1='\\[\\ePtmux;\\e\\e]133;D;$?\\a\\e]133;A\\a\\e\\\\\\]'"$PS1"'\\[\\ePtmux;\\e\\e]133;B\\a\\e\\\\\\]'
else
    PS0='\\e]133;C\\a'
    PS1='\\[\\e]133;D;$?\\a\\e]133;A\\a\\]'"$PS1"'\\[\\e]133;B\\a\\]'
fi`,
  },
  {
    id: 'zsh',
    rc: '~/.zshrc',
    snippet: `if [ -n "$TMUX" ]; then
    preexec() { print -Pn '\\ePtmux;\\e\\e]133;C\\a\\e\\\\' }
    precmd()  { print -Pn '\\ePtmux;\\e\\e]133;D;'$?'\\a\\e]133;A\\a\\e\\\\' }
else
    preexec() { print -Pn '\\e]133;C\\a' }
    precmd()  { print -Pn '\\e]133;D;'$?'\\a\\e]133;A\\a' }
fi`,
  },
  {
    id: 'fish',
    rc: '~/.config/fish/config.fish',
    snippet: `if test -n "$TMUX"
    function __mobux_osc133_preexec --on-event fish_preexec
        printf '\\ePtmux;\\e\\e]133;C\\a\\e\\\\'
    end
    function __mobux_osc133_postexec --on-event fish_postexec
        printf '\\ePtmux;\\e\\e]133;D;%s\\a\\e\\\\' $status
    end
    function __mobux_osc133_prompt --on-event fish_prompt
        printf '\\ePtmux;\\e\\e]133;A\\a\\e\\\\'
    end
else
    function __mobux_osc133_preexec --on-event fish_preexec
        printf '\\e]133;C\\a'
    end
    function __mobux_osc133_postexec --on-event fish_postexec
        printf '\\e]133;D;%s\\a' $status
    end
    function __mobux_osc133_prompt --on-event fish_prompt
        printf '\\e]133;A\\a'
    end
end`,
  },
];

const states = signal({}); // { bash: {state, version}, ... }
const status = signal(null); // { msg, ok }

function describe(s) {
  if (!s || !s.state) return { label: 'unknown', cls: '' };
  switch (s.state) {
    case 'not_present':
      return { label: 'rc file not present', cls: 'shell-state--missing' };
    case 'not_installed':
      return { label: 'not installed', cls: 'shell-state--off' };
    case 'installed':
      return { label: `installed v${s.version}`, cls: 'shell-state--on' };
    case 'outdated':
      return { label: `outdated (v${s.version}current)`, cls: 'shell-state--warn' };
    default:
      return { label: s.state, cls: '' };
  }
}

export function ShellIntegrationCard() {
  const t = useRef(null);

  useEffect(() => {
    localGet('/api/shell-integration/status')
      .then((p) => (states.value = p || {}))
      .catch((e) => flash('Load failed: ' + e.message, false));
  }, []);

  const flash = (msg, ok = true) => {
    status.value = { msg, ok };
    clearTimeout(t.current);
    t.current = setTimeout(() => (status.value = null), 2000);
  };

  const act = async (action, shell) => {
    try {
      const res = await localFetch(`/api/shell-integration/${action}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ shell }),
      });
      if (!res.ok) throw new Error(`${action} ${res.status}: ${await res.text()}`);
      states.value = await res.json();
      flash(`${shell}: ${action} ok`);
    } catch (err) {
      flash(`${shell} ${action} failed: ${err.message}`, false);
    }
  };

  return (
    <section class="settings-card" id="shell-integration">
      <h2>Shell integration</h2>
      <p class="settings-lede">
        The reader view classifies prompts and command output deterministically when your shell
        emits{' '}
        <a
          href="https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md"
          target="_blank"
          rel="noopener"
        >
          OSC 133
        </a>{' '}
        (FinalTerm) markers. Click install — mobux appends a managed, fenced block to your rc file
        and keeps a timestamped backup. Nothing outside the fence is touched. The snippet detects{' '}
        <code>$TMUX</code> and wraps OSC 133 in tmux's DCS passthrough envelope.
      </p>

      {SHELLS.map((sh) => {
        const s = states.value[sh.id];
        const d = describe(s);
        const isInstalled = s && s.state === 'installed';
        const isOutdated = s && s.state === 'outdated';
        return (
          <div class="shell-card" data-shell={sh.id} key={sh.id}>
            <div class="shell-card-head">
              <strong>{sh.id}</strong> <code>{sh.rc}</code>
              <span class={'shell-state ' + d.cls} data-role="state">
                {d.label}
              </span>
            </div>
            <div class="shell-card-actions">
              <button type="button" disabled={isInstalled} onClick={() => act('install', sh.id)}>
                {isInstalled ? 'Reinstall' : isOutdated ? 'Update' : 'Install'}
              </button>
              <button
                type="button"
                disabled={!(isInstalled || isOutdated)}
                onClick={() => act('uninstall', sh.id)}
              >
                Uninstall
              </button>
            </div>
            <details class="settings-detail">
              <summary>Show snippet</summary>
              <pre class="settings-snippet">
                <code>{sh.snippet}</code>
              </pre>
            </details>
          </div>
        );
      })}

      {status.value && (
        <div class="settings-status" style={{ color: status.value.ok ? '' : '#f87171' }}>
          {status.value.msg}
        </div>
      )}
      <p class="settings-foot">
        Reload the shell after installing. The fenced block is the contract — mobux only ever
        modifies what's between the fences. A timestamped <code>.mobux.bak.&lt;ts&gt;</code> is
        written next to the rc file before any change.
      </p>
    </section>
  );
}