<commit_before>// Copyright 2020 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package menu displays a Terminal UI based text menu to choose boot options
// from.
package menu

import (
	"fmt"
	"io"
	"log"
	"os"
	"os/signal"
	"strconv"
	"syscall"
	"time"

	"github.com/u-root/u-root/pkg/boot"
	"github.com/u-root/u-root/pkg/sh"
	"golang.org/x/crypto/ssh/terminal"
	"golang.org/x/sys/unix"
)

var (
	initialTimeout    = 10 * time.Second
	subsequentTimeout = 60 * time.Second
)

// Entry is a menu entry.
type Entry interface {
	// Label is the string displayed to the user in the menu.
	Label() string

	// Edit the kernel command line if possible. Must be called prior to
	// Load.
	Edit(func(cmdline string) string)

	// Load is called when the entry is chosen, but does not transfer
	// execution to another process or kernel.
	Load() error

	// Exec transfers execution to another process or kernel.
	//
	// Exec either returns an error or does not return at all.
	Exec() error

	// IsDefault indicates that this action should be run by default if the
	// user didn't make an entry choice.
	IsDefault() bool
}

func parseBootNum(choice string, entries []Entry) (int, error) {
	num, err := strconv.Atoi(choice)
	if err != nil {
		return -1, fmt.Errorf("%s is not a valid entry number: %v", choice, err)
	}
	if num < 1 || num > len(entries) {
		return -1, fmt.Errorf("%s is not a valid entry number", choice)
	}
	return num, nil
}

// Choose presents the user a menu on input to choose an entry from and returns that entry.
func Choose(input *os.File, entries ...Entry) Entry {
	fmt.Println("")
	for i, e := range entries {
		fmt.Printf("%02d. %s\n\n", i+1, e.Label())
	}
	fmt.Println("\r")

	oldState, err := terminal.MakeRaw(int(input.Fd()))
	if err != nil {
		log.Printf("BUG: Please report: We cannot actually let you choose from menu (MakeRaw failed): %v", err)
		return nil
	}
	defer terminal.Restore(int(input.Fd()), oldState)

	// TODO(chrisko): reduce this timeout a la GRUB. 3 seconds, and hitting
	// any button resets the timeout. We could save 7 seconds here.
	t := time.NewTimer(initialTimeout)

	boot := make(chan Entry, 1)

	go func() {
		// Read exactly one line.
		term := terminal.NewTerminal(input, "")

		term.AutoCompleteCallback = func(line string, pos int, key rune) (string, int, bool) {
			// We ain't gonna autocomplete, but we'll reset the countdown timer when you press a key.
			t.Reset(subsequentTimeout)
			return "", 0, false
		}

		for {
			term.SetPrompt("Enter an option ('01' is the default, 'e' to edit kernel cmdline):\n > ")
			choice, err := term.ReadLine()
			if err != nil {
				if err != io.EOF {
					fmt.Printf("BUG: Please report: Terminal read error: %v.\n", err)
				}
				boot <- nil
				return
			}

			if choice == "e" {
				// Edit command line.
				term.SetPrompt("Select a boot option to edit:\n > ")
				choice, err := term.ReadLine()
				if err != nil {
					fmt.Fprintln(term, err)
					fmt.Fprintln(term, "Returning to main menu...")
					continue
				}
				num, err := parseBootNum(choice, entries)
				if err != nil {
					fmt.Fprintln(term, err)
					fmt.Fprintln(term, "Returning to main menu...")
					continue
				}
				entries[num-1].Edit(func(cmdline string) string {
					fmt.Fprintf(term, "The current quoted cmdline for option %d is:\n > %q\n", num, cmdline)
					fmt.Fprintln(term, ` * Note the cmdline is c-style quoted. Ex: \n => newline, \\ => \`)
					term.SetPrompt("Enter an option:\n * (a)ppend, (o)verwrite, (r)eturn to main menu\n > ")
					choice, err := term.ReadLine()
					if err != nil {
						fmt.Fprintln(term, err)
						return cmdline
					}
					switch choice {
					case "a":
						term.SetPrompt("Enter unquoted cmdline to append:\n > ")
						appendCmdline, err := term.ReadLine()
						if err != nil {
							fmt.Fprintln(term, err)
							return cmdline
						}
						if appendCmdline != "" {
							cmdline += " " + appendCmdline
						}
					case "o":
						term.SetPrompt("Enter new unquoted cmdline:\n > ")
						newCmdline, err := term.ReadLine()
						if err != nil {
							fmt.Fprintln(term, err)
							return cmdline
						}
						cmdline = newCmdline
					case "r":
					default:
						fmt.Fprintf(term, "Unrecognized choice %q", choice)
					}
					fmt.Fprintf(term, "The new quoted cmdline for option %d is:\n > %q\n", num, cmdline)
					return cmdline
				})
				fmt.Fprintln(term, "Returning to main menu...")
				continue
			}
			if choice == "" {
				// nil will result in the default order.
				boot <- nil
				return
			}
			num, err := parseBootNum(choice, entries)<commit_msg>menu: Put \r\n back into menu prints<commit_after>// Copyright 2020 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package menu displays a Terminal UI based text menu to choose boot options
// from.
package menu

import (
	"fmt"
	"io"
	"log"
	"os"
	"os/signal"
	"strconv"
	"syscall"
	"time"

	"github.com/u-root/u-root/pkg/boot"
	"github.com/u-root/u-root/pkg/sh"
	"golang.org/x/crypto/ssh/terminal"
	"golang.org/x/sys/unix"
)

var (
	initialTimeout    = 10 * time.Second
	subsequentTimeout = 60 * time.Second
)

// Entry is a menu entry.
type Entry interface {
	// Label is the string displayed to the user in the menu.
	Label() string

	// Edit the kernel command line if possible. Must be called prior to
	// Load.
	Edit(func(cmdline string) string)

	// Load is called when the entry is chosen, but does not transfer
	// execution to another process or kernel.
	Load() error

	// Exec transfers execution to another process or kernel.
	//
	// Exec either returns an error or does not return at all.
	Exec() error

	// IsDefault indicates that this action should be run by default if the
	// user didn't make an entry choice.
	IsDefault() bool
}

func parseBootNum(choice string, entries []Entry) (int, error) {
	num, err := strconv.Atoi(choice)
	if err != nil {
		return -1, fmt.Errorf("%s is not a valid entry number: %v", choice, err)
	}
	if num < 1 || num > len(entries) {
		return -1, fmt.Errorf("%s is not a valid entry number", choice)
	}
	return num, nil
}

// Choose presents the user a menu on input to choose an entry from and returns that entry.
func Choose(input *os.File, entries ...Entry) Entry {
	fmt.Println("")
	for i, e := range entries {
		fmt.Printf("%02d. %s\n\n", i+1, e.Label())
	}
	fmt.Println("\r")

	oldState, err := terminal.MakeRaw(int(input.Fd()))
	if err != nil {
		log.Printf("BUG: Please report: We cannot actually let you choose from menu (MakeRaw failed): %v", err)
		return nil
	}
	defer terminal.Restore(int(input.Fd()), oldState)

	// TODO(chrisko): reduce this timeout a la GRUB. 3 seconds, and hitting
	// any button resets the timeout. We could save 7 seconds here.
	t := time.NewTimer(initialTimeout)

	boot := make(chan Entry, 1)

	go func() {
		// Note that term is in raw mode. Write \r\n whenever you would
		// write a \n. When testing in qemu, it might look fine because
		// there might be another tty cooking the newlines. In for
		// example minicom, the behavior is different. And you would
		// see something like:
		//
		//     Select a boot option to edit:
		//                                  >
		//
		// Instead of:
		//
		//     Select a boot option to edit:
		//      >
		term := terminal.NewTerminal(input, "")

		term.AutoCompleteCallback = func(line string, pos int, key rune) (string, int, bool) {
			// We ain't gonna autocomplete, but we'll reset the countdown timer when you press a key.
			t.Reset(subsequentTimeout)
			return "", 0, false
		}

		for {
			term.SetPrompt("Enter an option ('01' is the default, 'e' to edit kernel cmdline):\r\n > ")
			choice, err := term.ReadLine()
			if err != nil {
				if err != io.EOF {
					fmt.Printf("BUG: Please report: Terminal read error: %v.\n", err)
				}
				boot <- nil
				return
			}

			if choice == "e" {
				// Edit command line.
				term.SetPrompt("Select a boot option to edit:\r\n > ")
				choice, err := term.ReadLine()
				if err != nil {
					fmt.Fprintln(term, err)
					fmt.Fprintln(term, "Returning to main menu...")
					continue
				}
				num, err := parseBootNum(choice, entries)
				if err != nil {
					fmt.Fprintln(term, err)
					fmt.Fprintln(term, "Returning to main menu...")
					continue
				}
				entries[num-1].Edit(func(cmdline string) string {
					fmt.Fprintf(term, "The current quoted cmdline for option %d is:\r\n > %q\r\n", num, cmdline)
					fmt.Fprintln(term, ` * Note the cmdline is c-style quoted. Ex: \n => newline, \\ => \`)
					term.SetPrompt("Enter an option:\r\n * (a)ppend, (o)verwrite, (r)eturn to main menu\r\n > ")
					choice, err := term.ReadLine()
					if err != nil {
						fmt.Fprintln(term, err)
						return cmdline
					}
					switch choice {
					case "a":
						term.SetPrompt("Enter unquoted cmdline to append:\r\n > ")
						appendCmdline, err := term.ReadLine()
						if err != nil {
							fmt.Fprintln(term, err)
							return cmdline
						}
						if appendCmdline != "" {
							cmdline += " " + appendCmdline
						}
					case "o":
						term.SetPrompt("Enter new unquoted cmdline:\r\n > ")
						newCmdline, err := term.ReadLine()
						if err != nil {
							fmt.Fprintln(term, err)
							return cmdline
						}
						cmdline = newCmdline
					case "r":
					default:
						fmt.Fprintf(term, "Unrecognized choice %q", choice)
					}
					fmt.Fprintf(term, "The new quoted cmdline for option %d is:\r\n > %q\r\n", num, cmdline)
					return cmdline
				})
				fmt.Fprintln(term, "Returning to main menu...")
				continue
			}
			if choice == "" {
				// nil will result in the default order.
				boot <- nil
				return
			}
			num, err := parseBootNum(choice, entries)
{'fraction_non_alphanumeric': 0.12926482534524777, 'fraction_numerical': 0.004264825345247766, 'mean_word_length': 2.632976761342678, 'pattern_counts': {'":': 6, '<': 9, '<?xml version=': 0, '>': 25, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4899029', 'n_tokens_mistral': 3865, 'n_tokens_neox': 3210, 'n_words': 1131}