diff --git a/cmd/zenity/main.go b/cmd/zenity/main.go index 5ae76b3..8940349 100644 --- a/cmd/zenity/main.go +++ b/cmd/zenity/main.go @@ -520,9 +520,13 @@ func loadFlags() []zenity.Option { if ellipsize { opts = append(opts, zenity.Ellipsize()) } - if !noMarkup { - switch { - case errorDlg, infoDlg, warningDlg, questionDlg: + switch { + case entryDlg: + text = zencmd.StripMnemonic(text) + case calendarDlg, progressDlg: + text = zencmd.StripMarkup(text) + case errorDlg, infoDlg, warningDlg, questionDlg: + if !noMarkup { text = zencmd.StripMarkup(text) } } diff --git a/date_unix.go b/date_unix.go index d6f490d..90795c4 100644 --- a/date_unix.go +++ b/date_unix.go @@ -10,7 +10,7 @@ import ( ) func calendar(text string, opts options) (time.Time, error) { - args := []string{"--calendar", "--text", text, "--date-format", zenutil.DateFormat} + args := []string{"--calendar", "--text", quoteMarkup(text), "--date-format", zenutil.DateFormat} args = appendGeneral(args, opts) args = appendButtons(args, opts) args = appendWidthHeight(args, opts) diff --git a/entry_unix.go b/entry_unix.go index 9444854..02f5ae8 100644 --- a/entry_unix.go +++ b/entry_unix.go @@ -2,12 +2,10 @@ package zenity -import ( - "github.com/ncruces/zenity/internal/zenutil" -) +import "github.com/ncruces/zenity/internal/zenutil" func entry(text string, opts options) (string, error) { - args := []string{"--entry", "--text", text} + args := []string{"--entry", "--text", quoteMnemonics(text)} args = appendGeneral(args, opts) args = appendButtons(args, opts) args = appendWidthHeight(args, opts) diff --git a/internal/zencmd/markup.go b/internal/zencmd/markup.go index 34919fe..0d3b588 100644 --- a/internal/zencmd/markup.go +++ b/internal/zencmd/markup.go @@ -12,18 +12,18 @@ func StripMarkup(s string) string { // https://docs.gtk.org/Pango/pango_markup.html dec := xml.NewDecoder(strings.NewReader(s)) - var buf strings.Builder + var res strings.Builder for { t, err := dec.Token() if err == io.EOF { - return buf.String() + return res.String() } if err != nil { return s } if t, ok := t.(xml.CharData); ok { - buf.Write(t) + res.Write(t) } } } diff --git a/internal/zencmd/markup_test.go b/internal/zencmd/markup_test.go index f031037..69363c9 100644 --- a/internal/zencmd/markup_test.go +++ b/internal/zencmd/markup_test.go @@ -1,8 +1,6 @@ package zencmd -import ( - "testing" -) +import "testing" var markupTests = []struct { data string diff --git a/internal/zencmd/mnemonic.go b/internal/zencmd/mnemonic.go new file mode 100644 index 0000000..98eddfd --- /dev/null +++ b/internal/zencmd/mnemonic.go @@ -0,0 +1,25 @@ +package zencmd + +import "strings" + +// StripMnemonic is internal. +func StripMnemonic(s string) string { + // Strips mnemonics described in: + // https: //docs.gtk.org/gtk4/class.Label.html#mnemonics + + var res strings.Builder + + underscore := false + for _, b := range []byte(s) { + switch { + case underscore: + underscore = false + case b == '_': + underscore = true + continue + } + res.WriteByte(b) + } + + return res.String() +} diff --git a/internal/zencmd/mnemonic_test.go b/internal/zencmd/mnemonic_test.go new file mode 100644 index 0000000..29790b1 --- /dev/null +++ b/internal/zencmd/mnemonic_test.go @@ -0,0 +1,46 @@ +package zencmd + +import ( + "strings" + "testing" +) + +var mnemonicTests = []struct { + data string + want string +}{ + {"", ""}, + {"abc", "abc"}, + {"_abc", `abc`}, + {"_a_b_c_", "abc"}, + {"a__c", `a_c`}, + {"a___c", `a_c`}, + {"a____c", `a__c`}, +} + +func TestStripMnemonic(t *testing.T) { + t.Parallel() + for _, test := range mnemonicTests { + if got := StripMnemonic(test.data); got != test.want { + t.Errorf("StripMnemonic(%q) = %q; want %q", test.data, got, test.want) + } + } +} + +func FuzzStripMnemonic(f *testing.F) { + for _, test := range mnemonicTests { + f.Add(test.data) + } + + f.Fuzz(func(t *testing.T, s string) { + q := quoteMnemonic(s) + u := StripMnemonic(q) + if s != u { + t.Errorf("strip(quote(%q)) = strip(%q) = %q", s, q, u) + } + }) +} + +func quoteMnemonic(s string) string { + return strings.ReplaceAll(s, "_", "__") +} diff --git a/internal/zencmd/unescape_test.go b/internal/zencmd/unescape_test.go index 4f06e5b..09c6128 100644 --- a/internal/zencmd/unescape_test.go +++ b/internal/zencmd/unescape_test.go @@ -1,8 +1,6 @@ package zencmd -import ( - "testing" -) +import "testing" var unescapeTests = []struct { data string diff --git a/util.go b/util.go index efb5a89..a534cb5 100644 --- a/util.go +++ b/util.go @@ -2,6 +2,7 @@ package zenity import ( "bytes" + "encoding/xml" "fmt" "os/exec" "strconv" @@ -14,6 +15,19 @@ func quoteAccelerators(text string) string { return strings.ReplaceAll(text, "&", "&&") } +func quoteMnemonics(text string) string { + return strings.ReplaceAll(text, "_", "__") +} + +func quoteMarkup(text string) string { + var res strings.Builder + err := xml.EscapeText(&res, []byte(text)) + if err != nil { + return text + } + return res.String() +} + func appendGeneral(args []string, opts options) []string { if opts.title != nil { args = append(args, "--title", *opts.title) diff --git a/util_test.go b/util_test.go index 65ddb64..0d35afc 100644 --- a/util_test.go +++ b/util_test.go @@ -31,6 +31,49 @@ func Test_quoteAccelerators(t *testing.T) { } } +func Test_quoteMnemonics(t *testing.T) { + t.Parallel() + tests := []struct { + name string + text string + want string + }{ + {name: "None", text: "abc", want: "abc"}, + {name: "One", text: "_abc", want: "__abc"}, + {name: "Two", text: "_a_bc", want: "__a__bc"}, + {name: "Three", text: "ab__c", want: "ab____c"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := quoteMnemonics(tt.text); got != tt.want { + t.Errorf("quoteMnemonics(%q) = %q; want %q", tt.text, got, tt.want) + } + }) + } +} + +func Test_quoteMarkup(t *testing.T) { + t.Parallel() + tests := []struct { + name string + text string + want string + }{ + {name: "None", text: `abc`, want: "abc"}, + {name: "LT", text: `<`, want: "<"}, + {name: "Amp", text: `&`, want: "&"}, + {name: "Quot", text: `"`, want: """}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := quoteMarkup(tt.text); got != tt.want { + t.Errorf("quoteMarkup(%q) = %q; want %q", tt.text, got, tt.want) + } + }) + } +} + func Test_appendGeneral(t *testing.T) { t.Parallel() got := appendGeneral(nil, options{