diff --git a/cmd/zenity/main.go b/cmd/zenity/main.go index 3012f8d..a3bb851 100644 --- a/cmd/zenity/main.go +++ b/cmd/zenity/main.go @@ -38,20 +38,20 @@ var ( notification bool // General options - title string - width uint - height uint - okLabel string - cancelLabel string - extraButton string - text string - icon string - multiple bool + title string + width uint + height uint + okLabel string + cancelLabel string + extraButton string + text string + icon string + multiple bool + defaultCancel bool // Message options - noWrap bool - ellipsize bool - defaultCancel bool + noWrap bool + ellipsize bool // Entry options entryText string @@ -81,6 +81,9 @@ var ( autoKill bool noCancel bool + // Notify options + listen bool + // Windows specific options unixeol bool cygpath bool @@ -151,7 +154,7 @@ func main() { errResult(progress(opts...)) case notification: - errResult(zenity.Notify(text, opts...)) + errResult(notify(opts...)) default: flag.Usage() @@ -220,6 +223,9 @@ func setupFlags() { flag.BoolVar(&autoKill, "auto-kill", false, "Kill parent process if Cancel button is pressed (macOS and Unix only)") } + // Notify options + flag.BoolVar(&listen, "listen", false, "Listen for commands on stdin") + // Windows specific options if runtime.GOOS == "windows" { flag.BoolVar(&unixeol, "unixeol", false, "Use Unix line endings (Windows only)") @@ -332,6 +338,8 @@ func loadFlags() []zenity.Option { setDefault(&text, "Running...") setDefault(&okLabel, "OK") setDefault(&cancelLabel, "Cancel") + case notification: + setDefault(&icon, "dialog-information") default: setDefault(&text, "") } diff --git a/cmd/zenity/notify.go b/cmd/zenity/notify.go new file mode 100644 index 0000000..005b5ef --- /dev/null +++ b/cmd/zenity/notify.go @@ -0,0 +1,61 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/ncruces/zenity" + "github.com/ncruces/zenity/internal/zenutil" +) + +func notify(opts ...zenity.Option) error { + if !listen { + return zenity.Notify(text, opts...) + } + + zenutil.Command = false + var icon zenity.DialogIcon + for scanner := bufio.NewScanner(os.Stdin); scanner.Scan(); { + line := scanner.Text() + var cmd, msg string + if n := strings.IndexByte(line, ':'); n >= 0 { + cmd = strings.TrimSpace(line[:n]) + msg = strings.TrimSpace(zenutil.Unescape(line[n+1:])) + } else { + fmt.Fprint(os.Stderr, "Could not parse command from stdin") + } + switch cmd { + case "icon": + switch msg { + case "error", "dialog-error": + icon = zenity.ErrorIcon + case "info", "dialog-information": + icon = zenity.InfoIcon + case "question", "dialog-question": + icon = zenity.QuestionIcon + case "important", "warning", "dialog-warning": + icon = zenity.WarningIcon + case "dialog-password": + icon = zenity.PasswordIcon + default: + icon = zenity.NoIcon + } + case "message", "tooltip": + opts := []zenity.Option{zenity.Icon(icon)} + if n := strings.IndexByte(msg, '\n'); n >= 0 { + opts = append(opts, zenity.Title(msg[:n])) + msg = msg[n+1:] + } + if err := zenity.Notify(msg, opts...); err != nil { + return err + } + case "visible", "hints": + // ignored + default: + fmt.Fprintf(os.Stderr, "Unknown command %q", cmd) + } + } + return nil +} diff --git a/internal/zenutil/unescape.go b/internal/zenutil/unescape.go new file mode 100644 index 0000000..c81f9d2 --- /dev/null +++ b/internal/zenutil/unescape.go @@ -0,0 +1,89 @@ +package zenutil + +// Unescape is internal. +func Unescape(s string) string { + // Apply rules described in: + // https://developer.gnome.org/glib/stable/glib-String-Utility-Functions.html#g-strescape + + const ( + initial = iota + escape1 + escape2 + escape3 + ) + var oct byte + var res []byte + state := initial + for _, b := range []byte(s) { + switch state { + case initial: + switch b { + case '\\': + state = escape1 + default: + res = append(res, b) + state = initial + } + + case escape1: + switch b { + case '0', '1', '2', '3', '4', '5', '6', '7': + oct = b - '0' + state = escape2 + case 'b': + res = append(res, '\b') + state = initial + case 'f': + res = append(res, '\f') + state = initial + case 'n': + res = append(res, '\n') + state = initial + case 'r': + res = append(res, '\r') + state = initial + case 't': + res = append(res, '\t') + state = initial + case 'v': + res = append(res, '\v') + state = initial + default: + res = append(res, b) + state = initial + } + + case escape2: + switch b { + case '0', '1', '2', '3', '4', '5', '6', '7': + oct = oct<<3 | (b - '0') + state = escape3 + case '\\': + res = append(res, oct) + state = escape1 + default: + res = append(res, oct, b) + state = initial + } + + case escape3: + switch b { + case '0', '1', '2', '3', '4', '5', '6', '7': + oct = oct<<3 | (b - '0') + res = append(res, oct) + state = initial + case '\\': + res = append(res, oct) + state = escape1 + default: + res = append(res, oct, b) + state = initial + } + } + } + if state == escape2 || state == escape3 { + res = append(res, oct) + } + + return string(res) +} diff --git a/internal/zenutil/unescape_test.go b/internal/zenutil/unescape_test.go new file mode 100644 index 0000000..12a00f7 --- /dev/null +++ b/internal/zenutil/unescape_test.go @@ -0,0 +1,28 @@ +package zenutil + +import "testing" + +func TestUnescape(t *testing.T) { + tests := []struct { + data string + want string + }{ + {`abc`, "abc"}, + {`ab\c`, "abc"}, + {`a\bc`, "a\bc"}, + {`a\1c`, "a\001c"}, + {`a\12c`, "a\012c"}, + {`a\123c`, "a\123c"}, + {`a\1\b`, "a\001\b"}, + {`a\12\b`, "a\012\b"}, + {`a\123\b`, "a\123\b"}, + {`abc\1`, "abc\001"}, + {`abc\12`, "abc\012"}, + {`abc\123`, "abc\123"}, + } + for _, tt := range tests { + if got := Unescape(tt.data); got != tt.want { + t.Errorf("Unescape(%q) = %q, want %q", tt.data, got, tt.want) + } + } +} diff --git a/zenity.go b/zenity.go index f4f6350..90ac710 100644 --- a/zenity.go +++ b/zenity.go @@ -31,24 +31,24 @@ const ErrUnsupported = zenutil.ErrUnsupported type options struct { // General options - title *string - width uint - height uint - okLabel *string - cancelLabel *string - extraButton *string - icon DialogIcon + title *string + width uint + height uint + okLabel *string + cancelLabel *string + extraButton *string + icon DialogIcon + defaultCancel bool + + // Message options + noWrap bool + ellipsize bool // Entry options entryText string hideText bool username bool - // Message options - noWrap bool - ellipsize bool - defaultCancel bool - // List options disallowEmpty bool defaultItems []string