mirror of
https://github.com/sbrow/envr.git
synced 2026-08-26 09:53:32 -04:00
267 lines
5.3 KiB
Odin
267 lines
5.3 KiB
Odin
package main
|
|
|
|
import "core:fmt"
|
|
import "core:strings"
|
|
import "core:sys/posix"
|
|
import "core:terminal/ansi"
|
|
|
|
MultiSelect_Result :: enum {
|
|
Confirm,
|
|
Cancel,
|
|
}
|
|
|
|
Key :: enum {
|
|
Up,
|
|
Down,
|
|
Space,
|
|
Enter,
|
|
Escape,
|
|
Backspace,
|
|
Char,
|
|
Unknown,
|
|
}
|
|
|
|
Raw_State :: struct {
|
|
original: posix.termios,
|
|
fd: posix.FD,
|
|
}
|
|
|
|
MAX_VISIBLE :: 7
|
|
|
|
// Caller is responsible for deleting the responses.
|
|
multi_select :: proc(
|
|
prompt: string,
|
|
options: []string,
|
|
) -> (
|
|
selected: [dynamic]bool,
|
|
result: MultiSelect_Result,
|
|
) {
|
|
if len(options) == 0 {
|
|
return
|
|
}
|
|
|
|
selected = make([dynamic]bool, len(options))
|
|
filter: [dynamic]u8
|
|
defer delete(filter)
|
|
filtered: [dynamic]int
|
|
defer delete(filtered)
|
|
rebuild_filtered(options, filter[:], &filtered)
|
|
|
|
cursor: int = 0
|
|
scroll_offset: int = 0
|
|
|
|
fmt.printf(ansi.CSI + ansi.DECTCEM_HIDE)
|
|
visible := render_options(prompt, options, selected[:], filtered[:], string(filter[:]), cursor, scroll_offset)
|
|
|
|
raw, ok := enable_raw_mode(posix.STDIN_FILENO)
|
|
if !ok {
|
|
fmt.printf(ansi.CSI + ansi.DECTCEM_SHOW)
|
|
return
|
|
}
|
|
defer disable_raw_mode(&raw)
|
|
|
|
for {
|
|
key, ch := read_key()
|
|
|
|
switch key {
|
|
case .Char:
|
|
append(&filter, u8(ch))
|
|
rebuild_filtered(options, filter[:], &filtered)
|
|
cursor = clamp(cursor, 0, max(0, len(filtered) - 1))
|
|
case .Backspace:
|
|
if len(filter) > 0 {
|
|
pop(&filter)
|
|
rebuild_filtered(options, filter[:], &filtered)
|
|
cursor = clamp(cursor, 0, max(0, len(filtered) - 1))
|
|
}
|
|
case .Up:
|
|
if cursor > 0 {
|
|
cursor -= 1
|
|
}
|
|
case .Down:
|
|
if cursor < len(filtered) - 1 {
|
|
cursor += 1
|
|
}
|
|
case .Space:
|
|
if len(filtered) > 0 {
|
|
selected[filtered[cursor]] = !selected[filtered[cursor]]
|
|
}
|
|
case .Enter:
|
|
fmt.printf(
|
|
ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.ED + ansi.CSI + ansi.DECTCEM_SHOW,
|
|
visible + 1,
|
|
)
|
|
result = .Confirm
|
|
return
|
|
case .Escape:
|
|
fmt.printf(
|
|
ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.ED + ansi.CSI + ansi.DECTCEM_SHOW,
|
|
visible + 1,
|
|
)
|
|
result = .Cancel
|
|
return
|
|
case .Unknown:
|
|
}
|
|
|
|
scroll_offset = max(0, min(cursor - MAX_VISIBLE / 2, len(filtered) - MAX_VISIBLE))
|
|
fmt.printf(ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.RESET + ansi.ED, visible + 1)
|
|
visible = render_options(prompt, options, selected[:], filtered[:], string(filter[:]), cursor, scroll_offset)
|
|
}
|
|
}
|
|
|
|
rebuild_filtered :: proc(options: []string, filter: []u8, filtered: ^[dynamic]int) {
|
|
clear(filtered)
|
|
filter_str := string(filter)
|
|
filter_lower := strings.to_lower(filter_str, context.temp_allocator)
|
|
for opt, i in options {
|
|
opt_lower := strings.to_lower(opt, context.temp_allocator)
|
|
if strings.contains(opt_lower, filter_lower) {
|
|
append(filtered, i)
|
|
}
|
|
}
|
|
}
|
|
|
|
render_options :: proc(
|
|
prompt: string,
|
|
options: []string,
|
|
selected: []bool,
|
|
filtered: []int,
|
|
filter_text: string,
|
|
cursor: int,
|
|
scroll_offset: int,
|
|
) -> int {
|
|
fmt.printf(
|
|
"%s (type to filter, ↑/↓ move, space select, enter confirm)\r\n",
|
|
colorize(.Option_Label, prompt),
|
|
)
|
|
|
|
fmt.printf("filter: %s\r\n", filter_text)
|
|
|
|
line_count := 1
|
|
|
|
if len(filtered) == 0 {
|
|
fmt.printf(" No matches\r\n")
|
|
return line_count + 1
|
|
}
|
|
|
|
end := scroll_offset + MAX_VISIBLE
|
|
if end > len(filtered) {
|
|
end = len(filtered)
|
|
}
|
|
|
|
for i in scroll_offset ..< end {
|
|
original := filtered[i]
|
|
checkbox := " "
|
|
if selected[original] {
|
|
checkbox = "x"
|
|
}
|
|
if i == cursor {
|
|
fmt.printf(
|
|
"%s [%s] %s\r\n",
|
|
colorize(.Caret, ">"),
|
|
colorize(.Sucess, checkbox),
|
|
options[original],
|
|
)
|
|
} else {
|
|
fmt.printf(" [%s] %s\r\n", colorize(.Sucess, checkbox), options[original])
|
|
}
|
|
}
|
|
|
|
return line_count + (end - scroll_offset)
|
|
}
|
|
|
|
enable_raw_mode :: proc(fd: posix.FD) -> (Raw_State, bool) {
|
|
state: Raw_State
|
|
state.fd = fd
|
|
|
|
if posix.tcgetattr(fd, &state.original) != .OK {
|
|
return state, false
|
|
}
|
|
|
|
attr: posix.termios = state.original
|
|
attr.c_lflag -= {.ICANON, .ECHO, .ISIG, .IEXTEN}
|
|
attr.c_iflag -= {.IXON, .ICRNL, .BRKINT, .INPCK, .ISTRIP}
|
|
attr.c_oflag -= {.OPOST}
|
|
attr.c_cflag += {.CS8}
|
|
attr.c_cc[.VMIN] = 1
|
|
attr.c_cc[.VTIME] = 0
|
|
|
|
if posix.tcsetattr(fd, .TCSAFLUSH, &attr) != .OK {
|
|
return state, false
|
|
}
|
|
|
|
return state, true
|
|
}
|
|
|
|
disable_raw_mode :: proc(state: ^Raw_State) {
|
|
posix.tcsetattr(state.fd, .TCSAFLUSH, &state.original)
|
|
}
|
|
|
|
read_key :: proc() -> (key: Key, ch: rune) {
|
|
buf: [3]u8
|
|
|
|
n := posix.read(posix.STDIN_FILENO, &buf[0], 1)
|
|
if n <= 0 {
|
|
return .Unknown, 0
|
|
}
|
|
|
|
switch buf[0] {
|
|
case ' ':
|
|
return .Space, 0
|
|
case '\n', '\r':
|
|
return .Enter, 0
|
|
case 0x03:
|
|
return .Escape, 0
|
|
case 0x08, 0x7F:
|
|
return .Backspace, 0
|
|
case 0x1b:
|
|
tv: posix.timeval
|
|
tv.tv_sec = 0
|
|
tv.tv_usec = posix.suseconds_t(100000)
|
|
|
|
set: posix.fd_set
|
|
posix.FD_ZERO(&set)
|
|
posix.FD_SET(posix.STDIN_FILENO, &set)
|
|
|
|
ready := posix.select(1, &set, nil, nil, &tv)
|
|
if ready <= 0 {
|
|
return .Escape, 0
|
|
}
|
|
|
|
n2 := posix.read(posix.STDIN_FILENO, &buf[1], 1)
|
|
if n2 <= 0 || buf[1] != '[' {
|
|
return .Escape, 0
|
|
}
|
|
|
|
posix.FD_ZERO(&set)
|
|
posix.FD_SET(posix.STDIN_FILENO, &set)
|
|
tv.tv_sec = 0
|
|
tv.tv_usec = posix.suseconds_t(100000)
|
|
|
|
ready = posix.select(1, &set, nil, nil, &tv)
|
|
if ready <= 0 {
|
|
return .Escape, 0
|
|
}
|
|
|
|
n3 := posix.read(posix.STDIN_FILENO, &buf[2], 1)
|
|
if n3 <= 0 {
|
|
return .Escape, 0
|
|
}
|
|
|
|
switch buf[2] {
|
|
case 'A':
|
|
return .Up, 0
|
|
case 'B':
|
|
return .Down, 0
|
|
case:
|
|
return .Escape, 0
|
|
}
|
|
case:
|
|
if buf[0] >= 0x20 && buf[0] <= 0x7E {
|
|
return .Char, rune(buf[0])
|
|
}
|
|
return .Unknown, 0
|
|
}
|
|
}
|
|
|