From 72d9d6f8d4b0842e8042757fbf8c202142184863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Sat, 23 Jan 2021 21:15:17 +0100 Subject: [PATCH] Add user prompt/confirmation utilities --- adapter/fzf/finder.go | 4 +- adapter/tty/prompt.go | 84 ++++++++++++++++++++++++++++++++++++++ adapter/tty/styler.go | 25 +++++------- adapter/tty/styler_test.go | 17 ++++---- adapter/tty/tty.go | 9 ++++ cmd/container.go | 10 ++--- cmd/list.go | 2 +- core/style/style.go | 9 +++- main.go | 10 +++++ 9 files changed, 136 insertions(+), 34 deletions(-) create mode 100644 adapter/tty/prompt.go create mode 100644 adapter/tty/tty.go diff --git a/adapter/fzf/finder.go b/adapter/fzf/finder.go index 40efa84..5f07f32 100644 --- a/adapter/fzf/finder.go +++ b/adapter/fzf/finder.go @@ -46,8 +46,8 @@ func (f *NoteFinder) Find(opts note.FinderOpts) ([]note.Match, error) { for _, match := range matches { fzf.Add([]string{ match.Path, - f.styler.MustStyle(match.Title, style.Rule("yellow")), - f.styler.MustStyle(stringsutil.JoinLines(match.Body), style.Rule("faint")), + f.styler.MustStyle(match.Title, style.RuleYellow), + f.styler.MustStyle(stringsutil.JoinLines(match.Body), style.RuleBlack), }) } diff --git a/adapter/tty/prompt.go b/adapter/tty/prompt.go new file mode 100644 index 0000000..a13619b --- /dev/null +++ b/adapter/tty/prompt.go @@ -0,0 +1,84 @@ +package tty + +import ( + "fmt" + "strings" + + "github.com/mickael-menu/zk/core/style" +) + +// PromptOpt holds metadata about a possible prompt response. +type PromptOpt struct { + // Default value for the response. + Label string + // Short description explaining this response. + Description string + // All recognized values for this response. + AllowedResponses []string +} + +// Prompt displays a message and waits for the user to input one of the +// available options. +// Returns the selected option index. +func (t *TTY) Prompt(msg string, defaultOpt int, options []PromptOpt) int { + responses := "" + for i, opt := range options { + if i == len(options)-1 { + responses += " or " + } else if i > 0 { + responses += ", " + } + responses += opt.Label + } + + printHelp := func() { + fmt.Println("\nExpected responses:") + for _, opt := range options { + fmt.Printf(" %v\t%v\n", opt.Label, opt.Description) + } + fmt.Println() + } + + for { + fmt.Printf("%s\n%s > ", msg, responses) + + // Don't prompt when --no-input is on. + if t.NoInput { + fmt.Println(options[defaultOpt].AllowedResponses[0]) + return defaultOpt + } + + var response string + _, err := fmt.Scan(&response) + if err != nil { + return defaultOpt + } + response = strings.ToLower(response) + + for i, opt := range options { + for _, allowedResp := range opt.AllowedResponses { + if response == strings.ToLower(allowedResp) { + return i + } + } + } + + printHelp() + } +} + +// Confirm is a shortcut to prompt a yes/no question to the user. +func (t *TTY) Confirm(msg string, yesDescription string, noDescription string) bool { + return t.Prompt(msg, 1, []PromptOpt{ + { + Label: t.MustStyle("y", style.RuleEmphasis) + "es", + Description: yesDescription, + AllowedResponses: []string{"yes", "y", "ok"}, + }, + { + Label: t.MustStyle("n", style.RuleEmphasis) + "o", + Description: noDescription, + AllowedResponses: []string{"no", "n"}, + }, + }) == 0 +} diff --git a/adapter/tty/styler.go b/adapter/tty/styler.go index 286ff2c..8b56112 100644 --- a/adapter/tty/styler.go +++ b/adapter/tty/styler.go @@ -7,18 +7,12 @@ import ( "github.com/mickael-menu/zk/core/style" ) -// Styler is a text styler using ANSI escape codes to be used with a TTY. -type Styler struct{} - -func NewStyler() *Styler { - return &Styler{} -} - -func (s *Styler) Style(text string, rules ...style.Rule) (string, error) { +// Style implements style.Styler using ANSI escape codes to be used with a TTY. +func (t *TTY) Style(text string, rules ...style.Rule) (string, error) { if text == "" { return text, nil } - attrs, err := s.attributes(expandThemeAliases(rules)) + attrs, err := attributes(expandThemeAliases(rules)) if err != nil { return "", err } @@ -28,8 +22,8 @@ func (s *Styler) Style(text string, rules ...style.Rule) (string, error) { return color.New(attrs...).Sprint(text), nil } -func (s *Styler) MustStyle(text string, rules ...style.Rule) string { - text, err := s.Style(text, rules...) +func (t *TTY) MustStyle(text string, rules ...style.Rule) string { + text, err := t.Style(text, rules...) if err != nil { panic(err.Error()) } @@ -38,9 +32,10 @@ func (s *Styler) MustStyle(text string, rules ...style.Rule) string { // FIXME: User config var themeAliases = map[style.Rule][]style.Rule{ - "title": {"bold", "yellow"}, - "path": {"underline", "cyan"}, - "term": {"red"}, + "title": {"bold", "yellow"}, + "path": {"underline", "cyan"}, + "term": {"red"}, + "emphasis": {"bold", "cyan"}, } func expandThemeAliases(rules []style.Rule) []style.Rule { @@ -108,7 +103,7 @@ var attrsMapping = map[style.Rule]color.Attribute{ style.RuleBrightWhiteBg: color.BgHiWhite, } -func (s *Styler) attributes(rules []style.Rule) ([]color.Attribute, error) { +func attributes(rules []style.Rule) ([]color.Attribute, error) { attrs := make([]color.Attribute, 0) for _, rule := range rules { diff --git a/adapter/tty/styler_test.go b/adapter/tty/styler_test.go index 95538d5..ebe4ab5 100644 --- a/adapter/tty/styler_test.go +++ b/adapter/tty/styler_test.go @@ -8,42 +8,42 @@ import ( "github.com/mickael-menu/zk/util/test/assert" ) -func createStyler() *Styler { +func createTTY() *TTY { color.NoColor = false // Otherwise the color codes are not injected during tests - return &Styler{} + return New() } func TestStyleNoRule(t *testing.T) { - res, err := createStyler().Style("Hello") + res, err := createTTY().Style("Hello") assert.Nil(t, err) assert.Equal(t, res, "Hello") } func TestStyleOneRule(t *testing.T) { - res, err := createStyler().Style("Hello", style.Rule("red")) + res, err := createTTY().Style("Hello", style.Rule("red")) assert.Nil(t, err) assert.Equal(t, res, "\033[31mHello\033[0m") } func TestStyleMultipleRule(t *testing.T) { - res, err := createStyler().Style("Hello", style.Rule("red"), style.Rule("bold")) + res, err := createTTY().Style("Hello", style.Rule("red"), style.Rule("bold")) assert.Nil(t, err) assert.Equal(t, res, "\033[31;1mHello\033[0m") } func TestStyleUnknownRule(t *testing.T) { - _, err := createStyler().Style("Hello", style.Rule("unknown")) + _, err := createTTY().Style("Hello", style.Rule("unknown")) assert.Err(t, err, "unknown styling rule: unknown") } func TestStyleEmptyString(t *testing.T) { - res, err := createStyler().Style("", style.Rule("bold")) + res, err := createTTY().Style("", style.Rule("bold")) assert.Nil(t, err) assert.Equal(t, res, "") } func TestStyleAllRules(t *testing.T) { - styler := createStyler() + styler := createTTY() test := func(rule string, expected string) { res, err := styler.Style("Hello", style.Rule(rule)) assert.Nil(t, err) @@ -53,6 +53,7 @@ func TestStyleAllRules(t *testing.T) { test("title", "1;33m") test("path", "4;36m") test("term", "31m") + test("emphasis", "1;36m") test("bold", "1m") test("faint", "2m") diff --git a/adapter/tty/tty.go b/adapter/tty/tty.go new file mode 100644 index 0000000..2af549e --- /dev/null +++ b/adapter/tty/tty.go @@ -0,0 +1,9 @@ +package tty + +type TTY struct { + NoInput bool +} + +func New() *TTY { + return &TTY{} +} diff --git a/cmd/container.go b/cmd/container.go index c108a27..df948e6 100644 --- a/cmd/container.go +++ b/cmd/container.go @@ -18,6 +18,7 @@ import ( type Container struct { Date date.Provider Logger util.Logger + TTY *tty.TTY templateLoader *handlebars.Loader } @@ -29,28 +30,25 @@ func NewContainer() *Container { // zk is short-lived, so we freeze the current date to use the same // date for any rendering during the execution. Date: &date, + TTY: tty.New(), } } func (c *Container) TemplateLoader(lang string) *handlebars.Loader { if c.templateLoader == nil { - handlebars.Init(lang, c.Logger, tty.NewStyler()) + handlebars.Init(lang, c.Logger, c.TTY) c.templateLoader = handlebars.NewLoader() } return c.templateLoader } -func (c *Container) Styler() *tty.Styler { - return tty.NewStyler() -} - func (c *Container) Parser() *markdown.Parser { return markdown.NewParser() } func (c *Container) NoteFinder(tx sqlite.Transaction) note.Finder { notes := sqlite.NewNoteDAO(tx, c.Logger) - return fzf.NewNoteFinder(notes, c.Styler()) + return fzf.NewNoteFinder(notes, c.TTY) } // Database returns the DB instance for the given slip box, after executing any diff --git a/cmd/list.go b/cmd/list.go index 27d8277..158e05e 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -43,7 +43,7 @@ func (cmd *List) Run(container *Container) error { } templates := container.TemplateLoader(zk.Config.Lang) - styler := container.Styler() + styler := container.TTY format := opt.NewNotEmptyString(cmd.Format) formatter, err := note.NewFormatter(zk.Path, wd, format, templates, styler) if err != nil { diff --git a/core/style/style.go b/core/style/style.go index c5571a3..4ac2f4d 100644 --- a/core/style/style.go +++ b/core/style/style.go @@ -13,9 +13,14 @@ type Rule string // Predefined styling rules. var ( + // Title of a note. RuleTitle = Rule("title") - RulePath = Rule("path") - RuleTerm = Rule("term") + // Path to slip box file. + RulePath = Rule("path") + // Searched for term in a note. + RuleTerm = Rule("term") + // Element to emphasize, for example the short version of a prompt response: [y]es. + RuleEmphasis = Rule("emphasis") RuleBold = Rule("bold") RuleItalic = Rule("italic") diff --git a/main.go b/main.go index 3ea2f44..ca5b3c9 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ var cli struct { Init cmd.Init `cmd help:"Create a slip box in the given directory"` List cmd.List `cmd help:"List notes matching given criteria"` New cmd.New `cmd help:"Create a new note in the given slip box directory"` + NoInput NoInput `help:"Never prompt or ask for confirmation"` Version kong.VersionFlag `help:"Print zk version"` } @@ -21,6 +22,7 @@ func main() { container := cmd.NewContainer() ctx := kong.Parse(&cli, + kong.Bind(container), kong.Name("zk"), kong.Vars{ "version": Version, @@ -29,3 +31,11 @@ func main() { err := ctx.Run(container) ctx.FatalIfErrorf(err) } + +// NoInput is a flag preventing any user prompt when enabled. +type NoInput bool + +func (f NoInput) BeforeApply(container *cmd.Container) error { + container.TTY.NoInput = true + return nil +}