/**
 * OSC (Operating System Command) Types and Parser
 */
import { Buffer } from 'buffer';
import { env } from '../../utils/env.js';
import { execFileNoThrow } from '../../utils/execFileNoThrow.js';
import { BEL, ESC, ESC_TYPE, SEP } from './ansi.js';
export const OSC_PREFIX = ESC + String.fromCharCode(ESC_TYPE.OSC);
/** String Terminator (ESC \) - alternative to BEL for terminating OSC */
export const ST = ESC + '\\';
/** Generate an OSC sequence: ESC ] p1;p2;...;pN <terminator>
 * Uses ST terminator for Kitty (avoids beeps), BEL for others */
export function osc(...parts) {
    const terminator = env.terminal === 'kitty' ? ST : BEL;
    return `${OSC_PREFIX}${parts.join(SEP)}${terminator}`;
}
/**
 * Wrap an escape sequence for terminal multiplexer passthrough.
 * tmux and GNU screen intercept escape sequences; DCS passthrough
 * tunnels them to the outer terminal unmodified.
 *
 * tmux 3.3+ gates this behind `allow-passthrough` (default off). When off,
 * tmux silently drops the whole DCS — no junk, no worse than unwrapped OSC.
 * Users who want passthrough set it in their .tmux.conf; we don't mutate it.
 *
 * Do NOT wrap BEL: raw \x07 triggers tmux's bell-action (window flag);
 * wrapped \x07 is opaque DCS payload and tmux never sees the bell.
 */
export function wrapForMultiplexer(sequence) {
    if (process.env['TMUX']) {
        const escaped = sequence.replaceAll('\x1b', '\x1b\x1b');
        return `\x1bPtmux;${escaped}\x1b\\`;
    }
    if (process.env['STY']) {
        return `\x1bP${sequence}\x1b\\`;
    }
    return sequence;
}
export function getClipboardPath() {
    const nativeAvailable = process.platform === 'darwin' && !process.env['SSH_CONNECTION'];
    if (nativeAvailable)
        return 'native';
    if (process.env['TMUX'])
        return 'tmux-buffer';
    return 'osc52';
}
/**
 * Wrap a payload in tmux's DCS passthrough: ESC P tmux ; <payload> ESC \
 * tmux forwards the payload to the outer terminal, bypassing its own parser.
 * Inner ESCs must be doubled. Requires `set -g allow-passthrough on` in
 * ~/.tmux.conf; without it, tmux silently drops the whole DCS (no regression).
 */
function tmuxPassthrough(payload) {
    return `${ESC}Ptmux;${payload.replaceAll(ESC, ESC + ESC)}${ST}`;
}
/**
 * Load text into tmux's paste buffer via `tmux load-buffer`.
 * -w (tmux 3.2+) propagates to the outer terminal's clipboard via tmux's
 * own OSC 52 emission. -w is dropped for iTerm2: tmux's OSC 52 emission
 * crashes the iTerm2 session over SSH.
 *
 * Returns true if the buffer was loaded successfully.
 */
export async function tmuxLoadBuffer(text) {
    if (!process.env['TMUX'])
        return false;
    const args = process.env['LC_TERMINAL'] === 'iTerm2'
        ? ['load-buffer', '-']
        : ['load-buffer', '-w', '-'];
    const { code } = await execFileNoThrow('tmux', args, {
        input: text,
        useCwd: false,
        timeout: 2000,
    });
    return code === 0;
}
/**
 * OSC 52 clipboard write: ESC ] 52 ; c ; <base64> BEL/ST
 * 'c' selects the clipboard (vs 'p' for primary selection on X11).
 *
 * When inside tmux ($TMUX set), `tmux load-buffer -w -` is the primary
 * path. tmux's buffer is always reachable — works over SSH, survives
 * detach/reattach, immune to stale env vars. The -w flag (tmux 3.2+) tells
 * tmux to also propagate to the outer terminal via its own OSC 52 path,
 * which tmux wraps correctly for the attached client. On older tmux, -w is
 * ignored and the buffer is still loaded. -w is dropped for iTerm2 (#22432)
 * because tmux's own OSC 52 emission (empty selection param: ESC]52;;b64)
 * crashes iTerm2 over SSH.
 *
 * After load-buffer succeeds, we ALSO return a DCS-passthrough-wrapped
 * OSC 52 for the caller to write to stdout. Our sequence uses explicit `c`
 * (not tmux's crashy empty-param variant), so it sidesteps the #22432 path.
 * With `allow-passthrough on` + an OSC-52-capable outer terminal, selection
 * reaches the system clipboard; with either off, tmux silently drops the
 * DCS and prefix+] still works.
 *
 * If load-buffer fails entirely, fall through to raw OSC 52.
 *
 * Outside tmux, write raw OSC 52 to stdout (caller handles the write).
 *
 * Local (no SSH_CONNECTION): also shell out to a native clipboard utility.
 * OSC 52 and tmux -w both depend on terminal settings — iTerm2 disables
 * OSC 52 by default, VS Code shows a permission prompt on first use. Native
 * utilities (pbcopy/wl-copy/xclip/xsel/clip.exe) always work locally. Over
 * SSH these would write to the remote clipboard — OSC 52 is the right path there.
 *
 * Returns the sequence for the caller to write to stdout (raw OSC 52
 * outside tmux, DCS-wrapped inside).
 */
export async function setClipboard(text) {
    const b64 = Buffer.from(text, 'utf8').toString('base64');
    const raw = osc(OSC.CLIPBOARD, 'c', b64);
    // Native safety net — fire FIRST, before the tmux await, so a quick
    // focus-switch after selecting doesn't race pbcopy. Previously this ran
    // AFTER awaiting tmux load-buffer, adding ~50-100ms of subprocess latency
    // before pbcopy even started — fast cmd+tab → paste would beat it.
    // Gated on SSH_CONNECTION (not SSH_TTY) since tmux panes inherit SSH_TTY
    // forever but SSH_CONNECTION is in tmux's default update-environment and
    // clears on local attach. Fire-and-forget.
    if (!process.env['SSH_CONNECTION'])
        copyNative(text);
    const tmuxBufferLoaded = await tmuxLoadBuffer(text);
    // Inner OSC uses BEL directly (not osc()) — ST's ESC would need doubling
    // too, and BEL works everywhere for OSC 52.
    if (tmuxBufferLoaded)
        return tmuxPassthrough(`${ESC}]52;c;${b64}${BEL}`);
    return raw;
}
// Linux clipboard tool: undefined = not yet probed, null = none available.
// Probe order: wl-copy (Wayland) → xclip (X11) → xsel (X11 fallback).
// Cached after first attempt so repeated mouse-ups skip the probe chain.
let linuxCopy;
/**
 * Shell out to a native clipboard utility as a safety net for OSC 52.
 * Only called when not in an SSH session (over SSH, these would write to
 * the remote machine's clipboard — OSC 52 is the right path there).
 * Fire-and-forget: failures are silent since OSC 52 may have succeeded.
 */
function copyNative(text) {
    const opts = { input: text, useCwd: false, timeout: 2000 };
    switch (process.platform) {
        case 'darwin':
            void execFileNoThrow('pbcopy', [], opts);
            return;
        case 'linux': {
            if (linuxCopy === null)
                return;
            if (linuxCopy === 'wl-copy') {
                void execFileNoThrow('wl-copy', [], opts);
                return;
            }
            if (linuxCopy === 'xclip') {
                void execFileNoThrow('xclip', ['-selection', 'clipboard'], opts);
                return;
            }
            if (linuxCopy === 'xsel') {
                void execFileNoThrow('xsel', ['--clipboard', '--input'], opts);
                return;
            }
            // First call: probe wl-copy (Wayland) then xclip/xsel (X11), cache winner.
            void execFileNoThrow('wl-copy', [], opts).then(r => {
                if (r.code === 0) {
                    linuxCopy = 'wl-copy';
                    return;
                }
                void execFileNoThrow('xclip', ['-selection', 'clipboard'], opts).then(r2 => {
                    if (r2.code === 0) {
                        linuxCopy = 'xclip';
                        return;
                    }
                    void execFileNoThrow('xsel', ['--clipboard', '--input'], opts).then(r3 => {
                        linuxCopy = r3.code === 0 ? 'xsel' : null;
                    });
                });
            });
            return;
        }
        case 'win32':
            // clip.exe is always available on Windows. Unicode handling is
            // imperfect (system locale encoding) but good enough for a fallback.
            void execFileNoThrow('clip', [], opts);
            return;
    }
}
/** @internal test-only */
export function _resetLinuxCopyCache() {
    linuxCopy = undefined;
}
/**
 * OSC command numbers
 */
export const OSC = {
    SET_TITLE_AND_ICON: 0,
    SET_ICON: 1,
    SET_TITLE: 2,
    SET_COLOR: 4,
    SET_CWD: 7,
    HYPERLINK: 8,
    ITERM2: 9, // iTerm2 proprietary sequences
    SET_FG_COLOR: 10,
    SET_BG_COLOR: 11,
    SET_CURSOR_COLOR: 12,
    CLIPBOARD: 52,
    KITTY: 99, // Kitty notification protocol
    RESET_COLOR: 104,
    RESET_FG_COLOR: 110,
    RESET_BG_COLOR: 111,
    RESET_CURSOR_COLOR: 112,
    SEMANTIC_PROMPT: 133,
    GHOSTTY: 777, // Ghostty notification protocol
    TAB_STATUS: 21337, // Tab status extension
};
/**
 * Parse an OSC sequence into an action
 *
 * @param content - The sequence content (without ESC ] and terminator)
 */
export function parseOSC(content) {
    const semicolonIdx = content.indexOf(';');
    const command = semicolonIdx >= 0 ? content.slice(0, semicolonIdx) : content;
    const data = semicolonIdx >= 0 ? content.slice(semicolonIdx + 1) : '';
    const commandNum = parseInt(command, 10);
    // Window/icon title
    if (commandNum === OSC.SET_TITLE_AND_ICON) {
        return { type: 'title', action: { type: 'both', title: data } };
    }
    if (commandNum === OSC.SET_ICON) {
        return { type: 'title', action: { type: 'iconName', name: data } };
    }
    if (commandNum === OSC.SET_TITLE) {
        return { type: 'title', action: { type: 'windowTitle', title: data } };
    }
    // Hyperlinks (OSC 8)
    if (commandNum === OSC.HYPERLINK) {
        const parts = data.split(';');
        const paramsStr = parts[0] ?? '';
        const url = parts.slice(1).join(';');
        if (url === '') {
            return { type: 'link', action: { type: 'end' } };
        }
        const params = {};
        if (paramsStr) {
            for (const pair of paramsStr.split(':')) {
                const eqIdx = pair.indexOf('=');
                if (eqIdx >= 0) {
                    params[pair.slice(0, eqIdx)] = pair.slice(eqIdx + 1);
                }
            }
        }
        return {
            type: 'link',
            action: {
                type: 'start',
                url,
                params: Object.keys(params).length > 0 ? params : undefined,
            },
        };
    }
    // Tab status (OSC 21337)
    if (commandNum === OSC.TAB_STATUS) {
        return { type: 'tabStatus', action: parseTabStatus(data) };
    }
    return { type: 'unknown', sequence: `\x1b]${content}` };
}
/**
 * Parse an XParseColor-style color spec into an RGB Color.
 * Accepts `#RRGGBB` and `rgb:R/G/B` (1–4 hex digits per component, scaled
 * to 8-bit). Returns null on parse failure.
 */
export function parseOscColor(spec) {
    const hex = spec.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
    if (hex) {
        return {
            type: 'rgb',
            r: parseInt(hex[1], 16),
            g: parseInt(hex[2], 16),
            b: parseInt(hex[3], 16),
        };
    }
    const rgb = spec.match(/^rgb:([0-9a-f]{1,4})\/([0-9a-f]{1,4})\/([0-9a-f]{1,4})$/i);
    if (rgb) {
        // XParseColor: N hex digits → value / (16^N - 1), scale to 0-255
        const scale = (s) => Math.round((parseInt(s, 16) / (16 ** s.length - 1)) * 255);
        return {
            type: 'rgb',
            r: scale(rgb[1]),
            g: scale(rgb[2]),
            b: scale(rgb[3]),
        };
    }
    return null;
}
/**
 * Parse OSC 21337 payload: `key=value;key=value;...` with `\;` and `\\`
 * escapes inside values. Bare key or `key=` clears that field; unknown
 * keys are ignored.
 */
function parseTabStatus(data) {
    const action = {};
    for (const [key, value] of splitTabStatusPairs(data)) {
        switch (key) {
            case 'indicator':
                action.indicator = value === '' ? null : parseOscColor(value);
                break;
            case 'status':
                action.status = value === '' ? null : value;
                break;
            case 'status-color':
                action.statusColor = value === '' ? null : parseOscColor(value);
                break;
        }
    }
    return action;
}
/** Split `k=v;k=v` honoring `\;` and `\\` escapes. Yields [key, unescapedValue]. */
function* splitTabStatusPairs(data) {
    let key = '';
    let val = '';
    let inVal = false;
    let esc = false;
    for (const c of data) {
        if (esc) {
            if (inVal)
                val += c;
            else
                key += c;
            esc = false;
        }
        else if (c === '\\') {
            esc = true;
        }
        else if (c === ';') {
            yield [key, val];
            key = '';
            val = '';
            inVal = false;
        }
        else if (c === '=' && !inVal) {
            inVal = true;
        }
        else if (inVal) {
            val += c;
        }
        else {
            key += c;
        }
    }
    if (key || inVal)
        yield [key, val];
}
// Output generators
/** Start a hyperlink (OSC 8). Auto-assigns an id= param derived from the URL
 *  so terminals group wrapped lines of the same link together (the spec says
 *  cells with matching URI *and* nonempty id are joined; without an id each
 *  wrapped line is a separate link — inconsistent hover, partial tooltips).
 *  Empty url = close sequence (empty params per spec). */
export function link(url, params) {
    if (!url)
        return LINK_END;
    const p = { id: osc8Id(url), ...params };
    const paramStr = Object.entries(p)
        .map(([k, v]) => `${k}=${v}`)
        .join(':');
    return osc(OSC.HYPERLINK, paramStr, url);
}
function osc8Id(url) {
    let h = 0;
    for (let i = 0; i < url.length; i++)
        h = ((h << 5) - h + url.charCodeAt(i)) | 0;
    return (h >>> 0).toString(36);
}
/** End a hyperlink (OSC 8) */
export const LINK_END = osc(OSC.HYPERLINK, '', '');
// iTerm2 OSC 9 subcommands
/** iTerm2 OSC 9 subcommand numbers */
export const ITERM2 = {
    NOTIFY: 0,
    BADGE: 2,
    PROGRESS: 4,
};
/** Progress operation codes (for use with ITERM2.PROGRESS) */
export const PROGRESS = {
    CLEAR: 0,
    SET: 1,
    ERROR: 2,
    INDETERMINATE: 3,
};
/**
 * Clear iTerm2 progress bar sequence (OSC 9;4;0;BEL)
 * Uses BEL terminator since this is for cleanup (not runtime notification)
 * and we want to ensure it's always sent regardless of terminal type.
 */
export const CLEAR_ITERM2_PROGRESS = `${OSC_PREFIX}${OSC.ITERM2};${ITERM2.PROGRESS};${PROGRESS.CLEAR};${BEL}`;
/**
 * Clear terminal title sequence (OSC 0 with empty string + BEL).
 * Uses BEL terminator for cleanup — safe on all terminals.
 */
export const CLEAR_TERMINAL_TITLE = `${OSC_PREFIX}${OSC.SET_TITLE_AND_ICON};${BEL}`;
/** Clear all three OSC 21337 tab-status fields. Used on exit. */
export const CLEAR_TAB_STATUS = osc(OSC.TAB_STATUS, 'indicator=;status=;status-color=');
/**
 * Gate for emitting OSC 21337 (tab-status indicator). Ant-only while the
 * spec is unstable. Terminals that don't recognize it discard silently, so
 * emission is safe unconditionally — we don't gate on terminal detection
 * since support is expected across several terminals.
 *
 * Callers must wrap output with wrapForMultiplexer() so tmux/screen
 * DCS-passthrough carries the sequence to the outer terminal.
 */
export function supportsTabStatus() {
    return process.env.USER_TYPE === 'ant';
}
/**
 * Emit an OSC 21337 tab-status sequence. Omitted fields are left unchanged
 * by the receiving terminal; `null` sends an empty value to clear.
 * `;` and `\` in status text are escaped per the spec.
 */
export function tabStatus(fields) {
    const parts = [];
    const rgb = (c) => c.type === 'rgb'
        ? `#${[c.r, c.g, c.b].map(n => n.toString(16).padStart(2, '0')).join('')}`
        : '';
    if ('indicator' in fields)
        parts.push(`indicator=${fields.indicator ? rgb(fields.indicator) : ''}`);
    if ('status' in fields)
        parts.push(`status=${fields.status?.replaceAll('\\', '\\\\').replaceAll(';', '\\;') ?? ''}`);
    if ('statusColor' in fields)
        parts.push(`status-color=${fields.statusColor ? rgb(fields.statusColor) : ''}`);
    return osc(OSC.TAB_STATUS, parts.join(';'));
}
//# sourceMappingURL=osc.js.map