Start work on porting the ask kitten
This commit is contained in:
parent
672ecde68b
commit
0aa55fb755
@ -539,3 +539,11 @@ if __name__ == '__main__':
|
|||||||
if ans:
|
if ans:
|
||||||
import json
|
import json
|
||||||
print(json.dumps(ans))
|
print(json.dumps(ans))
|
||||||
|
elif __name__ == '__doc__':
|
||||||
|
import sys
|
||||||
|
|
||||||
|
cd = sys.cli_docs # type: ignore
|
||||||
|
cd['usage'] = ''
|
||||||
|
cd['options'] = option_text
|
||||||
|
cd['help_text'] = 'Ask the user for input'
|
||||||
|
cd['short_desc'] = 'Ask the user for input'
|
||||||
|
|||||||
@ -24,7 +24,7 @@ exec_kitty() {
|
|||||||
|
|
||||||
|
|
||||||
is_wrapped_kitten() {
|
is_wrapped_kitten() {
|
||||||
wrapped_kittens="clipboard icat hyperlinked_grep unicode_input ssh"
|
wrapped_kittens="clipboard icat hyperlinked_grep ask unicode_input ssh"
|
||||||
[ -n "$1" ] && {
|
[ -n "$1" ] && {
|
||||||
case " $wrapped_kittens " in
|
case " $wrapped_kittens " in
|
||||||
*" $1 "*) printf "%s" "$1" ;;
|
*" $1 "*) printf "%s" "$1" ;;
|
||||||
|
|||||||
434
tools/cmd/ask/choices.go
Normal file
434
tools/cmd/ask/choices.go
Normal file
@ -0,0 +1,434 @@
|
|||||||
|
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
package ask
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"kitty/tools/cli/markup"
|
||||||
|
"kitty/tools/tui/loop"
|
||||||
|
"kitty/tools/utils"
|
||||||
|
"kitty/tools/utils/style"
|
||||||
|
"kitty/tools/wcswidth"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = fmt.Print
|
||||||
|
|
||||||
|
type Choice struct {
|
||||||
|
text string
|
||||||
|
idx int
|
||||||
|
color, letter string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self Choice) prefix() string {
|
||||||
|
return string([]rune(self.text)[:self.idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self Choice) display_letter() string {
|
||||||
|
return string([]rune(self.text)[self.idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self Choice) suffix() string {
|
||||||
|
return string([]rune(self.text)[self.idx+1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
type Range struct {
|
||||||
|
start, end, y int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Range) has_point(x, y int) bool {
|
||||||
|
return y == self.y && self.start <= x && x <= self.end
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncate_at_space(text string, width int) (string, string) {
|
||||||
|
truncated, p := wcswidth.TruncateToVisualLengthWithWidth(text, width)
|
||||||
|
if p >= len(text) {
|
||||||
|
return text, ""
|
||||||
|
}
|
||||||
|
i := strings.LastIndexByte(truncated, ' ')
|
||||||
|
if i > 0 && p-i < 12 {
|
||||||
|
p = i + 1
|
||||||
|
}
|
||||||
|
return text[:p], text[p:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func extra_for(width, screen_width int) int {
|
||||||
|
return utils.Max(0, screen_width-width)/2 + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func choices(o *Options, items []string) (ans map[string]any, err error) {
|
||||||
|
response := ""
|
||||||
|
lp, err := loop.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lp.MouseTrackingMode(loop.BUTTONS_ONLY_MOUSE_TRACKING)
|
||||||
|
|
||||||
|
prefix_style_pat := regexp.MustCompile("^(?:\x1b\\[[^m]*?m)+")
|
||||||
|
choice_order := make([]Choice, len(o.Choices))
|
||||||
|
clickable_ranges := make(map[string][]Range, 16)
|
||||||
|
allowed := utils.NewSet[string](utils.Max(2, len(o.Choices)))
|
||||||
|
response_on_accept := o.Default
|
||||||
|
switch o.Type {
|
||||||
|
case "yesno":
|
||||||
|
allowed.AddItems("y", "n")
|
||||||
|
if !allowed.Has(response_on_accept) {
|
||||||
|
response_on_accept = "y"
|
||||||
|
}
|
||||||
|
case "choices":
|
||||||
|
first_choice := ""
|
||||||
|
for i, x := range o.Choices {
|
||||||
|
letter, text, _ := strings.Cut(x, ":")
|
||||||
|
color := ""
|
||||||
|
if strings.Contains(letter, ";") {
|
||||||
|
letter, color, _ = strings.Cut(letter, ";")
|
||||||
|
}
|
||||||
|
letter = strings.ToLower(letter)
|
||||||
|
idx := strings.Index(strings.ToLower(text), letter)
|
||||||
|
idx = len([]rune(strings.ToLower(text)[:idx]))
|
||||||
|
allowed.Add(letter)
|
||||||
|
c := Choice{text: text, idx: idx, color: color, letter: letter}
|
||||||
|
choice_order = append(choice_order, c)
|
||||||
|
if i == 0 {
|
||||||
|
first_choice = letter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !allowed.Has(response_on_accept) {
|
||||||
|
response_on_accept = first_choice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
message := o.Message
|
||||||
|
hidden_text_start_pos := -1
|
||||||
|
hidden_text_end_pos := -1
|
||||||
|
hidden_text := ""
|
||||||
|
m := markup.New(true)
|
||||||
|
replacement_text := fmt.Sprintf("Press %s or click to show", m.Green(o.UnhideKey))
|
||||||
|
replacement_range := Range{-1, -1, -1}
|
||||||
|
if message != "" && o.HiddenTextPlaceholder != "" {
|
||||||
|
hidden_text_start_pos = strings.Index(message, o.HiddenTextPlaceholder)
|
||||||
|
if hidden_text_start_pos > -1 {
|
||||||
|
raw, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to read hidden text from STDIN: %w", err)
|
||||||
|
}
|
||||||
|
hidden_text = strings.TrimRightFunc(utils.UnsafeBytesToString(raw), unicode.IsSpace)
|
||||||
|
hidden_text_end_pos = hidden_text_start_pos + len(replacement_text)
|
||||||
|
suffix := message[hidden_text_start_pos+len(o.HiddenTextPlaceholder):]
|
||||||
|
message = message[:hidden_text_start_pos] + replacement_text + suffix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_long_text := func(screen_width int, text string, msg_lines []string) []string {
|
||||||
|
if text == "" {
|
||||||
|
msg_lines = append(msg_lines, "")
|
||||||
|
} else {
|
||||||
|
width := screen_width - 2
|
||||||
|
prefix := prefix_style_pat.FindString(text)
|
||||||
|
for text != "" {
|
||||||
|
var t string
|
||||||
|
t, text = truncate_at_space(text, width)
|
||||||
|
t = strings.TrimSpace(t)
|
||||||
|
msg_lines = append(msg_lines, strings.Repeat(" ", extra_for(wcswidth.Stringwidth(t), width))+m.Bold(prefix+t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return msg_lines
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := style.Context{AllowEscapeCodes: true}
|
||||||
|
|
||||||
|
draw_choice_boxes := func(y, screen_width, screen_height int, choices ...Choice) {
|
||||||
|
clickable_ranges = map[string][]Range{}
|
||||||
|
width := screen_width - 2
|
||||||
|
current_line_length := 0
|
||||||
|
type Item struct{ letter, text string }
|
||||||
|
type Line = []Item
|
||||||
|
var current_line Line
|
||||||
|
lines := make([]Line, 0, 32)
|
||||||
|
sep := " "
|
||||||
|
sep_sz := len(sep) + 2 // for the borders
|
||||||
|
|
||||||
|
for _, choice := range choices {
|
||||||
|
clickable_ranges[choice.letter] = make([]Range, 0, 8)
|
||||||
|
text := " " + choice.prefix()
|
||||||
|
color := choice.color
|
||||||
|
if choice.color == "" {
|
||||||
|
color = "green"
|
||||||
|
}
|
||||||
|
text += ctx.SprintFunc("fg=" + color)(choice.display_letter())
|
||||||
|
text += choice.suffix() + " "
|
||||||
|
sz := wcswidth.Stringwidth(text)
|
||||||
|
if sz+sep_sz+current_line_length > width {
|
||||||
|
lines = append(lines, current_line)
|
||||||
|
current_line = nil
|
||||||
|
current_line_length = 0
|
||||||
|
}
|
||||||
|
current_line = append(current_line, Item{choice.letter, text})
|
||||||
|
current_line_length += sz + sep_sz
|
||||||
|
}
|
||||||
|
if len(current_line) > 0 {
|
||||||
|
lines = append(lines, current_line)
|
||||||
|
}
|
||||||
|
|
||||||
|
highlight := func(text string) string {
|
||||||
|
return m.Yellow(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
top := func(text string, highlight_frame bool) (ans string) {
|
||||||
|
ans = "╭" + strings.Repeat("─", wcswidth.Stringwidth(text)) + "╮"
|
||||||
|
if highlight_frame {
|
||||||
|
ans = highlight(ans)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
middle := func(text string, highlight_frame bool) (ans string) {
|
||||||
|
f := "│"
|
||||||
|
if highlight_frame {
|
||||||
|
f = highlight(f)
|
||||||
|
}
|
||||||
|
return f + text + f
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom := func(text string, highlight_frame bool) (ans string) {
|
||||||
|
ans = "╰" + strings.Repeat("─", wcswidth.Stringwidth(text)) + "╯"
|
||||||
|
if highlight_frame {
|
||||||
|
ans = highlight(ans)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
print_line := func(add_borders func(string, bool) string, is_last bool, items ...Item) {
|
||||||
|
type Position struct {
|
||||||
|
letter string
|
||||||
|
x, size int
|
||||||
|
}
|
||||||
|
texts := make([]string, 0, 8)
|
||||||
|
positions := make([]Position, 0, 8)
|
||||||
|
x := 0
|
||||||
|
for _, item := range items {
|
||||||
|
text := item.text
|
||||||
|
positions = append(positions, Position{item.letter, x, wcswidth.Stringwidth(text) + 2})
|
||||||
|
text = add_borders(text, item.letter == response_on_accept)
|
||||||
|
text += sep
|
||||||
|
x += wcswidth.Stringwidth(text)
|
||||||
|
texts = append(texts, text)
|
||||||
|
}
|
||||||
|
line := strings.TrimRightFunc(strings.Join(texts, ""), unicode.IsSpace)
|
||||||
|
offset := extra_for(wcswidth.Stringwidth(line), width)
|
||||||
|
for _, pos := range positions {
|
||||||
|
x += offset
|
||||||
|
clickable_ranges[pos.letter] = append(clickable_ranges[pos.letter], Range{x, x + pos.size - 1, y})
|
||||||
|
}
|
||||||
|
end := "\r\n"
|
||||||
|
if is_last {
|
||||||
|
end = ""
|
||||||
|
}
|
||||||
|
lp.QueueWriteString(strings.Repeat(" ", offset) + line + end)
|
||||||
|
y++
|
||||||
|
}
|
||||||
|
lp.AllowLineWrapping(false)
|
||||||
|
defer func() { lp.AllowLineWrapping(true) }()
|
||||||
|
for i, boxed_line := range lines {
|
||||||
|
print_line(top, false, boxed_line...)
|
||||||
|
print_line(middle, false, boxed_line...)
|
||||||
|
is_last := i == len(lines)-1
|
||||||
|
print_line(bottom, is_last, boxed_line...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_yesno := func(y, screen_width, screen_height int) {
|
||||||
|
yes := m.Green("Y") + "es"
|
||||||
|
no := m.BrightRed("N") + "o"
|
||||||
|
if y+3 <= screen_height {
|
||||||
|
draw_choice_boxes(y, screen_width, screen_height, Choice{"Yes", 0, "green", "y"}, Choice{"No", 0, "red", "n"})
|
||||||
|
} else {
|
||||||
|
sep := strings.Repeat(" ", 3)
|
||||||
|
text := yes + sep + no
|
||||||
|
w := wcswidth.Stringwidth(text)
|
||||||
|
x := extra_for(w, screen_width-2)
|
||||||
|
nx := x + wcswidth.Stringwidth(yes) + len(sep)
|
||||||
|
clickable_ranges = map[string][]Range{
|
||||||
|
"y": {{x, x + wcswidth.Stringwidth(yes) - 1, y}},
|
||||||
|
"n": {{nx, nx + wcswidth.Stringwidth(no) - 1, y}},
|
||||||
|
}
|
||||||
|
lp.QueueWriteString(strings.Repeat(" ", x) + text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_choice := func(y, screen_width, screen_height int) {
|
||||||
|
if y+3 <= screen_height {
|
||||||
|
draw_choice_boxes(y, screen_width, screen_height, choice_order...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clickable_ranges = map[string][]Range{}
|
||||||
|
current_line := ""
|
||||||
|
current_ranges := map[string]int{}
|
||||||
|
width := screen_width - 2
|
||||||
|
|
||||||
|
commit_line := func(add_newline bool) {
|
||||||
|
x := extra_for(wcswidth.Stringwidth(current_line), width)
|
||||||
|
text := strings.Repeat(" ", x) + current_line
|
||||||
|
if add_newline {
|
||||||
|
lp.Println(text)
|
||||||
|
} else {
|
||||||
|
lp.QueueWriteString(text)
|
||||||
|
}
|
||||||
|
for letter, sz := range current_ranges {
|
||||||
|
clickable_ranges[letter] = []Range{{x, x + sz - 3, y}}
|
||||||
|
x += sz
|
||||||
|
}
|
||||||
|
current_ranges = map[string]int{}
|
||||||
|
y++
|
||||||
|
current_line = ""
|
||||||
|
}
|
||||||
|
for _, choice := range choice_order {
|
||||||
|
text := choice.prefix()
|
||||||
|
spec := ""
|
||||||
|
if choice.color != "" {
|
||||||
|
spec = "fg=" + choice.color
|
||||||
|
} else {
|
||||||
|
spec = "fg=green"
|
||||||
|
}
|
||||||
|
if choice.letter == response_on_accept {
|
||||||
|
spec += " u=straight"
|
||||||
|
}
|
||||||
|
text += ctx.SprintFunc(spec)(choice.display_letter())
|
||||||
|
text += choice.suffix()
|
||||||
|
text += " "
|
||||||
|
sz := wcswidth.Stringwidth(text)
|
||||||
|
if sz+wcswidth.Stringwidth(current_line) >= width {
|
||||||
|
commit_line(true)
|
||||||
|
}
|
||||||
|
current_line += text
|
||||||
|
current_ranges[choice.letter] = sz
|
||||||
|
}
|
||||||
|
if current_line != "" {
|
||||||
|
commit_line(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_screen := func() error {
|
||||||
|
lp.StartAtomicUpdate()
|
||||||
|
defer lp.EndAtomicUpdate()
|
||||||
|
lp.ClearScreen()
|
||||||
|
msg_lines := make([]string, 0, 8)
|
||||||
|
sz, err := lp.ScreenSize()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if message != "" {
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(message))
|
||||||
|
for scanner.Scan() {
|
||||||
|
msg_lines = draw_long_text(int(sz.WidthCells), scanner.Text(), msg_lines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
y := int(sz.HeightCells) - len(msg_lines)
|
||||||
|
y = utils.Max(0, (y/2)-2)
|
||||||
|
lp.QueueWriteString(strings.Repeat("\r\n", y))
|
||||||
|
for _, line := range msg_lines {
|
||||||
|
if replacement_text != "" {
|
||||||
|
idx := strings.Index(line, replacement_text)
|
||||||
|
if idx > -1 {
|
||||||
|
x := wcswidth.Stringwidth(line[:idx])
|
||||||
|
replacement_range = Range{x, x + wcswidth.Stringwidth(replacement_text), y}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lp.Println(line)
|
||||||
|
y++
|
||||||
|
}
|
||||||
|
if sz.HeightCells > 2 {
|
||||||
|
lp.Println()
|
||||||
|
y++
|
||||||
|
}
|
||||||
|
switch o.Type {
|
||||||
|
case "yesno":
|
||||||
|
draw_yesno(y, int(sz.WidthCells), int(sz.HeightCells))
|
||||||
|
case "choices":
|
||||||
|
draw_choice(y, int(sz.WidthCells), int(sz.HeightCells))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
unhide := func() {
|
||||||
|
if hidden_text != "" && message != "" {
|
||||||
|
message = message[:hidden_text_start_pos] + hidden_text + message[hidden_text_end_pos:]
|
||||||
|
hidden_text = ""
|
||||||
|
draw_screen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.OnInitialize = func() (string, error) {
|
||||||
|
lp.SetCursorVisible(false)
|
||||||
|
return "", draw_screen()
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.OnFinalize = func() string {
|
||||||
|
lp.SetCursorVisible(true)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.OnText = func(text string, from_key_event, in_bracketed_paste bool) error {
|
||||||
|
text = strings.ToLower(text)
|
||||||
|
if allowed.Has(text) {
|
||||||
|
response = text
|
||||||
|
lp.Quit(0)
|
||||||
|
} else if hidden_text != "" && text == o.UnhideKey {
|
||||||
|
unhide()
|
||||||
|
} else if o.Type == "yesno" {
|
||||||
|
lp.Quit(1)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.OnKeyEvent = func(ev *loop.KeyEvent) error {
|
||||||
|
if ev.MatchesPressOrRepeat("esc") || ev.MatchesPressOrRepeat("ctrl+c") {
|
||||||
|
ev.Handled = true
|
||||||
|
lp.Quit(1)
|
||||||
|
} else if ev.MatchesPressOrRepeat("enter") {
|
||||||
|
ev.Handled = true
|
||||||
|
response = response_on_accept
|
||||||
|
lp.Quit(0)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.OnMouseEvent = func(ev *loop.MouseEvent) error {
|
||||||
|
if ev.Event_type == loop.MOUSE_CLICK {
|
||||||
|
for letter, ranges := range clickable_ranges {
|
||||||
|
for _, r := range ranges {
|
||||||
|
if r.has_point(ev.Cell.X, ev.Cell.Y) {
|
||||||
|
response = letter
|
||||||
|
lp.Quit(0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hidden_text != "" && replacement_range.has_point(ev.Cell.X, ev.Cell.Y) {
|
||||||
|
unhide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.OnResize = func(old, news loop.ScreenSize) error {
|
||||||
|
return draw_screen()
|
||||||
|
}
|
||||||
|
|
||||||
|
err = lp.Run()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ds := lp.DeathSignalName()
|
||||||
|
if ds != "" {
|
||||||
|
fmt.Println("Killed by signal: ", ds)
|
||||||
|
lp.KillIfSignalled()
|
||||||
|
return nil, fmt.Errorf("Filled by signal: %s", ds)
|
||||||
|
}
|
||||||
|
ans = map[string]any{"items": items, "response": response}
|
||||||
|
return
|
||||||
|
}
|
||||||
39
tools/cmd/ask/main.go
Normal file
39
tools/cmd/ask/main.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
package ask
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"kitty/tools/cli"
|
||||||
|
"kitty/tools/tui"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = fmt.Print
|
||||||
|
|
||||||
|
func main(_ *cli.Command, o *Options, args []string) (rc int, err error) {
|
||||||
|
output := tui.KittenOutputSerializer()
|
||||||
|
var result any
|
||||||
|
switch o.Type {
|
||||||
|
case "yesno", "choices":
|
||||||
|
result, err = choices(o, args)
|
||||||
|
if err != nil {
|
||||||
|
return rc, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return 1, fmt.Errorf("Unknown type: %s", o.Type)
|
||||||
|
}
|
||||||
|
s, err := output(result)
|
||||||
|
if err != nil {
|
||||||
|
return 1, err
|
||||||
|
}
|
||||||
|
_, err = fmt.Println(s)
|
||||||
|
if err != nil {
|
||||||
|
return 1, err
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func EntryPoint(parent *cli.Command) {
|
||||||
|
create_cmd(parent, main)
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"kitty/tools/cli"
|
"kitty/tools/cli"
|
||||||
|
"kitty/tools/cmd/ask"
|
||||||
"kitty/tools/cmd/at"
|
"kitty/tools/cmd/at"
|
||||||
"kitty/tools/cmd/clipboard"
|
"kitty/tools/cmd/clipboard"
|
||||||
"kitty/tools/cmd/edit_in_kitty"
|
"kitty/tools/cmd/edit_in_kitty"
|
||||||
@ -39,6 +40,8 @@ func KittyToolEntryPoints(root *cli.Command) {
|
|||||||
unicode_input.EntryPoint(root)
|
unicode_input.EntryPoint(root)
|
||||||
// hyperlinked_grep
|
// hyperlinked_grep
|
||||||
hyperlinked_grep.EntryPoint(root)
|
hyperlinked_grep.EntryPoint(root)
|
||||||
|
// ask
|
||||||
|
ask.EntryPoint(root)
|
||||||
// __pytest__
|
// __pytest__
|
||||||
pytest.EntryPoint(root)
|
pytest.EntryPoint(root)
|
||||||
// __hold_till_enter__
|
// __hold_till_enter__
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user