kitty/kittens/hints/main.go
2023-03-27 13:06:02 +05:30

328 lines
7.8 KiB
Go

// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package hints
import (
"fmt"
"io"
"os"
"strconv"
"strings"
"unicode"
"kitty/tools/cli"
"kitty/tools/tty"
"kitty/tools/tui"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/utils/style"
"kitty/tools/wcswidth"
)
var _ = fmt.Print
func convert_text(text string, cols int) string {
lines := make([]string, 0, 64)
empty_line := strings.Repeat("\x00", cols) + "\n"
s1 := utils.NewLineScanner(text)
for s1.Scan() {
full_line := s1.Text()
if full_line == "" {
continue
}
if strings.TrimRight(full_line, "\r") == "" {
for i := 0; i < len(full_line); i++ {
lines = append(lines, empty_line)
}
continue
}
appended := false
s2 := utils.NewSeparatorScanner(full_line, "\r")
for s2.Scan() {
line := s2.Text()
if line != "" {
line_sz := wcswidth.Stringwidth(line)
extra := cols - line_sz
if extra > 0 {
line += strings.Repeat("\x00", extra)
}
lines = append(lines, line)
lines = append(lines, "\r")
appended = true
}
}
if appended {
lines[len(lines)-1] = "\n"
}
}
ans := strings.Join(lines, "")
return strings.TrimRight(ans, "\r\n")
}
func parse_input(text string) string {
cols, err := strconv.Atoi(os.Getenv("OVERLAID_WINDOW_COLS"))
if err == nil {
return convert_text(text, cols)
}
term, err := tty.OpenControllingTerm()
if err == nil {
sz, err := term.GetSize()
term.Close()
if err == nil {
return convert_text(text, int(sz.Col))
}
}
return convert_text(text, 80)
}
type Result struct {
Match []string `json:"match"`
Programs []string `json:"programs"`
Multiple_joiner string `json:"multiple_joiner"`
Customize_processing string `json:"customize_processing"`
Type string `json:"type"`
Groupdicts []map[string]any `json:"groupdicts"`
Extra_cli_args []string `json:"extra_cli_args"`
Linenum_action string `json:"linenum_action"`
Cwd string `json:"cwd"`
}
func encode_hint(num int, alphabet string) (res string) {
runes := []rune(alphabet)
d := len(runes)
for res == "" || num > 0 {
res = string(runes[num%d]) + res
num /= d
}
return
}
func decode_hint(x string, alphabet string) (ans int) {
base := len(alphabet)
index_map := make(map[rune]int, len(alphabet))
for i, c := range alphabet {
index_map[c] = i
}
for _, char := range x {
ans = ans*base + index_map[char]
}
return
}
func main(_ *cli.Command, o *Options, args []string) (rc int, err error) {
output := tui.KittenOutputSerializer()
if tty.IsTerminal(os.Stdin.Fd()) {
tui.ReportError(fmt.Errorf("You must pass the text to be hinted on STDIN"))
return 1, nil
}
stdin, err := io.ReadAll(os.Stdin)
if err != nil {
tui.ReportError(fmt.Errorf("Failed to read from STDIN with error: %w", err))
return 1, nil
}
if len(args) > 0 && o.CustomizeProcessing == "" && o.Type != "linenum" {
tui.ReportError(fmt.Errorf("Extra command line arguments present: %s", strings.Join(args, " ")))
return 1, nil
}
input_text := parse_input(utils.UnsafeBytesToString(stdin))
text, all_marks, index_map, err := find_marks(input_text, o, os.Args[2:]...)
if err != nil {
tui.ReportError(err)
return 1, nil
}
result := Result{
Programs: o.Program, Multiple_joiner: o.MultipleJoiner, Customize_processing: o.CustomizeProcessing, Type: o.Type,
Extra_cli_args: args, Linenum_action: o.LinenumAction,
}
result.Cwd, _ = os.Getwd()
alphabet := o.Alphabet
if alphabet == "" {
alphabet = DEFAULT_HINT_ALPHABET
}
ignore_mark_indices := utils.NewSet[int](8)
window_title := o.WindowTitle
if window_title == "" {
switch o.Type {
case "url":
window_title = "Choose URL"
default:
window_title = "Choose text"
}
}
current_text := ""
current_input := ""
match_suffix := ""
switch o.AddTrailingSpace {
case "always":
match_suffix = " "
case "never":
default:
if o.Multiple {
match_suffix = " "
}
}
chosen := []*Mark{}
lp, err := loop.New(loop.NoAlternateScreen) // no alternate screen reduces flicker on exit
if err != nil {
return
}
fctx := style.Context{AllowEscapeCodes: true}
faint := fctx.SprintFunc("dim")
hint_style := fctx.SprintFunc(fmt.Sprintf("fg=%s bg=%s bold", o.HintsForegroundColor, o.HintsBackgroundColor))
text_style := fctx.SprintFunc(fmt.Sprintf("fg=bright-%s bold", o.HintsTextColor))
highlight_mark := func(m *Mark, mark_text string) string {
hint := encode_hint(m.Index, alphabet)
if current_input != "" && !strings.HasPrefix(hint, current_input) {
return faint(mark_text)
}
hint = hint[len(current_input):]
if hint == "" {
hint = " "
}
mark_text = mark_text[len(hint):]
return hint_style(hint) + text_style(mark_text)
}
render := func() string {
ans := text
for i := len(all_marks) - 1; i >= 0; i-- {
mark := &all_marks[i]
if ignore_mark_indices.Has(mark.Index) {
continue
}
mtext := highlight_mark(mark, ans[mark.Start:mark.End])
ans = ans[:mark.Start] + mtext + ans[mark.End:]
}
ans = strings.ReplaceAll(ans, "\x00", "")
return strings.TrimRightFunc(strings.NewReplacer("\r", "\r\n", "\n", "\r\n").Replace(ans), unicode.IsSpace)
}
draw_screen := func() {
lp.StartAtomicUpdate()
defer lp.EndAtomicUpdate()
if current_text == "" {
current_text = render()
}
lp.ClearScreen()
lp.QueueWriteString(current_text)
}
reset := func() {
current_input = ""
current_text = ""
}
lp.OnInitialize = func() (string, error) {
lp.SendOverlayReady()
lp.SetCursorVisible(false)
lp.SetWindowTitle(window_title)
lp.AllowLineWrapping(false)
draw_screen()
return "", nil
}
lp.OnFinalize = func() string {
lp.SetCursorVisible(true)
return ""
}
lp.OnResize = func(old_size, new_size loop.ScreenSize) error {
draw_screen()
return nil
}
lp.OnText = func(text string, _, _ bool) error {
changed := false
for _, ch := range text {
if strings.ContainsRune(alphabet, ch) {
current_input += string(ch)
changed = true
}
}
if changed {
matches := []*Mark{}
for idx, m := range index_map {
if eh := encode_hint(idx, alphabet); strings.HasPrefix(eh, current_input) {
matches = append(matches, m)
}
}
if len(matches) == 1 {
chosen = append(chosen, matches[0])
if o.Multiple {
ignore_mark_indices.Add(matches[0].Index)
reset()
} else {
lp.Quit(0)
return nil
}
}
current_text = ""
draw_screen()
}
return nil
}
lp.OnKeyEvent = func(ev *loop.KeyEvent) error {
if ev.MatchesPressOrRepeat("backspace") {
ev.Handled = true
r := []rune(current_input)
if len(r) > 0 {
r = r[:len(r)-1]
current_input = string(r)
current_text = ""
}
draw_screen()
} else if ev.MatchesPressOrRepeat("enter") || ev.MatchesPressOrRepeat("space") {
ev.Handled = true
if current_input != "" {
idx := decode_hint(current_input, alphabet)
if m := index_map[idx]; m != nil {
chosen = append(chosen, m)
ignore_mark_indices.Add(idx)
if o.Multiple {
reset()
draw_screen()
} else {
lp.Quit(0)
}
} else {
current_input = ""
current_text = ""
draw_screen()
}
}
} else if ev.MatchesPressOrRepeat("esc") {
if o.Multiple {
lp.Quit(0)
} else {
lp.Quit(1)
}
}
return nil
}
err = lp.Run()
if err != nil {
return 1, err
}
ds := lp.DeathSignalName()
if ds != "" {
fmt.Println("Killed by signal: ", ds)
lp.KillIfSignalled()
return 1, nil
}
if lp.ExitCode() != 0 {
return lp.ExitCode(), nil
}
result.Match = make([]string, len(chosen))
result.Groupdicts = make([]map[string]any, len(chosen))
for i, m := range chosen {
result.Match[i] = m.Text + match_suffix
result.Groupdicts[i] = m.Groupdict
}
fmt.Println(output(result))
return
}
func EntryPoint(parent *cli.Command) {
create_cmd(parent, main)
}