import { AltScreenSearchComponent, findAltScreenSearchMatches, getAltScreenSearchMatchKey, } from "./alt-screen-search.js";
import { AltScreenFlashContainer } from "./components/alt-screen-flash.js";
import { ScrollView } from "./components/scroll-view.js";
import { getKeybindings } from "./keybindings.js";
import { isKeyRelease } from "./keys.js";
import { getScrollbarGeometry, getScrollViewBox, getScrollViewsAt, renderLayoutFrame, } from "./layout.js";
import { deleteAllKittyImages, deleteAllKittyPlacements, deleteKittyImage, getCapabilities, getKittyImagePlacement, isImageLine, setCapabilities, } from "./terminal-image.js";
import { CURSOR_MARKER, compositeTuiLine, TuiBase, VIEWPORT_TUI, } from "./tui.js";
import { extractAnsiCode, getGraphemeCellRange, getOsc8LinkAtColumn, getWordSegmenter, sliceByColumn, stripTerminalSequences, visibleWidth, } from "./utils.js";
const ENTER_ALT_SCREEN = "\x1b[?1049h";
const EXIT_ALT_SCREEN = "\x1b[?1049l";
const DISABLE_AUTOWRAP = "\x1b[?7l";
const ENABLE_AUTOWRAP = "\x1b[?7h";
const ENABLE_BUTTON_MOTION_MOUSE = "\x1b[?1000h\x1b[?1002h\x1b[?1004h\x1b[?1006h";
const ENABLE_ALL_MOTION_MOUSE = "\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1004h\x1b[?1006h";
const DISABLE_MOUSE = "\x1b[?1006l\x1b[?1004l\x1b[?1003l\x1b[?1002l\x1b[?1000l";
const FOCUS_IN = "\x1b[I";
const FOCUS_OUT = "\x1b[O";
const BEGIN_SYNCHRONIZED_OUTPUT = "\x1b[?2026h";
const END_SYNCHRONIZED_OUTPUT = "\x1b[?2026l";
const OSC133_ZONE_PREFIX = /^(?:\x1b\]133;[ABC](?:\x07|\x1b\\))+/;
const OSC133_PROMPT_START = /^\x1b\]133;A(?:\x07|\x1b\\)/;
const PAGE_SCROLL_OVERLAP = 4;
const MAX_CACHED_OFFSCREEN_KITTY_IMAGES = 16;
const MAX_CACHED_OFFSCREEN_KITTY_TRANSMISSION_BYTES = 32 * 1024 * 1024;
const MAX_CACHED_OFFSCREEN_KITTY_DECODED_BYTES = 64 * 1024 * 1024;
const DOUBLE_CLICK_INTERVAL_MS = 500;
// Regular mode delegates double-click selection to the terminal emulator. Fullscreen owns mouse selection,
// so mirror common terminal word-selection behavior by keeping paths and kebab-case tokens whole.
const TERMINAL_WORD_SELECTION_JOINERS = new Set(["/", "-"]);
const wordSegmenter = getWordSegmenter();
/** Alternate-screen TUI with a scrollable, application-owned viewport. */
export class TuiAltScreen extends TuiBase {
    mode = "fullscreen";
    [VIEWPORT_TUI] = true;
    previousScreen = [];
    lastDocument = [];
    previousScreenWidth = 0;
    previousScreenHeight = 0;
    layoutRoot;
    currentLayout;
    implicitDocument;
    implicitScrollView;
    flashes;
    altScreenActive = false;
    imageProtocol = null;
    savedCapabilities;
    uploadedKittyImages = new Map();
    selectionAnchor;
    selectionFocus;
    selectionGranularity = "character";
    selectionInitialRange;
    lastClick;
    selectionDragPointer;
    selectionAutoScrollDirection = 0;
    selectionAutoScrollTimer;
    selectionPressActive = false;
    scrollbarDrag;
    scrollbarHover;
    activeSearch;
    pressedUrl;
    selectionDragged = false;
    wheelScrollLines;
    mouseEnabled;
    searchMatchStyle;
    searchCurrentMatchStyle;
    openUrl;
    onRightClickPaste;
    copyOnSelect;
    copySelection;
    constructor(terminal, showHardwareCursor, logDirectory, options = {}) {
        super(terminal, showHardwareCursor, logDirectory);
        this.implicitDocument = {
            render: (width) => super.render(width),
            invalidate: () => {
                for (const child of this.children)
                    child.invalidate();
            },
        };
        this.implicitScrollView = new ScrollView(this.implicitDocument, { follow: "end", primary: true });
        this.flashes = new AltScreenFlashContainer(() => this.requestRender());
        this.wheelScrollLines = Math.max(1, Math.floor(options.wheelScrollLines ?? 1));
        this.mouseEnabled = options.mouse ?? true;
        this.searchMatchStyle = options.searchMatchStyle ?? ((text) => `\x1b[4m${text}\x1b[24m`);
        this.searchCurrentMatchStyle = options.searchCurrentMatchStyle ?? ((text) => `\x1b[1;7m${text}\x1b[22;27m`);
        this.openUrl = options.openUrl;
        this.onRightClickPaste = options.onRightClickPaste;
        this.copyOnSelect = options.copyOnSelect ?? true;
        this.copySelection = options.copySelection;
        this.addInputListener((data) => this.handleViewportInput(data));
    }
    get viewportTop() {
        return this.getPrimaryScrollView().scrollTop;
    }
    get isFollowingOutput() {
        return this.getPrimaryScrollView().isFollowingEnd;
    }
    getCopyOnSelect() {
        return this.copyOnSelect;
    }
    setCopyOnSelect(enabled) {
        this.copyOnSelect = enabled;
    }
    /** Whether the fullscreen viewport has a non-empty active text selection. */
    hasActiveSelection() {
        return this.getActiveSelectionText() !== undefined;
    }
    /** Copy the active fullscreen text selection, if any, using the configured selection clipboard path. */
    async copyActiveSelectionToClipboard() {
        const text = this.getActiveSelectionText();
        if (!text)
            return false;
        return this.copyTextToClipboard(text);
    }
    setLayoutRoot(component) {
        if (this.layoutRoot === component)
            return;
        this.layoutRoot = component;
        this.currentLayout = undefined;
        this.requestRender();
    }
    render(width) {
        return this.layoutRoot?.render(width) ?? super.render(width);
    }
    getMountedRoots() {
        return this.layoutRoot ? [this.layoutRoot] : this.children;
    }
    getPrimaryScrollView() {
        return this.currentLayout?.primaryScrollView ?? this.implicitScrollView;
    }
    beforeTerminalStart() {
        this.stopSelectionAutoScroll();
        this.selectionPressActive = false;
        this.stopScrollbarHover();
        this.stopScrollbarDrag();
        this.flashes.dispose();
        this.altScreenActive = true;
        const capabilities = getCapabilities();
        this.imageProtocol = capabilities.images;
        this.uploadedKittyImages.clear();
        if (capabilities.images === "iterm2") {
            this.savedCapabilities = capabilities;
            setCapabilities({ ...capabilities, images: null });
            this.invalidate();
        }
        this.lastDocument = [];
        this.selectionAnchor = undefined;
        this.selectionFocus = undefined;
        this.selectionGranularity = "character";
        this.selectionInitialRange = undefined;
        this.lastClick = undefined;
        this.pressedUrl = undefined;
        this.selectionDragged = false;
        this.resetRenderState();
        const term = process.env.TERM?.toLowerCase() ?? "";
        // Multiplexers can lag when every pointer movement is forwarded. Button-motion
        // tracking preserves clicks, wheel events, selections, and scrollbar dragging.
        const mouseSequence = process.env.TMUX !== undefined ||
            process.env.ZELLIJ !== undefined ||
            process.env.STY !== undefined ||
            term.startsWith("tmux") ||
            term.startsWith("screen")
            ? ENABLE_BUTTON_MOTION_MOUSE
            : ENABLE_ALL_MOTION_MOUSE;
        this.terminal.write(`${ENTER_ALT_SCREEN}${DISABLE_AUTOWRAP}${this.mouseEnabled ? mouseSequence : ""}\x1b[2J\x1b[H\x1b[?25l`);
    }
    beforeTerminalStop(_options) {
        this.closeSearch();
        this.stopSelectionAutoScroll();
        this.selectionPressActive = false;
        this.stopScrollbarHover();
        this.stopScrollbarDrag();
        this.flashes.dispose();
        if (!this.altScreenActive)
            return;
        this.terminal.write(`${BEGIN_SYNCHRONIZED_OUTPUT}${this.deleteKittyImages()}${this.mouseEnabled ? DISABLE_MOUSE : ""}${ENABLE_AUTOWRAP}${END_SYNCHRONIZED_OUTPUT}`);
        this.uploadedKittyImages.clear();
    }
    afterTerminalStop(options) {
        if (!this.altScreenActive)
            return;
        this.altScreenActive = false;
        if (options.preserveScreen) {
            this.terminal.write(`${BEGIN_SYNCHRONIZED_OUTPUT}${EXIT_ALT_SCREEN}\x1b[?25h${END_SYNCHRONIZED_OUTPUT}`);
        }
        else {
            const width = Math.max(1, this.terminal.columns);
            const documentLines = this.render(width).map((line) => line.replace(OSC133_ZONE_PREFIX, ""));
            this.lastDocument = this.applyLineResets(documentLines.map((line) => line.replaceAll(CURSOR_MARKER, ""))).map((line) => (isImageLine(line) || visibleWidth(line) <= width ? line : sliceByColumn(line, 0, width, true)));
            let buffer = `${BEGIN_SYNCHRONIZED_OUTPUT}${EXIT_ALT_SCREEN}${DISABLE_AUTOWRAP}`;
            for (let row = 0; row < this.lastDocument.length; row++) {
                if (row > 0)
                    buffer += "\r\n";
                buffer += `\r\x1b[2K${this.lastDocument[row] ?? ""}`;
            }
            buffer += `\x1b[0m${ENABLE_AUTOWRAP}\r\n\x1b[?25h${END_SYNCHRONIZED_OUTPUT}`;
            this.terminal.write(buffer);
        }
        if (this.savedCapabilities) {
            setCapabilities(this.savedCapabilities);
            this.savedCapabilities = undefined;
        }
    }
    deleteKittyImages() {
        return this.imageProtocol === "kitty" ? deleteAllKittyImages() : "";
    }
    prepareKittyScreen(screen) {
        const visibleImageIds = new Set();
        const lines = screen.map((line) => {
            const placement = getKittyImagePlacement(line);
            if (!placement)
                return line;
            visibleImageIds.add(placement.imageId);
            const cachedImage = this.uploadedKittyImages.get(placement.imageId);
            const nextCachedImage = {
                transmissionGeneration: placement.transmissionGeneration,
                transmissionBytes: placement.transmissionBytes,
                estimatedDecodedBytes: placement.estimatedDecodedBytes,
            };
            if (cachedImage)
                this.uploadedKittyImages.delete(placement.imageId);
            this.uploadedKittyImages.set(placement.imageId, nextCachedImage);
            return cachedImage?.transmissionGeneration === placement.transmissionGeneration
                ? placement.replacementLine
                : line;
        });
        let cachedOffscreenImageCount = 0;
        let cachedOffscreenTransmissionBytes = 0;
        let cachedOffscreenDecodedBytes = 0;
        for (const [imageId, cachedImage] of this.uploadedKittyImages) {
            if (visibleImageIds.has(imageId))
                continue;
            cachedOffscreenImageCount += 1;
            cachedOffscreenTransmissionBytes += cachedImage.transmissionBytes;
            cachedOffscreenDecodedBytes += cachedImage.estimatedDecodedBytes;
        }
        let evictedImageDeletion = "";
        for (const [imageId, cachedImage] of this.uploadedKittyImages) {
            if (cachedOffscreenImageCount <= MAX_CACHED_OFFSCREEN_KITTY_IMAGES &&
                cachedOffscreenTransmissionBytes <= MAX_CACHED_OFFSCREEN_KITTY_TRANSMISSION_BYTES &&
                cachedOffscreenDecodedBytes <= MAX_CACHED_OFFSCREEN_KITTY_DECODED_BYTES) {
                break;
            }
            if (visibleImageIds.has(imageId))
                continue;
            evictedImageDeletion += deleteKittyImage(imageId);
            this.uploadedKittyImages.delete(imageId);
            cachedOffscreenImageCount -= 1;
            cachedOffscreenTransmissionBytes -= cachedImage.transmissionBytes;
            cachedOffscreenDecodedBytes -= cachedImage.estimatedDecodedBytes;
        }
        return { lines, evictedImageDeletion };
    }
    resetRenderState() {
        this.previousScreen = [];
        this.previousScreenWidth = 0;
        this.previousScreenHeight = 0;
        this.currentLayout = undefined;
    }
    scrollBy(lines) {
        this.getPrimaryScrollView().scrollBy(lines);
        this.requestRender();
    }
    scrollToTop() {
        this.getPrimaryScrollView().scrollToStart();
        this.requestRender();
    }
    scrollToBottom() {
        this.getPrimaryScrollView().scrollToEnd();
        this.requestRender();
    }
    scrollToPrompt(direction) {
        if (!this.currentLayout)
            return;
        const scrollView = this.getPrimaryScrollView();
        const lines = getScrollViewBox(this.currentLayout, scrollView)?.scrollContentLines;
        if (!lines)
            return;
        for (let row = scrollView.scrollTop + direction; row >= 0 && row < lines.length; row += direction) {
            if (!OSC133_PROMPT_START.test(lines[row] ?? ""))
                continue;
            scrollView.scrollTo(row);
            this.requestRender();
            return;
        }
    }
    openSearch() {
        if (this.activeSearch) {
            this.activeSearch.overlay?.focus();
            return;
        }
        const component = new AltScreenSearchComponent((query) => this.updateSearchQuery(query));
        const search = {
            component,
            query: "",
            matches: [],
            selectedIndex: -1,
            anchorRow: this.getPrimaryScrollView().scrollTop,
            selectionMode: "query",
        };
        this.activeSearch = search;
        search.overlay = this.showOverlay(component, {
            anchor: "top-right",
            width: "40%",
            minWidth: 24,
            margin: 1,
        });
    }
    closeSearch() {
        const search = this.activeSearch;
        if (!search)
            return;
        this.activeSearch = undefined;
        search.overlay?.hide();
        this.requestRender();
    }
    updateSearchQuery(query) {
        const search = this.activeSearch;
        if (!search || query === search.query)
            return;
        const selected = search.matches[search.selectedIndex];
        search.anchorRow = selected?.segments[0]?.row ?? this.getPrimaryScrollView().scrollTop;
        search.query = query;
        search.selectionMode = "query";
        search.component.setResult(-1, 0);
        this.requestRender();
    }
    navigateSearch(direction) {
        const search = this.activeSearch;
        if (!search?.query)
            return;
        search.selectionMode = direction < 0 ? "previous" : "next";
        this.requestRender();
    }
    refreshSearch(layout) {
        const search = this.activeSearch;
        if (!search)
            return false;
        const scrollView = layout.primaryScrollView ?? this.implicitScrollView;
        const box = getScrollViewBox(layout, scrollView);
        const lines = box?.scrollContentLines;
        if (!lines || !search.query.trim()) {
            search.matches = [];
            search.selectedIndex = -1;
            search.selectedKey = undefined;
            search.selectionMode = "retain";
            search.component.setResult(-1, 0);
            return false;
        }
        const shouldRevealSelection = search.selectionMode !== "retain";
        const matches = findAltScreenSearchMatches(lines, search.query);
        const exactIndex = search.selectedKey
            ? matches.findIndex((match) => getAltScreenSearchMatchKey(match) === search.selectedKey)
            : -1;
        let selectedIndex = -1;
        if (matches.length > 0) {
            if (search.selectionMode === "query") {
                selectedIndex = matches.findIndex((match) => (match.segments[0]?.row ?? 0) >= search.anchorRow);
                if (selectedIndex < 0)
                    selectedIndex = 0;
            }
            else if (search.selectionMode === "next") {
                const baseIndex = exactIndex >= 0 ? exactIndex : Math.min(search.selectedIndex, matches.length - 1);
                selectedIndex = baseIndex < 0 ? 0 : (baseIndex + 1) % matches.length;
            }
            else if (search.selectionMode === "previous") {
                const baseIndex = exactIndex >= 0 ? exactIndex : Math.min(search.selectedIndex, matches.length - 1);
                selectedIndex = baseIndex < 0 ? matches.length - 1 : (baseIndex - 1 + matches.length) % matches.length;
            }
            else {
                selectedIndex =
                    exactIndex >= 0 ? exactIndex : Math.min(Math.max(0, search.selectedIndex), matches.length - 1);
            }
        }
        search.matches = matches;
        search.selectedIndex = selectedIndex;
        search.selectedKey = selectedIndex >= 0 ? getAltScreenSearchMatchKey(matches[selectedIndex]) : undefined;
        search.selectionMode = "retain";
        search.component.setResult(selectedIndex, matches.length);
        if (!shouldRevealSelection)
            return false;
        const selected = matches[selectedIndex];
        const firstSegment = selected?.segments[0];
        const lastSegment = selected?.segments[selected.segments.length - 1];
        if (!box || !firstSegment || !lastSegment || scrollView.viewportHeight <= 0)
            return false;
        const before = scrollView.scrollTop;
        const visibleBottom = before + scrollView.viewportHeight - 1;
        let target = before;
        if (firstSegment.row < before || lastSegment.row > visibleBottom) {
            target = firstSegment.row - Math.floor(scrollView.viewportHeight / 3);
        }
        scrollView.scrollTo(target, { disableFollow: true });
        return scrollView.scrollTop !== before;
    }
    /** Show a transient message in the alternate-screen flash stack. */
    flash(message, durationMs) {
        this.flashes.flash(message, durationMs);
    }
    shouldDeferViewportInputToOverlay() {
        return this.isOverlayFocused() && this.activeSearch?.overlay?.isFocused() !== true;
    }
    handleViewportInput(data) {
        if (data === FOCUS_OUT) {
            const hadActiveSelection = this.selectionPressActive;
            const hadNonEmptyActiveSelection = hadActiveSelection && this.getSelectionBounds() !== undefined;
            this.selectionPressActive = false;
            this.stopSelectionAutoScroll();
            this.stopScrollbarHover();
            this.stopScrollbarDrag();
            this.pressedUrl = undefined;
            this.selectionDragged = false;
            if (hadActiveSelection) {
                this.selectionAnchor = undefined;
                this.selectionFocus = undefined;
                this.selectionGranularity = "character";
                this.selectionInitialRange = undefined;
                if (hadNonEmptyActiveSelection)
                    this.requestRender();
            }
            this.lastClick = undefined;
            return { consume: true };
        }
        if (data === FOCUS_IN)
            return { consume: true };
        const wheelEvent = this.parseWheelEvent(data);
        if (wheelEvent) {
            if (this.shouldDeferViewportInputToOverlay())
                return undefined;
            this.routeWheel(wheelEvent);
            return { consume: true };
        }
        const mouseEvent = this.parseSgrMouseEvent(data);
        if (mouseEvent) {
            if (this.handleRightClickPaste(mouseEvent))
                return { consume: true };
            const handled = this.handleScrollbarMouseEvent(mouseEvent);
            if (!this.scrollbarDrag)
                this.updateScrollbarHover(mouseEvent.x, mouseEvent.y);
            if (!handled)
                this.handleSelectionMouseEvent(mouseEvent);
            return { consume: true };
        }
        if (this.isMouseSequence(data))
            return { consume: true };
        const keybindings = getKeybindings();
        const isRelease = isKeyRelease(data);
        if (keybindings.matches(data, "tui.altScreen.search")) {
            if (!isRelease)
                this.openSearch();
            return { consume: true };
        }
        if (this.activeSearch?.overlay?.isFocused()) {
            if (keybindings.matches(data, "tui.altScreen.searchNext")) {
                if (!isRelease)
                    this.navigateSearch(1);
                return { consume: true };
            }
            if (keybindings.matches(data, "tui.altScreen.searchPrevious")) {
                if (!isRelease)
                    this.navigateSearch(-1);
                return { consume: true };
            }
            if (keybindings.matches(data, "tui.altScreen.searchClose")) {
                if (!isRelease)
                    this.closeSearch();
                return { consume: true };
            }
        }
        if (this.shouldDeferViewportInputToOverlay())
            return undefined;
        if (keybindings.matches(data, "tui.altScreen.pageUp")) {
            if (!isRelease) {
                this.scrollBy(-Math.max(1, this.getPrimaryScrollView().viewportHeight - PAGE_SCROLL_OVERLAP));
            }
            return { consume: true };
        }
        if (keybindings.matches(data, "tui.altScreen.pageDown")) {
            if (!isRelease) {
                this.scrollBy(Math.max(1, this.getPrimaryScrollView().viewportHeight - PAGE_SCROLL_OVERLAP));
            }
            return { consume: true };
        }
        if (keybindings.matches(data, "tui.altScreen.halfPageUp")) {
            if (!isRelease)
                this.scrollBy(-Math.max(1, Math.floor(this.getPrimaryScrollView().viewportHeight / 2)));
            return { consume: true };
        }
        if (keybindings.matches(data, "tui.altScreen.halfPageDown")) {
            if (!isRelease)
                this.scrollBy(Math.max(1, Math.floor(this.getPrimaryScrollView().viewportHeight / 2)));
            return { consume: true };
        }
        if (keybindings.matches(data, "tui.altScreen.lineUp")) {
            if (!isRelease)
                this.scrollBy(-1);
            return { consume: true };
        }
        if (keybindings.matches(data, "tui.altScreen.lineDown")) {
            if (!isRelease)
                this.scrollBy(1);
            return { consume: true };
        }
        if (keybindings.matches(data, "tui.altScreen.previousPrompt")) {
            if (!isRelease)
                this.scrollToPrompt(-1);
            return { consume: true };
        }
        if (keybindings.matches(data, "tui.altScreen.nextPrompt")) {
            if (!isRelease)
                this.scrollToPrompt(1);
            return { consume: true };
        }
        if (keybindings.matches(data, "tui.altScreen.top")) {
            if (!isRelease)
                this.scrollToTop();
            return { consume: true };
        }
        if (keybindings.matches(data, "tui.altScreen.bottom")) {
            if (!isRelease)
                this.scrollToBottom();
            return { consume: true };
        }
        return undefined;
    }
    parseWheelEvent(data) {
        const sgr = /^\x1b\[<(\d+);(\d+);(\d+)[Mm]$/.exec(data);
        if (sgr) {
            const button = Number.parseInt(sgr[1], 10);
            if ((button & 64) === 0)
                return undefined;
            const direction = button & 3;
            if (direction !== 0 && direction !== 1)
                return undefined;
            return {
                direction: direction === 0 ? -1 : 1,
                x: Number.parseInt(sgr[2], 10) - 1,
                y: Number.parseInt(sgr[3], 10) - 1,
            };
        }
        if (data.length === 6 && data.startsWith("\x1b[M")) {
            const button = data.charCodeAt(3) - 32;
            if ((button & 64) === 0)
                return undefined;
            const direction = button & 3;
            if (direction !== 0 && direction !== 1)
                return undefined;
            return {
                direction: direction === 0 ? -1 : 1,
                x: data.charCodeAt(4) - 33,
                y: data.charCodeAt(5) - 33,
            };
        }
        return undefined;
    }
    routeWheel(event) {
        let remaining = event.direction * this.wheelScrollLines;
        const seen = new Set();
        for (const scrollView of this.currentLayout ? getScrollViewsAt(this.currentLayout, event.x, event.y) : []) {
            seen.add(scrollView);
            remaining = scrollView.scrollBy(remaining);
            if (remaining === 0 || scrollView.overscroll === "contain")
                break;
        }
        const primary = this.getPrimaryScrollView();
        if (remaining !== 0 && !seen.has(primary))
            primary.scrollBy(remaining);
        this.updateScrollbarHover(event.x, event.y);
        this.requestRender();
    }
    parseSgrMouseEvent(data) {
        const match = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/.exec(data);
        if (!match)
            return undefined;
        return {
            button: Number.parseInt(match[1], 10),
            x: Number.parseInt(match[2], 10) - 1,
            y: Number.parseInt(match[3], 10) - 1,
            release: match[4] === "m",
        };
    }
    handleRightClickPaste(event) {
        if (!this.onRightClickPaste ||
            process.platform !== "win32" ||
            process.env.TERM_PROGRAM?.toLowerCase() === "vscode" ||
            event.release ||
            event.button !== 2) {
            return false;
        }
        try {
            this.onRightClickPaste();
        }
        catch {
            // Clipboard paste is best-effort.
        }
        return true;
    }
    getScrollbarTargetAt(x, y) {
        if (this.hasOverlay() || !this.currentLayout)
            return undefined;
        for (const scrollView of getScrollViewsAt(this.currentLayout, x, y)) {
            const box = getScrollViewBox(this.currentLayout, scrollView);
            const geometry = box ? getScrollbarGeometry(box) : undefined;
            if (geometry &&
                x === geometry.column &&
                y >= geometry.thumbTop &&
                y < geometry.thumbTop + geometry.thumbHeight) {
                return { scrollView, geometry };
            }
        }
        return undefined;
    }
    setScrollbarHover(scrollView) {
        if (scrollView === this.scrollbarHover)
            return;
        this.scrollbarHover?.setScrollbarActive(false);
        this.scrollbarHover = scrollView;
        this.scrollbarHover?.setScrollbarActive(true);
    }
    updateScrollbarHover(x, y) {
        this.setScrollbarHover(this.getScrollbarTargetAt(x, y)?.scrollView);
    }
    stopScrollbarHover() {
        this.setScrollbarHover(undefined);
    }
    handleScrollbarMouseEvent(event) {
        if (this.scrollbarDrag) {
            if (event.release) {
                this.stopScrollbarDrag();
                return true;
            }
            const box = this.currentLayout
                ? getScrollViewBox(this.currentLayout, this.scrollbarDrag.scrollView)
                : undefined;
            const geometry = box ? getScrollbarGeometry(box) : undefined;
            if (geometry) {
                const maxThumbOffset = geometry.trackHeight - geometry.thumbHeight;
                const thumbOffset = Math.max(0, Math.min(maxThumbOffset, event.y - geometry.trackTop - this.scrollbarDrag.grabOffset));
                const scrollTop = maxThumbOffset === 0 ? 0 : Math.round((thumbOffset / maxThumbOffset) * geometry.maxScrollTop);
                this.scrollbarDrag.scrollView.scrollTo(scrollTop);
            }
            return true;
        }
        if (event.release || (event.button & 32) !== 0 || (event.button & 3) !== 0)
            return false;
        const target = this.getScrollbarTargetAt(event.x, event.y);
        if (!target)
            return false;
        this.stopSelectionAutoScroll();
        this.selectionPressActive = false;
        this.selectionAnchor = undefined;
        this.selectionFocus = undefined;
        this.selectionGranularity = "character";
        this.selectionInitialRange = undefined;
        this.lastClick = undefined;
        this.pressedUrl = undefined;
        this.selectionDragged = false;
        this.setScrollbarHover(target.scrollView);
        this.scrollbarDrag = {
            scrollView: target.scrollView,
            grabOffset: event.y - target.geometry.thumbTop,
        };
        return true;
    }
    stopScrollbarDrag() {
        this.scrollbarDrag = undefined;
    }
    getScrollSelectionPoint(scrollView, x, y) {
        if (!this.currentLayout)
            return undefined;
        const box = getScrollViewBox(this.currentLayout, scrollView);
        if (!box || box.rect.height <= 0 || box.clip.height <= 0)
            return undefined;
        const visibleTop = Math.max(0, box.rect.y, box.clip.y);
        const visibleBottom = Math.min(this.terminal.rows - 1, box.rect.y + box.rect.height - 1, box.clip.y + box.clip.height - 1);
        if (visibleBottom < visibleTop)
            return undefined;
        const pointerRow = Math.max(visibleTop, Math.min(visibleBottom, y));
        const maxContentRow = Math.max(0, (box.scrollContentLines?.length ?? 1) - 1);
        return {
            row: Math.max(0, Math.min(maxContentRow, scrollView.scrollTop + pointerRow - box.rect.y)),
            col: Math.max(0, Math.min(box.rect.width - 1, x - box.rect.x)),
            scrollView,
        };
    }
    getSelectionPoint(event, scrollView) {
        if (scrollView) {
            const point = this.getScrollSelectionPoint(scrollView, event.x, event.y);
            if (point)
                return point;
        }
        return {
            row: Math.max(0, Math.min(this.terminal.rows - 1, event.y)),
            col: Math.max(0, Math.min(this.terminal.columns - 1, event.x)),
        };
    }
    getSelectionSourceLine(point) {
        if (point.scrollView && this.currentLayout) {
            const lines = getScrollViewBox(this.currentLayout, point.scrollView)?.scrollContentLines;
            if (lines)
                return lines[point.row] ?? "";
        }
        return this.previousScreen[point.row] ?? "";
    }
    getWordSelection(point) {
        const line = stripTerminalSequences(this.getSelectionSourceLine(point));
        const segments = [];
        let start = 0;
        for (const segment of wordSegmenter.segment(line)) {
            const end = start + visibleWidth(segment.segment);
            const joiner = TERMINAL_WORD_SELECTION_JOINERS.has(segment.segment);
            segments.push({ start, end, selectable: segment.isWordLike === true || joiner, joiner });
            start = end;
        }
        const clickedSegmentIndex = segments.findIndex((segment) => point.col >= segment.start && point.col < segment.end);
        if (clickedSegmentIndex < 0)
            return undefined;
        const canJoin = (left, right) => left.selectable && right.selectable && (left.joiner || right.joiner);
        let selectionStart = segments[clickedSegmentIndex].start;
        let selectionEnd = segments[clickedSegmentIndex].end;
        for (let index = clickedSegmentIndex; index > 0 && canJoin(segments[index - 1], segments[index]); index--) {
            selectionStart = segments[index - 1].start;
        }
        for (let index = clickedSegmentIndex; index < segments.length - 1 && canJoin(segments[index], segments[index + 1]); index++) {
            selectionEnd = segments[index + 1].end;
        }
        return {
            start: { ...point, col: selectionStart },
            end: { ...point, col: selectionEnd, boundary: true },
        };
    }
    getLineSelection(point) {
        return {
            start: { ...point, col: 0 },
            end: { ...point, col: visibleWidth(this.getSelectionSourceLine(point)), boundary: true },
        };
    }
    updateSelectionFocus(point) {
        if (this.selectionGranularity === "character" || !this.selectionInitialRange) {
            this.selectionFocus = point;
            return;
        }
        const range = this.selectionGranularity === "word" ? this.getWordSelection(point) : this.getLineSelection(point);
        if (!range)
            return;
        const initial = this.selectionInitialRange;
        const targetBeforeInitial = range.start.row < initial.start.row ||
            (range.start.row === initial.start.row && range.start.col < initial.start.col);
        if (targetBeforeInitial) {
            this.selectionAnchor = initial.end;
            this.selectionFocus = range.start;
        }
        else {
            this.selectionAnchor = initial.start;
            this.selectionFocus = range.end;
        }
    }
    getClickCount(point, word) {
        const now = Date.now();
        const previous = this.lastClick;
        const count = word &&
            previous &&
            now - previous.timestamp <= DOUBLE_CLICK_INTERVAL_MS &&
            previous.row === point.row &&
            previous.scrollView === point.scrollView &&
            previous.wordStart === word.start.col &&
            previous.wordEnd === word.end.col
            ? (previous.count % 3) + 1
            : 1;
        this.lastClick = word
            ? {
                timestamp: now,
                count,
                row: point.row,
                scrollView: point.scrollView,
                wordStart: word.start.col,
                wordEnd: word.end.col,
            }
            : undefined;
        return count;
    }
    updateSelectionAutoScroll(event) {
        const scrollView = this.selectionAnchor?.scrollView;
        if (!scrollView || !this.currentLayout) {
            this.stopSelectionAutoScroll();
            return;
        }
        const box = getScrollViewBox(this.currentLayout, scrollView);
        if (!box || box.rect.height <= 0 || box.clip.height <= 0) {
            this.stopSelectionAutoScroll();
            return;
        }
        const visibleTop = Math.max(0, box.rect.y, box.clip.y);
        const visibleBottom = Math.min(this.terminal.rows - 1, box.rect.y + box.rect.height - 1, box.clip.y + box.clip.height - 1);
        this.selectionDragPointer = { x: event.x, y: event.y };
        this.selectionAutoScrollDirection = event.y <= visibleTop ? -1 : event.y >= visibleBottom ? 1 : 0;
        if (this.selectionAutoScrollDirection === 0) {
            this.stopSelectionAutoScroll();
            return;
        }
        if (this.selectionAutoScrollTimer)
            return;
        this.selectionAutoScrollTimer = setInterval(() => this.autoScrollSelection(), 50);
        this.selectionAutoScrollTimer.unref();
    }
    autoScrollSelection() {
        const scrollView = this.selectionAnchor?.scrollView;
        const pointer = this.selectionDragPointer;
        const direction = this.selectionAutoScrollDirection;
        if (!scrollView || !pointer || direction === 0) {
            this.stopSelectionAutoScroll();
            return;
        }
        const remaining = scrollView.scrollBy(direction);
        if (remaining === direction) {
            this.stopSelectionAutoScroll();
            return;
        }
        const point = this.getScrollSelectionPoint(scrollView, pointer.x, pointer.y);
        if (point)
            this.updateSelectionFocus(point);
        this.requestRender();
    }
    stopSelectionAutoScroll() {
        if (this.selectionAutoScrollTimer) {
            clearInterval(this.selectionAutoScrollTimer);
            this.selectionAutoScrollTimer = undefined;
        }
        this.selectionAutoScrollDirection = 0;
        this.selectionDragPointer = undefined;
    }
    handleSelectionMouseEvent(event) {
        const button = event.button & 3;
        if (button !== 0 && !(event.release && button === 3))
            return;
        const anchorScrollView = this.selectionAnchor?.scrollView;
        const point = this.getSelectionPoint(event, anchorScrollView);
        if (event.release) {
            if (!this.selectionPressActive)
                return;
            this.selectionPressActive = false;
            this.stopSelectionAutoScroll();
            if (!this.selectionAnchor)
                return;
            this.updateSelectionFocus(point);
            const clickedUrl = !this.selectionDragged &&
                this.selectionAnchor.scrollView === point.scrollView &&
                this.selectionAnchor.row === point.row &&
                this.selectionAnchor.col === point.col
                ? this.pressedUrl
                : undefined;
            this.pressedUrl = undefined;
            if (clickedUrl && this.openUrl) {
                this.selectionAnchor = undefined;
                this.selectionFocus = undefined;
                try {
                    this.openUrl(clickedUrl);
                }
                catch {
                    // URL activation is best-effort.
                }
                this.requestRender();
                return;
            }
            if (this.copyOnSelect)
                void this.copySelectionToClipboard();
            this.requestRender();
            return;
        }
        if ((event.button & 32) !== 0) {
            if (!this.selectionPressActive || !this.selectionAnchor)
                return;
            this.selectionDragged = true;
            this.lastClick = undefined;
            this.pressedUrl = undefined;
            this.updateSelectionFocus(point);
            this.updateSelectionAutoScroll(event);
            this.requestRender();
            return;
        }
        this.stopSelectionAutoScroll();
        this.selectionPressActive = true;
        const scrollView = !this.hasOverlay() && this.currentLayout
            ? getScrollViewsAt(this.currentLayout, event.x, event.y)[0]
            : undefined;
        const anchor = this.getSelectionPoint(event, scrollView);
        const word = this.getWordSelection(anchor);
        const clickCount = this.getClickCount(anchor, word);
        const range = clickCount === 2 ? word : clickCount === 3 ? this.getLineSelection(anchor) : undefined;
        this.selectionGranularity = range ? (clickCount === 2 ? "word" : "line") : "character";
        this.selectionInitialRange = range;
        this.selectionAnchor = range?.start ?? anchor;
        this.selectionFocus = range?.end ?? anchor;
        this.selectionDragged = false;
        this.pressedUrl = range
            ? undefined
            : getOsc8LinkAtColumn(this.previousScreen[Math.max(0, Math.min(this.terminal.rows - 1, event.y))] ?? "", Math.max(0, Math.min(this.terminal.columns - 1, event.x)));
        this.requestRender();
    }
    getSelectionBounds() {
        if (!this.selectionAnchor || !this.selectionFocus)
            return undefined;
        if (this.selectionAnchor.scrollView !== this.selectionFocus.scrollView)
            return undefined;
        const anchorBeforeFocus = this.selectionAnchor.row < this.selectionFocus.row ||
            (this.selectionAnchor.row === this.selectionFocus.row && this.selectionAnchor.col < this.selectionFocus.col);
        if (this.selectionAnchor.row === this.selectionFocus.row &&
            this.selectionAnchor.col === this.selectionFocus.col) {
            return undefined;
        }
        return anchorBeforeFocus
            ? { start: this.selectionAnchor, end: this.selectionFocus }
            : { start: this.selectionFocus, end: this.selectionAnchor };
    }
    getSelectionColumns(line, row, selection, minColumn = 0, maxColumn = visibleWidth(line)) {
        const lineWidth = visibleWidth(line);
        let start = Math.max(0, minColumn);
        let end = Math.min(lineWidth, maxColumn);
        if (row === selection.start.row) {
            start = getGraphemeCellRange(line, selection.start.col)?.start ?? Math.min(selection.start.col, lineWidth);
        }
        if (row === selection.end.row) {
            end = selection.end.boundary
                ? Math.min(selection.end.col, lineWidth)
                : (getGraphemeCellRange(line, selection.end.col)?.end ?? Math.min(selection.end.col + 1, lineWidth));
        }
        return { start: Math.max(minColumn, start), end: Math.min(maxColumn, end) };
    }
    getActiveSelectionText() {
        const selection = this.getSelectionBounds();
        if (!selection)
            return undefined;
        let sourceLines = this.previousScreen;
        if (selection.start.scrollView) {
            if (!this.currentLayout)
                return undefined;
            const box = getScrollViewBox(this.currentLayout, selection.start.scrollView);
            if (!box?.scrollContentLines)
                return undefined;
            sourceLines = box.scrollContentLines;
        }
        const lines = [];
        for (let row = selection.start.row; row <= selection.end.row; row++) {
            const line = sourceLines[row] ?? "";
            const columns = this.getSelectionColumns(line, row, selection);
            lines.push(stripTerminalSequences(sliceByColumn(line, columns.start, Math.max(0, columns.end - columns.start), true)).trimEnd());
        }
        const text = lines.join("\n");
        return text.length === 0 ? undefined : text;
    }
    async copySelectionToClipboard() {
        const text = this.getActiveSelectionText();
        if (!text)
            return false;
        return this.copyTextToClipboard(text);
    }
    async copyTextToClipboard(text) {
        // Prefer an injected clipboard implementation (native clipboard + platform tools with a
        // verified success path) when the host app provides one. A bare OSC 52 write can show
        // "Copied!" while leaving the system clipboard untouched (e.g. macOS Terminal.app, tmux
        // without OSC 52 clipboard passthrough), so only report success when it actually copies.
        if (this.copySelection) {
            const ok = await this.copySelection(text);
            this.flash(ok ? "Copied!" : "Copy failed");
            return ok;
        }
        this.terminal.write(`\x1b]52;c;${Buffer.from(text).toString("base64")}\x07`);
        this.flash("Copied!");
        return true;
    }
    applySearchTextHighlight(text, current) {
        const style = current ? this.searchCurrentMatchStyle : this.searchMatchStyle;
        let result = "";
        let plainStart = 0;
        let index = 0;
        while (index < text.length) {
            const ansi = extractAnsiCode(text, index);
            if (!ansi) {
                index += 1;
                continue;
            }
            if (index > plainStart)
                result += style(text.slice(plainStart, index));
            result += ansi.code;
            index += ansi.length;
            plainStart = index;
        }
        if (plainStart < text.length)
            result += style(text.slice(plainStart));
        return result;
    }
    applySearchHighlights(screen, layout) {
        const search = this.activeSearch;
        if (!search || search.selectedIndex < 0 || search.matches.length === 0)
            return screen;
        const scrollView = layout.primaryScrollView ?? this.implicitScrollView;
        const box = getScrollViewBox(layout, scrollView);
        if (!box)
            return screen;
        const rangesByRow = new Map();
        const scrollbarColumn = getScrollbarGeometry(box)?.column;
        const minRow = Math.max(0, box.rect.y, box.clip.y);
        const maxRow = Math.min(screen.length, box.rect.y + box.rect.height, box.clip.y + box.clip.height);
        const minColumn = Math.max(0, box.rect.x, box.clip.x);
        const maxColumn = Math.min(this.terminal.columns, box.rect.x + box.rect.width, box.clip.x + box.clip.width, scrollbarColumn ?? Number.POSITIVE_INFINITY);
        for (let matchIndex = 0; matchIndex < search.matches.length; matchIndex++) {
            for (const segment of search.matches[matchIndex].segments) {
                const row = box.rect.y + segment.row - scrollView.scrollTop;
                if (row < minRow || row >= maxRow)
                    continue;
                const startCol = Math.max(minColumn, box.rect.x + segment.startCol);
                const endCol = Math.min(maxColumn, box.rect.x + segment.endCol);
                if (endCol <= startCol)
                    continue;
                const ranges = rangesByRow.get(row) ?? [];
                ranges.push({ startCol, endCol, current: matchIndex === search.selectedIndex });
                rangesByRow.set(row, ranges);
            }
        }
        const result = [...screen];
        for (const [row, ranges] of rangesByRow) {
            let line = result[row] ?? "";
            if (isImageLine(line))
                continue;
            const lineWidth = visibleWidth(line);
            for (const range of ranges.sort((a, b) => b.startCol - a.startCol)) {
                const startCol = Math.min(range.startCol, lineWidth);
                const endCol = Math.min(range.endCol, lineWidth);
                if (endCol <= startCol)
                    continue;
                const before = sliceByColumn(line, 0, startCol, true);
                const highlighted = sliceByColumn(line, startCol, endCol - startCol, true);
                const after = sliceByColumn(line, endCol, Math.max(0, lineWidth - endCol), true);
                line = `${before}${this.applySearchTextHighlight(highlighted, range.current)}${after}`;
            }
            result[row] = line;
        }
        return result;
    }
    applySelectionHighlight(text) {
        let result = "\x1b[7m";
        let index = 0;
        while (index < text.length) {
            const ansi = extractAnsiCode(text, index);
            if (!ansi) {
                result += text[index];
                index += 1;
                continue;
            }
            result += ansi.code;
            if (ansi.code.endsWith("m"))
                result += "\x1b[7m";
            index += ansi.length;
        }
        return `${result}\x1b[27m`;
    }
    applySelection(screen, layout = this.currentLayout) {
        const selection = this.getSelectionBounds();
        if (!selection)
            return screen;
        let screenSelection = selection;
        let minRow = 0;
        let maxRow = screen.length - 1;
        let minColumn = 0;
        let maxColumn = this.terminal.columns;
        if (selection.start.scrollView) {
            if (!layout)
                return screen;
            const box = getScrollViewBox(layout, selection.start.scrollView);
            if (!box)
                return screen;
            minRow = Math.max(0, box.rect.y, box.clip.y);
            maxRow = Math.min(screen.length - 1, box.rect.y + box.rect.height - 1, box.clip.y + box.clip.height - 1);
            minColumn = Math.max(0, box.rect.x, box.clip.x);
            maxColumn = Math.min(this.terminal.columns, box.rect.x + box.rect.width, box.clip.x + box.clip.width);
            screenSelection = {
                start: {
                    ...selection.start,
                    row: box.rect.y + selection.start.row - selection.start.scrollView.scrollTop,
                    col: box.rect.x + selection.start.col,
                },
                end: {
                    ...selection.end,
                    row: box.rect.y + selection.end.row - selection.start.scrollView.scrollTop,
                    col: box.rect.x + selection.end.col,
                },
            };
        }
        return screen.map((line, row) => {
            if (row < minRow ||
                row > maxRow ||
                row < screenSelection.start.row ||
                row > screenSelection.end.row ||
                isImageLine(line)) {
                return line;
            }
            const lineWidth = visibleWidth(line);
            const columns = this.getSelectionColumns(line, row, screenSelection, minColumn, maxColumn);
            if (columns.end <= columns.start)
                return line;
            const before = sliceByColumn(line, 0, columns.start, true);
            const selected = sliceByColumn(line, columns.start, columns.end - columns.start, true);
            const after = sliceByColumn(line, columns.end, Math.max(0, lineWidth - columns.end), true);
            return `${before}${this.applySelectionHighlight(selected)}${after}`;
        });
    }
    isMouseSequence(data) {
        return /^\x1b\[<\d+;\d+;\d+[Mm]$/.test(data) || (data.length === 6 && data.startsWith("\x1b[M"));
    }
    compositeFlashes(screen, width, height) {
        const flashLines = this.flashes.render(width).slice(-height);
        if (flashLines.length === 0)
            return screen;
        const result = [...screen];
        while (result.length < height)
            result.push("");
        for (let row = 0; row < flashLines.length; row++) {
            const line = flashLines[row];
            const flashWidth = visibleWidth(line);
            if (flashWidth === 0)
                continue;
            result[row] = compositeTuiLine(result[row] ?? "", line, width - flashWidth, flashWidth, width);
        }
        return result;
    }
    doRender() {
        if (this.stopped || !this.altScreenActive)
            return;
        const width = Math.max(1, this.terminal.columns);
        const height = Math.max(1, this.terminal.rows);
        const root = this.layoutRoot ?? this.implicitScrollView;
        let nextLayout = renderLayoutFrame(root, width, height, () => this.requestRender());
        if (this.refreshSearch(nextLayout)) {
            nextLayout = renderLayoutFrame(root, width, height, () => this.requestRender());
        }
        let screen = nextLayout.lines.map((line) => line.replace(OSC133_ZONE_PREFIX, ""));
        screen = this.applySearchHighlights(screen, nextLayout);
        screen = this.compositeOverlays(screen, width, height);
        if (screen.length > height)
            screen = screen.slice(screen.length - height);
        screen = this.applySelection(screen, nextLayout);
        screen = this.compositeFlashes(screen, width, height);
        const cursorPos = this.extractCursorPosition(screen, height);
        screen = this.applyLineResets(screen).map((line) => {
            if (isImageLine(line) || visibleWidth(line) <= width)
                return line;
            return sliceByColumn(line, 0, width, true);
        });
        const fullRedraw = this.previousScreen.length === 0 || this.previousScreenWidth !== width || this.previousScreenHeight !== height;
        const imagesNeedRedraw = screen.some((line, row) => line !== this.previousScreen[row] && (isImageLine(line) || isImageLine(this.previousScreen[row] ?? "")));
        const redrawImages = fullRedraw || imagesNeedRedraw;
        const hadUploadedKittyImages = this.uploadedKittyImages.size > 0;
        const preparedKittyScreen = redrawImages && this.imageProtocol === "kitty"
            ? this.prepareKittyScreen(screen)
            : { lines: screen, evictedImageDeletion: "" };
        let buffer = BEGIN_SYNCHRONIZED_OUTPUT;
        if (fullRedraw) {
            this.fullRedrawCount += 1;
            const clearImages = this.imageProtocol === "kitty" && hadUploadedKittyImages
                ? deleteAllKittyPlacements()
                : this.deleteKittyImages();
            buffer += `${clearImages}\x1b[2J`;
        }
        else if (imagesNeedRedraw) {
            if (this.imageProtocol === "iterm2")
                buffer += "\x1b[2J";
            else if (this.imageProtocol === "kitty")
                buffer += deleteAllKittyPlacements();
        }
        buffer += preparedKittyScreen.evictedImageDeletion;
        for (let row = 0; row < height; row++) {
            if (!fullRedraw && !imagesNeedRedraw && screen[row] === this.previousScreen[row])
                continue;
            buffer += `\x1b[${row + 1};1H\x1b[2K${preparedKittyScreen.lines[row] ?? ""}`;
        }
        if (cursorPos) {
            buffer += `\x1b[${cursorPos.row + 1};${Math.min(width, cursorPos.col) + 1}H`;
            buffer += this.getShowHardwareCursor() ? "\x1b[?25h" : "\x1b[?25l";
        }
        else {
            buffer += "\x1b[?25l";
        }
        buffer += END_SYNCHRONIZED_OUTPUT;
        this.terminal.write(buffer);
        this.previousScreen = screen;
        this.previousScreenWidth = width;
        this.previousScreenHeight = height;
        this.currentLayout = nextLayout;
    }
}
//# sourceMappingURL=tui-alt-screen.js.map