import { useEffect, useRef } from 'preact/hooks';
import { signal } from '@preact/signals';
import { localGet, localFetch } from '../../lib/api.js';
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({}); const status = signal(null);
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.<ts></code> is
written next to the rc file before any change.
</p>
</section>
);
}