core/menu/win.ps1

Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod parse_list {
    # X
    if ($PSCompletions.config.enable_list_follow_cursor) {
        $PSCompletions.menu.pos.X = $Host.UI.RawUI.CursorPosition.X
        # 如果跟随鼠标,且超过右侧边界,则向左偏移
        $PSCompletions.menu.pos.X = [Math]::Min($PSCompletions.menu.pos.X, $Host.UI.RawUI.BufferSize.Width - 1 - $PSCompletions.menu.ui_width)
    }
    else {
        $PSCompletions.menu.pos.X = 0
    }

    # Y
    $PSCompletions.menu.ui_height = $PSCompletions.menu.filter_list.Count + 2
    if ($PSCompletions.menu.is_show_above) {
        $PSCompletions.menu.ui_height = [Math]::Min($PSCompletions.menu.cursor_to_top, $PSCompletions.menu.ui_height)
        $list_limit = if ($PSCompletions.config.list_max_count_when_above -eq -1) { 12 }else { $PSCompletions.config.list_max_count_when_above + 2 }
        $PSCompletions.menu.ui_height = [Math]::Min($list_limit, $PSCompletions.menu.ui_height)
        $PSCompletions.menu.pos.Y = $Host.UI.RawUI.CursorPosition.Y - $PSCompletions.menu.ui_height - $PSCompletions.config.height_from_menu_bottom_to_cursor_when_above
    }
    else {
        $PSCompletions.menu.ui_height = [Math]::Min($PSCompletions.menu.cursor_to_bottom, $PSCompletions.menu.ui_height)
        $list_limit = if ($PSCompletions.config.list_max_count_when_below -eq -1) { 12 }else { $PSCompletions.config.list_max_count_when_below + 2 }
        $PSCompletions.menu.ui_height = [Math]::Min($list_limit, $PSCompletions.menu.ui_height)
        $PSCompletions.menu.pos.Y = $Host.UI.RawUI.CursorPosition.Y + 1
    }
    $PSCompletions.menu.page_max_index = $PSCompletions.menu.ui_height - 3
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod get_buffer {
    param($startPos, $endPos)
    $top = New-Object System.Management.Automation.Host.Coordinates $startPos.X, $startPos.Y
    $bottom = New-Object System.Management.Automation.Host.Coordinates $endPos.X , $endPos.Y
    $buffer = $Host.UI.RawUI.GetBufferContents((New-Object System.Management.Automation.Host.Rectangle $top, $bottom))
    @{
        top    = $top
        bottom = $bottom
        buffer = $buffer
    }
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_buffer {
    # XXX: 在 Windows PowerShell 5.x 中,边框使用以下符号以处理兼容性问题
    if ($PSEdition -ne 'Core') {
        $horizontal = '-'
        $vertical = '|'
        $top_left = '+'
        $bottom_left = '+'
        $top_right = '+'
        $bottom_right = '+'
    }
    else {
        $horizontal = $PSCompletions.config.horizontal
        $vertical = $PSCompletions.config.vertical
        $top_left = $PSCompletions.config.top_left
        $bottom_left = $PSCompletions.config.bottom_left
        $top_right = $PSCompletions.config.top_right
        $bottom_right = $PSCompletions.config.bottom_right
    }

    $border_box = @()
    $content_box = @()
    $list_area = $PSCompletions.menu.list_max_width

    $border_box += [string]$top_left + $horizontal * $list_area + $top_right

    $line = [string]$vertical + ' ' * $list_area + [string]$vertical
    $content = ' ' * $list_area
    foreach ($_ in 0..($PSCompletions.menu.ui_height - 3)) {
        $border_box += $line
        $content_box += $content
    }

    $status = "$(([string]($PSCompletions.menu.selected_index + 1)).PadLeft($PSCompletions.menu.filter_list.Count.ToString().Length, ' '))"

    $border_box += [string]$bottom_left + $horizontal * 2 + ' ' * ($status.Length + 1) + $horizontal * ($list_area - $status.Length - 3) + $bottom_right

    $Host.UI.RawUI.SetBufferContents($PSCompletions.menu.pos, $Host.UI.RawUI.NewBufferCellArray($border_box, $PSCompletions.config.border_text, $PSCompletions.config.border_back))

    $Host.UI.RawUI.SetBufferContents(@{
            X = $PSCompletions.menu.pos.X + 1
            Y = $PSCompletions.menu.pos.Y + 1
        }, $Host.UI.RawUI.NewBufferCellArray($content_box, $PSCompletions.config.item_text, $PSCompletions.config.item_back)
    )
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_list_buffer {
    param([int]$offset)
    $content_box = @()
    foreach ($_ in $offset..($PSCompletions.menu.ui_height - 3 + $offset)) {
        $content_length = $PSCompletions.menu.get_length($PSCompletions.menu.filter_list[$_].ListItemText)
        $content = $PSCompletions.menu.filter_list[$_].ListItemText + ' ' * ($PSCompletions.menu.list_max_width - $content_length)
        $content_box += $content
    }
    $Host.UI.RawUI.SetBufferContents(@{
            X = $PSCompletions.menu.pos.X + 1
            Y = $PSCompletions.menu.pos.Y + 1
        },
        $Host.UI.RawUI.NewBufferCellArray($content_box, $PSCompletions.config.item_text, $PSCompletions.config.item_back)
    )
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_filter_buffer {
    param([string]$filter)
    $char = $PSCompletions.config.filter_symbol
    $middle = [System.Math]::Ceiling($char.Length / 2)
    $start = $char.Substring(0, $middle)
    $end = $char.Substring($middle)

    $buffer_filter = $Host.UI.RawUI.NewBufferCellArray(@(@($start, $filter, $end) -join ''), $PSCompletions.config.filter_text, $PSCompletions.config.filter_back)
    $Host.UI.RawUI.SetBufferContents(
        @{
            X = $PSCompletions.menu.pos.X + 2
            Y = $PSCompletions.menu.pos.Y
        },
        $buffer_filter
    )
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_status_buffer {
    $X = $PSCompletions.menu.pos.X + 3
    if ($PSCompletions.menu.is_show_above) {
        $Y = $Host.UI.RawUI.CursorPosition.Y - 1 - $PSCompletions.config.height_from_menu_bottom_to_cursor_when_above
    }
    else {
        $Y = $PSCompletions.menu.pos.Y + $PSCompletions.menu.ui_height - 1
    }

    $current = "$(([string]($PSCompletions.menu.selected_index + 1)).PadLeft($PSCompletions.menu.filter_list.Count.ToString().Length, ' '))"
    $buffer_status = $Host.UI.RawUI.NewBufferCellArray(@("$current$($PSCompletions.config.status_symbol)$($PSCompletions.menu.filter_list.Count)"), $PSCompletions.config.status_text, $PSCompletions.config.status_back)

    $Host.UI.RawUI.SetBufferContents(@{ X = $X; Y = $Y }, $buffer_status)
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_tip_buffer {
    param([int]$index)
    if ($PSCompletions.menu.is_show_above) {
        $start = 0
        $line = $PSCompletions.menu.pos.Y
    }
    else {
        $start = $PSCompletions.menu.pos.Y + $PSCompletions.menu.ui_height
        $line = $Host.UI.RawUI.BufferSize.Height - $start
    }
    if ($line -gt 0) {
        $box = @()
        $content = ' ' * $Host.UI.RawUI.BufferSize.Width
        foreach ($_ in 1..$line) {
            $box += $content
        }
        $Host.UI.RawUI.SetBufferContents(
            @{
                X = 0
                Y = $start
            },
            $Host.UI.RawUI.NewBufferCellArray($box, $Host.UI.RawUI.BackgroundColor, $Host.UI.RawUI.BackgroundColor)
        )
    }

    if ($PSCompletions.menu.is_show_tip) {
        if ($PSCompletions.menu.is_show_above) {
            $rest_line = $PSCompletions.menu.cursor_to_top - $PSCompletions.menu.ui_height
        }
        else {
            $rest_line = $PSCompletions.menu.cursor_to_bottom - $PSCompletions.menu.ui_height
        }
        if ($rest_line -le 0) {
            return
        }

        function Get-MultilineTruncatedString {
            param ([string]$inputString)

            $lineWidth = $Host.UI.RawUI.BufferSize.Width

            if ($PSCompletions.config.enable_tip_follow_cursor) {
                $lineWidth -= $Host.UI.RawUI.CursorPosition.X
            }

            $currentWidth = 0
            $outputString = ''
            $currentLine = ''

            $char_record = @{}

            foreach ($char in $inputString.ToCharArray()) {
                if ($char_record.ContainsKey($char)) {
                    $charWidth = $char_record[$char]
                }
                else {
                    $charWidth = $Host.UI.RawUI.NewBufferCellArray($char, $Host.UI.RawUI.BackgroundColor, $Host.UI.RawUI.BackgroundColor).LongLength
                    $char_record[$char] = $charWidth
                }

                if ($currentWidth + $charWidth -gt $lineWidth) {
                    $outputString += $currentLine + "`n"
                    $currentLine = ''
                    $currentWidth = 0
                }
                $currentLine += $char
                $currentWidth += $charWidth
            }
            $outputString += $currentLine

            return $outputString
        }

        $tip = $PSCompletions.menu.filter_list[$index].ToolTip
        if ($tip -ne $null) {
            $json = $PSCompletions.completions.$($PSCompletions.root_cmd)
            $info = $json.info

            $tip_arr = @()
            foreach ($v in ($PSCompletions.replace_content($tip).Split("`n"))) {
                $tip_arr += (Get-MultilineTruncatedString $v).Split("`n")
            }

            $pos = @{
                X = if ($PSCompletions.config.enable_tip_follow_cursor) { $PSCompletions.menu.pos.X }else { 0 }
                Y = $PSCompletions.menu.pos.Y + $PSCompletions.menu.ui_height + 1
            }
            $full = $rest_line - $tip_arr.Count

            if ($PSCompletions.menu.is_show_above) {
                if ($full -lt 0) {
                    $pos.Y = 0
                    $maxIndex = $tip_arr.Count + $full - 1
                }
                else {
                    $pos.Y = $full
                    $maxIndex = $tip_arr.Count - 1
                }
                $tip_arr = $tip_arr[0..$maxIndex]
            }
            else {
                if ($pos.Y -ge $Host.UI.RawUI.BufferSize.Height - 1) {
                    return
                }
            }
            $Host.UI.RawUI.SetBufferContents($pos, $Host.UI.RawUI.NewBufferCellArray($tip_arr, $PSCompletions.config.tip_text, $PSCompletions.config.tip_back))
        }
    }
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod set_selection {
    if ($PSCompletions.menu.old_selection) {
        $Host.UI.RawUI.SetBufferContents($PSCompletions.menu.old_selection.pos, $PSCompletions.menu.old_selection.buffer)
    }

    $X = $PSCompletions.menu.pos.X + 1
    $to_X = $X + $PSCompletions.menu.list_max_width - 1

    # 当前页的第几个
    $Y = $PSCompletions.menu.pos.Y + 1 + $PSCompletions.menu.page_current_index

    # 根据坐标,生成需要被改变内容的矩形,也就是要被选中的项
    $Rectangle = New-Object System.Management.Automation.Host.Rectangle $X, $Y, $to_X, $Y

    # 通过矩形,获取到这个矩形中的原本的内容
    $LineBuffer = $Host.UI.RawUI.GetBufferContents($Rectangle)
    $PSCompletions.menu.old_selection = @{
        pos    = @{
            X = $X
            Y = $Y
        }
        buffer = $LineBuffer
    }
    # 给原本的内容设置前景颜色和背景颜色
    # XXX: 对于多字节字符,需要过滤掉 Trailing 类型字符以确保正确渲染
    # $content = foreach ($i in $LineBuffer) { $i.Character }
    $content = foreach ($i in $LineBuffer.Where({ $_.BufferCellType -ne 'Trailing' })) { $i.Character }
    $LineBuffer = $Host.UI.RawUI.NewBufferCellArray(@([string]::Join('', $content)), $PSCompletions.config.selected_text, $PSCompletions.config.selected_back)
    $Host.UI.RawUI.SetBufferContents(@{ X = $X; Y = $Y }, $LineBuffer)
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod move_selection {
    param([bool]$isDown)

    $moveDirection = if ($isDown) { 1 } else { -1 }

    $is_move = if ($isDown) {
        $PSCompletions.menu.page_current_index -lt $PSCompletions.menu.page_max_index
    }
    else {
        $PSCompletions.menu.page_current_index -gt 0
    }

    $new_selected_index = $PSCompletions.menu.selected_index + $moveDirection

    if ($PSCompletions.config.enable_list_loop) {
        $PSCompletions.menu.selected_index = ($new_selected_index + $PSCompletions.menu.filter_list.Count) % $PSCompletions.menu.filter_list.Count
    }
    else {
        $PSCompletions.menu.selected_index = if ($new_selected_index -lt 0) {
            0
        }
        elseif ($new_selected_index -ge $PSCompletions.menu.filter_list.Count) {
            $PSCompletions.menu.filter_list.Count - 1
        }
        else {
            $new_selected_index
        }
    }

    if ($is_move) {
        $PSCompletions.menu.page_current_index = ($PSCompletions.menu.page_current_index + $moveDirection) % ($PSCompletions.menu.page_max_index + 1)
        if ($PSCompletions.menu.page_current_index -lt 0) {
            $PSCompletions.menu.page_current_index += $PSCompletions.menu.page_max_index + 1
        }
        $PSCompletions.menu.set_selection()
        $PSCompletions.menu.new_status_buffer()
        $PSCompletions.menu.new_tip_buffer($PSCompletions.menu.selected_index)
        $PSCompletions.menu.handle_data('edit')
    }
    elseif ($PSCompletions.config.enable_list_loop -or ($new_selected_index -ge 0 -and $new_selected_index -lt $PSCompletions.menu.filter_list.Count)) {
        if ($isDown) {
            if ($PSCompletions.menu.selected_index -eq 0) {
                $PSCompletions.menu.page_current_index -= $PSCompletions.menu.page_max_index
            }
        }
        else {
            if ($PSCompletions.menu.selected_index -eq $PSCompletions.menu.filter_list.Count - 1) {
                $PSCompletions.menu.page_current_index += $PSCompletions.menu.page_max_index
            }
        }

        $PSCompletions.menu.offset = ($PSCompletions.menu.offset + $moveDirection) % ($PSCompletions.menu.filter_list.Count - $PSCompletions.menu.page_max_index)
        if ($PSCompletions.menu.offset -lt 0) {
            $PSCompletions.menu.offset += $PSCompletions.menu.filter_list.Count - $PSCompletions.menu.page_max_index
        }

        $PSCompletions.menu.old_selection = $null
        $PSCompletions.menu.new_list_buffer($PSCompletions.menu.offset)
        $PSCompletions.menu.set_selection()
        $PSCompletions.menu.new_filter_buffer($PSCompletions.menu.filter)
        $PSCompletions.menu.new_status_buffer()
        $PSCompletions.menu.new_tip_buffer($PSCompletions.menu.selected_index)
        $PSCompletions.menu.handle_data('edit')
    }
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod reset {
    param([bool]$clearAll = $true)
    if ($clearAll) {
        $PSCompletions.menu.data.Clear()
        if ($PSCompletions.menu.origin_full_buffer) {
            $Host.UI.RawUI.SetBufferContents($PSCompletions.menu.origin_full_buffer.top, $PSCompletions.menu.origin_full_buffer.buffer)
            $PSCompletions.menu.origin_full_buffer = $null
        }
    }
    $PSCompletions.menu.old_selection = $null
    $PSCompletions.menu.offset = 0
    $PSCompletions.menu.selected_index = 0
    $PSCompletions.menu.page_current_index = 0
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod handle_data {
    param([string]$Type)

    switch ($Type) {
        add {
            $PSCompletions.menu.data.Add(
                @{
                    page_current_index = $PSCompletions.menu.page_current_index
                    page_max_index     = $PSCompletions.menu.page_max_index
                    selected_index     = $PSCompletions.menu.selected_index
                    offset             = $PSCompletions.menu.offset
                    filter             = $PSCompletions.menu.filter
                    filter_list        = $PSCompletions.menu.filter_list.Clone()
                    old_selection      = $PSCompletions.menu.old_selection.Clone()
                    old_full_buffer    = $PSCompletions.menu.get_buffer($PSCompletions.menu.buffer_start, $PSCompletions.menu.buffer_end)

                    # XXX: 这里必须使用基础类型,否则有可能出现数据不一致,导致菜单塌陷
                    ui_height          = $PSCompletions.menu.ui_height
                    pos_y              = $PSCompletions.menu.pos.Y
                }
            )
        }
        get {
            $data = $PSCompletions.menu.data[-1]
            $PSCompletions.menu.page_current_index = $data.page_current_index
            $PSCompletions.menu.page_max_index = $data.page_max_index
            $PSCompletions.menu.selected_index = $data.selected_index
            $PSCompletions.menu.offset = $data.offset
            $PSCompletions.menu.filter = $data.filter
            $PSCompletions.menu.filter_list = $data.filter_list
            $PSCompletions.menu.old_selection = $data.old_selection

            # XXX: 这里必须使用基础类型,否则有可能出现数据不一致,导致菜单塌陷
            $PSCompletions.menu.ui_height = $data.ui_height
            $PSCompletions.menu.pos.Y = $data.pos_y
        }
        edit {
            $PSCompletions.menu.data[-1] = @{
                page_current_index = $PSCompletions.menu.page_current_index
                page_max_index     = $PSCompletions.menu.page_max_index
                selected_index     = $PSCompletions.menu.selected_index
                offset             = $PSCompletions.menu.offset
                filter             = $PSCompletions.menu.filter
                filter_list        = $PSCompletions.menu.filter_list.Clone()
                old_selection      = $PSCompletions.menu.old_selection.Clone()
                old_full_buffer    = $PSCompletions.menu.get_buffer($PSCompletions.menu.buffer_start, $PSCompletions.menu.buffer_end)

                # XXX: 这里必须使用基础类型,否则有可能出现数据不一致,导致菜单塌陷
                ui_height          = $PSCompletions.menu.ui_height
                pos_y              = $PSCompletions.menu.pos.Y
            }
        }
    }
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod show_module_menu {
    param($filter_list)
    if ($Host.UI.RawUI.BufferSize.Height -lt 5) {
        [Microsoft.PowerShell.PSConsoleReadLine]::UndoAll()
        [Microsoft.PowerShell.PSConsoleReadLine]::Insert($PSCompletions.info.min_area)
        return ''
    }
    if (!$filter_list) { return '' }

    $suffix = $PSCompletions.config.completion_suffix

    function handleOutput($item) {
        $out = $item.CompletionText

        # 不是由 TabExpansion2 获取的补全,即通过 psc add 添加的补全
        if ($null -eq $item.ResultType) {
            return "$out$suffix"
        }

        if ($item.ResultType -in @(
                [System.Management.Automation.CompletionResultType]::Method,
                [System.Management.Automation.CompletionResultType]::Property,
                [System.Management.Automation.CompletionResultType]::Variable,
                [System.Management.Automation.CompletionResultType]::Type,
                [System.Management.Automation.CompletionResultType]::Namespace
            )) {
            return $out
        }

        # Directory, registry key, or other container types
        if ($item.ResultType -eq [System.Management.Automation.CompletionResultType]::ProviderContainer) {
            if ($PSCompletions.config.enable_path_with_trailing_separator) {
                if ($out.Length -ge 1 -and $out[-1] -match "^['`"]$") {
                    if ($out.Length -ge 2 -and $out[-2] -match "^[/\\]$") {
                        return $out
                    }
                    return $out.Insert($out.Length - 1, $PSCompletions.separator)
                }
                return $out + $PSCompletions.separator
            }
            return $out
        }

        # File or other leaf items (e.g., registry values)
        # [System.Management.Automation.CompletionResultType]::ProviderItem

        # [System.Management.Automation.CompletionResultType]::Command

        # [System.Management.Automation.CompletionResultType]::ParameterName

        # [System.Management.Automation.CompletionResultType]::ParameterValue

        # if foreach switch ...
        # [System.Management.Automation.CompletionResultType]::Keyword

        # [System.Management.Automation.CompletionResultType]::Text

        return "$out$suffix"
    }

    $PSCompletions.menu.pos = @{ X = 0; Y = 0 }

    $PSCompletions.menu.ui_width = 0
    $PSCompletions.menu.ui_height = 0
    $PSCompletions.menu.list_max_width = $PSCompletions.config.list_min_width

    $PSCompletions.menu.filter = ''  # 过滤的关键词

    $PSCompletions.menu.page_current_index = 0 # 当前显示页中的索引

    $PSCompletions.menu.selected_index = 0  # 当前选中项的实际索引

    $PSCompletions.menu.offset = 0  # 索引的偏移量,用于滚动翻页

    # 记录每一次过滤的数据
    $PSCompletions.menu.data = [System.Collections.Generic.List[System.Object]]::new()

    if ($PSCompletions.menu.by_TabExpansion2) {
        $PSCompletions.menu.is_show_tip = $PSCompletions.config.enable_tip_when_enhance
    }
    else {
        $enable_tip = $PSCompletions.config.comp_config.$($PSCompletions.root_cmd).enable_tip
        if ($null -eq $enable_tip) {
            $PSCompletions.menu.is_show_tip = $PSCompletions.config.enable_tip
        }
        else {
            $PSCompletions.menu.is_show_tip = $enable_tip
        }
    }

    $PSCompletions.menu.filter_list = [System.Collections.Generic.List[System.Object]]::new()
    foreach ($item in $filter_list) {
        $PSCompletions.menu.list_max_width = [Math]::Max($PSCompletions.menu.list_max_width, $PSCompletions.menu.get_length($item.ListItemText))
        $PSCompletions.menu.filter_list.Add($item)
    }
    $PSCompletions.menu.ui_width = $PSCompletions.menu.list_max_width + 2

    if ($PSCompletions.config.enable_enter_when_single -and $PSCompletions.menu.filter_list.Count -eq 1) {
        return handleOutput $PSCompletions.menu.filter_list[0]
    }

    $current_encoding = [console]::OutputEncoding
    [console]::OutputEncoding = $PSCompletions.encoding

    $PSCompletions.menu.cursor_to_bottom = $Host.UI.RawUI.BufferSize.Height - $Host.UI.RawUI.CursorPosition.Y - 1
    $PSCompletions.menu.cursor_to_top = $Host.UI.RawUI.CursorPosition.Y - $PSCompletions.config.height_from_menu_bottom_to_cursor_when_above - 1

    $PSCompletions.menu.is_show_above = $PSCompletions.menu.cursor_to_bottom -lt $PSCompletions.menu.cursor_to_top

    if ($PSCompletions.menu.is_show_above) {
        $startY = 0
        $endY = $PSCompletions.menu.cursor_to_top
    }
    else {
        $startY = $Host.UI.RawUI.CursorPosition.Y + 1
        $endY = $Host.UI.RawUI.BufferSize.Height - 1
    }

    $PSCompletions.menu.buffer_start = New-Object System.Management.Automation.Host.Coordinates 0, $startY
    $PSCompletions.menu.buffer_end = New-Object System.Management.Automation.Host.Coordinates ($Host.UI.RawUI.BufferSize.Width - 1), $endY

    $PSCompletions.menu.parse_list()

    # 如果解析后的菜单高度小于 3 (上下边框 + 1个补全项)
    if ($PSCompletions.menu.ui_height -lt 3 -or $PSCompletions.menu.ui_width -gt $Host.ui.RawUI.BufferSize.Width - 2) {
        [Microsoft.PowerShell.PSConsoleReadLine]::UndoAll()
        [Microsoft.PowerShell.PSConsoleReadLine]::Insert($PSCompletions.info.min_area)
        return ''
    }

    # 显示菜单之前,记录 buffer
    $PSCompletions.menu.origin_full_buffer = $PSCompletions.menu.get_buffer($PSCompletions.menu.buffer_start, $PSCompletions.menu.buffer_end)

    # 显示菜单
    $PSCompletions.menu.new_buffer()
    $PSCompletions.menu.new_list_buffer($PSCompletions.menu.offset)

    $PSCompletions.menu.set_selection()
    $PSCompletions.menu.new_filter_buffer($PSCompletions.menu.filter)
    $PSCompletions.menu.new_status_buffer()
    $PSCompletions.menu.new_tip_buffer($PSCompletions.menu.selected_index)

    $PSCompletions.menu.handle_data('add')

    :loop while (($PressKey = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown,AllowCtrlC')).VirtualKeyCode) {
        $pressShift = 0x10 -band [int]$PressKey.ControlKeyState
        $pressCtrl = $PressKey.ControlKeyState -like '*CtrlPressed*'

        switch ($PressKey.VirtualKeyCode) {
            9 {
                # 9: Tab
                if ($PSCompletions.menu.filter_list.Count -eq 1) {
                    $PSCompletions.menu.reset()
                    handleOutput $PSCompletions.menu.filter_list[$PSCompletions.menu.selected_index]
                    break loop
                }
                $PSCompletions.menu.move_selection(!$pressShift)
                break
            }
            { $_ -eq 27 -or ($pressCtrl -and $_ -eq 67) } {
                # 27: ESC
                # 67: Ctrl + c
                $PSCompletions.menu.reset()
                ''
                break loop
            }
            { $_ -in @(32, 13) } {
                # 32: Space
                # 13: Enter
                handleOutput $PSCompletions.menu.filter_list[$PSCompletions.menu.selected_index]
                $PSCompletions.menu.reset()
                break loop
            }

            # 向上
            # 37: Up
            # 38: Left
            # 85: Ctrl + u
            # 80: Ctrl + p
            # 75: Ctrl + k
            { $_ -in @(37, 38) -or ($pressCtrl -and $_ -in @(85, 80, 75)) } {
                $PSCompletions.menu.move_selection($false)
                break
            }
            # 向下
            # 39: Right
            # 40: Down
            # 68: Ctrl + d
            # 78: Ctrl + n
            # 74: Ctrl + j
            { $_ -in @(39, 40) -or ($pressCtrl -and $_ -in @(68, 78, 74)) } {
                $PSCompletions.menu.move_selection($true)
                break
            }
            # filter character
            { $PressKey.Character } {
                # remove
                if ($PressKey.Character -eq 8) {
                    # 8: Backspace
                    if ($PSCompletions.menu.filter) {
                        $PSCompletions.menu.filter = $PSCompletions.menu.filter.Substring(0, $PSCompletions.menu.filter.Length - 1)
                    }
                    else {
                        $PSCompletions.menu.reset()
                        ''
                        break loop
                    }

                    if ($PSCompletions.menu.data.Count -gt 1) {
                        $old_buffer = $PSCompletions.menu.data[-2].old_full_buffer
                        $Host.UI.RawUI.SetBufferContents($old_buffer.top, $old_buffer.buffer)
                        if ($PSCompletions.menu.data.Count -gt 1) {
                            $PSCompletions.menu.data.RemoveAt($PSCompletions.menu.data.Count - 1)
                        }
                    }
                    else {
                        $old_buffer = $PSCompletions.menu.data[0].old_full_buffer
                        $Host.UI.RawUI.SetBufferContents($old_buffer.top, $old_buffer.buffer)
                        $PSCompletions.menu.data.Clear()
                    }

                    $PSCompletions.menu.handle_data('get')
                }
                else {
                    # add
                    if ($PSCompletions.menu.filter -match '\*$' -and $PressKey.Character -eq '*') {
                        break
                    }
                    $PSCompletions.menu.filter += $PressKey.Character

                    $escapedFilter = $PSCompletions.menu.filter.Replace('[', '`[').Replace(']', '`]')
                    if ($escapedFilter.StartsWith('^')) {
                        $comparison = {
                            param($text)
                            $text -like $escapedFilter.Substring(1) + '*'
                        }
                    }
                    else {
                        $comparison = {
                            param($text)
                            $text -like "*$escapedFilter*"
                        }
                    }
                    $resultList = [System.Collections.Generic.List[System.Object]]::new()
                    foreach ($f in $PSCompletions.menu.filter_list) {
                        if ($f.ListItemText -and $comparison.Invoke($f.ListItemText)) {
                            $resultList.Add($f)
                        }
                    }
                    $PSCompletions.menu.filter_list = $resultList.ToArray()

                    if (!$PSCompletions.menu.filter_list) {
                        $PSCompletions.menu.filter = $PSCompletions.menu.data[-1].filter
                        $PSCompletions.menu.filter_list = $PSCompletions.menu.data[-1].filter_list
                    }
                    else {
                        $PSCompletions.menu.reset($false)
                        $PSCompletions.menu.parse_list()
                        $PSCompletions.menu.new_buffer()
                        $PSCompletions.menu.new_list_buffer($PSCompletions.menu.offset)
                        $PSCompletions.menu.new_tip_buffer($PSCompletions.menu.selected_index)
                        $PSCompletions.menu.new_status_buffer()
                        $PSCompletions.menu.new_filter_buffer($PSCompletions.menu.filter)
                        $PSCompletions.menu.set_selection(0)

                        $PSCompletions.menu.handle_data('add')
                    }
                }
                break
            }
        }
    }
    [console]::OutputEncoding = $current_encoding
}