Add user prompt/confirmation utilities

pull/6/head
Mickaël Menu 3 years ago
parent 5cffc71da6
commit 72d9d6f8d4
No known key found for this signature in database
GPG Key ID: 53D73664CD359895

@ -46,8 +46,8 @@ func (f *NoteFinder) Find(opts note.FinderOpts) ([]note.Match, error) {
for _, match := range matches { for _, match := range matches {
fzf.Add([]string{ fzf.Add([]string{
match.Path, match.Path,
f.styler.MustStyle(match.Title, style.Rule("yellow")), f.styler.MustStyle(match.Title, style.RuleYellow),
f.styler.MustStyle(stringsutil.JoinLines(match.Body), style.Rule("faint")), f.styler.MustStyle(stringsutil.JoinLines(match.Body), style.RuleBlack),
}) })
} }

@ -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
}

@ -7,18 +7,12 @@ import (
"github.com/mickael-menu/zk/core/style" "github.com/mickael-menu/zk/core/style"
) )
// Styler is a text styler using ANSI escape codes to be used with a TTY. // Style implements style.Styler using ANSI escape codes to be used with a TTY.
type Styler struct{} func (t *TTY) Style(text string, rules ...style.Rule) (string, error) {
func NewStyler() *Styler {
return &Styler{}
}
func (s *Styler) Style(text string, rules ...style.Rule) (string, error) {
if text == "" { if text == "" {
return text, nil return text, nil
} }
attrs, err := s.attributes(expandThemeAliases(rules)) attrs, err := attributes(expandThemeAliases(rules))
if err != nil { if err != nil {
return "", err return "", err
} }
@ -28,8 +22,8 @@ func (s *Styler) Style(text string, rules ...style.Rule) (string, error) {
return color.New(attrs...).Sprint(text), nil return color.New(attrs...).Sprint(text), nil
} }
func (s *Styler) MustStyle(text string, rules ...style.Rule) string { func (t *TTY) MustStyle(text string, rules ...style.Rule) string {
text, err := s.Style(text, rules...) text, err := t.Style(text, rules...)
if err != nil { if err != nil {
panic(err.Error()) panic(err.Error())
} }
@ -38,9 +32,10 @@ func (s *Styler) MustStyle(text string, rules ...style.Rule) string {
// FIXME: User config // FIXME: User config
var themeAliases = map[style.Rule][]style.Rule{ var themeAliases = map[style.Rule][]style.Rule{
"title": {"bold", "yellow"}, "title": {"bold", "yellow"},
"path": {"underline", "cyan"}, "path": {"underline", "cyan"},
"term": {"red"}, "term": {"red"},
"emphasis": {"bold", "cyan"},
} }
func expandThemeAliases(rules []style.Rule) []style.Rule { func expandThemeAliases(rules []style.Rule) []style.Rule {
@ -108,7 +103,7 @@ var attrsMapping = map[style.Rule]color.Attribute{
style.RuleBrightWhiteBg: color.BgHiWhite, 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) attrs := make([]color.Attribute, 0)
for _, rule := range rules { for _, rule := range rules {

@ -8,42 +8,42 @@ import (
"github.com/mickael-menu/zk/util/test/assert" "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 color.NoColor = false // Otherwise the color codes are not injected during tests
return &Styler{} return New()
} }
func TestStyleNoRule(t *testing.T) { func TestStyleNoRule(t *testing.T) {
res, err := createStyler().Style("Hello") res, err := createTTY().Style("Hello")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, res, "Hello") assert.Equal(t, res, "Hello")
} }
func TestStyleOneRule(t *testing.T) { 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.Nil(t, err)
assert.Equal(t, res, "\033[31mHello\033[0m") assert.Equal(t, res, "\033[31mHello\033[0m")
} }
func TestStyleMultipleRule(t *testing.T) { 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.Nil(t, err)
assert.Equal(t, res, "\033[31;1mHello\033[0m") assert.Equal(t, res, "\033[31;1mHello\033[0m")
} }
func TestStyleUnknownRule(t *testing.T) { 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") assert.Err(t, err, "unknown styling rule: unknown")
} }
func TestStyleEmptyString(t *testing.T) { func TestStyleEmptyString(t *testing.T) {
res, err := createStyler().Style("", style.Rule("bold")) res, err := createTTY().Style("", style.Rule("bold"))
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, res, "") assert.Equal(t, res, "")
} }
func TestStyleAllRules(t *testing.T) { func TestStyleAllRules(t *testing.T) {
styler := createStyler() styler := createTTY()
test := func(rule string, expected string) { test := func(rule string, expected string) {
res, err := styler.Style("Hello", style.Rule(rule)) res, err := styler.Style("Hello", style.Rule(rule))
assert.Nil(t, err) assert.Nil(t, err)
@ -53,6 +53,7 @@ func TestStyleAllRules(t *testing.T) {
test("title", "1;33m") test("title", "1;33m")
test("path", "4;36m") test("path", "4;36m")
test("term", "31m") test("term", "31m")
test("emphasis", "1;36m")
test("bold", "1m") test("bold", "1m")
test("faint", "2m") test("faint", "2m")

@ -0,0 +1,9 @@
package tty
type TTY struct {
NoInput bool
}
func New() *TTY {
return &TTY{}
}

@ -18,6 +18,7 @@ import (
type Container struct { type Container struct {
Date date.Provider Date date.Provider
Logger util.Logger Logger util.Logger
TTY *tty.TTY
templateLoader *handlebars.Loader templateLoader *handlebars.Loader
} }
@ -29,28 +30,25 @@ func NewContainer() *Container {
// zk is short-lived, so we freeze the current date to use the same // zk is short-lived, so we freeze the current date to use the same
// date for any rendering during the execution. // date for any rendering during the execution.
Date: &date, Date: &date,
TTY: tty.New(),
} }
} }
func (c *Container) TemplateLoader(lang string) *handlebars.Loader { func (c *Container) TemplateLoader(lang string) *handlebars.Loader {
if c.templateLoader == nil { if c.templateLoader == nil {
handlebars.Init(lang, c.Logger, tty.NewStyler()) handlebars.Init(lang, c.Logger, c.TTY)
c.templateLoader = handlebars.NewLoader() c.templateLoader = handlebars.NewLoader()
} }
return c.templateLoader return c.templateLoader
} }
func (c *Container) Styler() *tty.Styler {
return tty.NewStyler()
}
func (c *Container) Parser() *markdown.Parser { func (c *Container) Parser() *markdown.Parser {
return markdown.NewParser() return markdown.NewParser()
} }
func (c *Container) NoteFinder(tx sqlite.Transaction) note.Finder { func (c *Container) NoteFinder(tx sqlite.Transaction) note.Finder {
notes := sqlite.NewNoteDAO(tx, c.Logger) 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 // Database returns the DB instance for the given slip box, after executing any

@ -43,7 +43,7 @@ func (cmd *List) Run(container *Container) error {
} }
templates := container.TemplateLoader(zk.Config.Lang) templates := container.TemplateLoader(zk.Config.Lang)
styler := container.Styler() styler := container.TTY
format := opt.NewNotEmptyString(cmd.Format) format := opt.NewNotEmptyString(cmd.Format)
formatter, err := note.NewFormatter(zk.Path, wd, format, templates, styler) formatter, err := note.NewFormatter(zk.Path, wd, format, templates, styler)
if err != nil { if err != nil {

@ -13,9 +13,14 @@ type Rule string
// Predefined styling rules. // Predefined styling rules.
var ( var (
// Title of a note.
RuleTitle = Rule("title") RuleTitle = Rule("title")
RulePath = Rule("path") // Path to slip box file.
RuleTerm = Rule("term") 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") RuleBold = Rule("bold")
RuleItalic = Rule("italic") RuleItalic = Rule("italic")

@ -13,6 +13,7 @@ var cli struct {
Init cmd.Init `cmd help:"Create a slip box in the given directory"` Init cmd.Init `cmd help:"Create a slip box in the given directory"`
List cmd.List `cmd help:"List notes matching given criteria"` 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"` 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"` Version kong.VersionFlag `help:"Print zk version"`
} }
@ -21,6 +22,7 @@ func main() {
container := cmd.NewContainer() container := cmd.NewContainer()
ctx := kong.Parse(&cli, ctx := kong.Parse(&cli,
kong.Bind(container),
kong.Name("zk"), kong.Name("zk"),
kong.Vars{ kong.Vars{
"version": Version, "version": Version,
@ -29,3 +31,11 @@ func main() {
err := ctx.Run(container) err := ctx.Run(container)
ctx.FatalIfErrorf(err) 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
}

Loading…
Cancel
Save