diff --git a/cmd/zenity/build.sh b/cmd/zenity/build.sh new file mode 100755 index 0000000..db013f4 --- /dev/null +++ b/cmd/zenity/build.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +GOOS=windows GOARCH=386 go build -ldflags="-s -w" && +zip -9 zenity_win32.zip zenity.exe + +GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" && +zip -9 zenity_win64.zip zenity.exe + +GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" && +zip -9 zenity_macos.zip zenity + +go build diff --git a/cmd/zenity/main.go b/cmd/zenity/main.go index 2dc04d3..71640ad 100644 --- a/cmd/zenity/main.go +++ b/cmd/zenity/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "flag" "image/color" "os" @@ -8,6 +9,7 @@ import ( "path/filepath" "runtime" "strings" + "time" "github.com/ncruces/zenity" "github.com/ncruces/zenity/internal/zenutil" @@ -46,7 +48,6 @@ var ( confirmCreate bool showHidden bool filename string - separator string fileFilters FileFilters // Color selection options @@ -58,12 +59,25 @@ var ( wslpath bool ) +func init() { + prevUsage := flag.Usage + flag.Usage = func() { + prevUsage() + os.Exit(-1) + } +} + func main() { setupFlags() flag.Parse() validateFlags() opts := loadFlags() zenutil.Command = true + if zenutil.Timeout > 0 { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(zenutil.Timeout)*time.Second) + opts = append(opts, zenity.Context(ctx)) + _ = cancel + } switch { case notification: @@ -93,12 +107,10 @@ func main() { } flag.Usage() - os.Exit(-1) } func setupFlags() { // Application Options - flag.BoolVar(¬ification, "notification", false, "Display notification") flag.BoolVar(&errorDlg, "error", false, "Display error dialog") flag.BoolVar(&infoDlg, "info", false, "Display info dialog") @@ -108,12 +120,10 @@ func setupFlags() { flag.BoolVar(&colorSelectionDlg, "color-selection", false, "Display color selection dialog") // General options - flag.StringVar(&title, "title", "", "Set the dialog title") flag.StringVar(&icon, "window-icon", "", "Set the window icon (error, info, question, warning)") // Message options - flag.StringVar(&text, "text", "", "Set the dialog text") flag.StringVar(&icon, "icon-name", "", "Set the dialog icon (error, info, question, warning)") flag.StringVar(&okLabel, "ok-label", "", "Set the label of the OK button") @@ -124,7 +134,6 @@ func setupFlags() { flag.BoolVar(&defaultCancel, "default-cancel", false, "Give Cancel button focus by default") // File selection options - flag.BoolVar(&save, "save", false, "Activate save mode") flag.BoolVar(&multiple, "multiple", false, "Allow multiple files to be selected") flag.BoolVar(&directory, "directory", false, "Activate directory-only selection") @@ -132,7 +141,6 @@ func setupFlags() { flag.BoolVar(&confirmCreate, "confirm-create", false, "Confirm file selection if filename does not yet exist (Windows only)") flag.BoolVar(&showHidden, "show-hidden", false, "Show hidden files (Windows and macOS only)") flag.StringVar(&filename, "filename", "", "Set the filename") - flag.StringVar(&separator, "separator", "|", "Set output separator character") flag.Var(&fileFilters, "file-filter", "Set a filename filter (NAME | PATTERN1 PATTERN2 ...)") // Color selection options @@ -144,6 +152,10 @@ func setupFlags() { flag.BoolVar(&cygpath, "cygpath", false, "Use cygpath for path translation (Windows only)") flag.BoolVar(&wslpath, "wslpath", false, "Use wslpath for path translation (Windows only)") } + + // Internal options + flag.IntVar(&zenutil.Timeout, "timeout", 0, "Set dialog timeout in seconds") + flag.StringVar(&zenutil.Separator, "separator", "|", "Set output separator character") } func validateFlags() { @@ -171,16 +183,15 @@ func validateFlags() { } if n != 1 { flag.Usage() - os.Exit(-1) } } func loadFlags() []zenity.Option { - var options []zenity.Option + var opts []zenity.Option // General options - options = append(options, zenity.Title(title)) + opts = append(opts, zenity.Title(title)) // Message options @@ -196,51 +207,49 @@ func loadFlags() []zenity.Option { ico = zenity.WarningIcon } - options = append(options, zenity.Icon(ico)) - options = append(options, zenity.OKLabel(okLabel)) - options = append(options, zenity.CancelLabel(cancelLabel)) - options = append(options, zenity.ExtraButton(extraButton)) + opts = append(opts, zenity.Icon(ico)) + opts = append(opts, zenity.OKLabel(okLabel)) + opts = append(opts, zenity.CancelLabel(cancelLabel)) + opts = append(opts, zenity.ExtraButton(extraButton)) if noWrap { - options = append(options, zenity.NoWrap()) + opts = append(opts, zenity.NoWrap()) } if ellipsize { - options = append(options, zenity.Ellipsize()) + opts = append(opts, zenity.Ellipsize()) } if defaultCancel { - options = append(options, zenity.DefaultCancel()) + opts = append(opts, zenity.DefaultCancel()) } // File selection options - options = append(options, fileFilters) + opts = append(opts, fileFilters) if filename != "" { - options = append(options, zenity.Filename(ingestPath(filename))) + opts = append(opts, zenity.Filename(ingestPath(filename))) } if directory { - options = append(options, zenity.Directory()) + opts = append(opts, zenity.Directory()) } if confirmOverwrite { - options = append(options, zenity.ConfirmOverwrite()) + opts = append(opts, zenity.ConfirmOverwrite()) } if confirmCreate { - options = append(options, zenity.ConfirmCreate()) + opts = append(opts, zenity.ConfirmCreate()) } if showHidden { - options = append(options, zenity.ShowHidden()) + opts = append(opts, zenity.ShowHidden()) } - zenutil.Separator = separator - // Color selection options if defaultColor != "" { - options = append(options, zenity.Color(zenutil.ParseColor(defaultColor))) + opts = append(opts, zenity.Color(zenutil.ParseColor(defaultColor))) } if showPalette { - options = append(options, zenity.ShowPalette()) + opts = append(opts, zenity.ShowPalette()) } - return options + return opts } func errResult(err error) { @@ -289,7 +298,7 @@ func listResult(l []string, err error) { os.Stderr.WriteString(zenutil.LineBreak) os.Exit(-1) } - os.Stdout.WriteString(strings.Join(l, separator)) + os.Stdout.WriteString(strings.Join(l, zenutil.Separator)) os.Stdout.WriteString(zenutil.LineBreak) if l == nil { os.Exit(1) diff --git a/color_darwin.go b/color_darwin.go index 9ae3273..7fcd6cd 100644 --- a/color_darwin.go +++ b/color_darwin.go @@ -16,7 +16,7 @@ func selectColor(options []Option) (color.Color, error) { data.Color = []uint16{n.R, n.G, n.B} } - out, err := zenutil.Run("color", data) + out, err := zenutil.Run(opts.ctx, "color", data) if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { return nil, nil } diff --git a/color_unix.go b/color_unix.go index 8ca9ed9..ef155ac 100644 --- a/color_unix.go +++ b/color_unix.go @@ -24,7 +24,7 @@ func selectColor(options []Option) (color.Color, error) { args = append(args, "--show-palette") } - out, err := zenutil.Run(args) + out, err := zenutil.Run(opts.ctx, args) if err, ok := err.(*exec.ExitError); ok && err.ExitCode() != 255 { return nil, nil } diff --git a/file_darwin.go b/file_darwin.go index 8ae43d9..b9651ae 100644 --- a/file_darwin.go +++ b/file_darwin.go @@ -22,7 +22,7 @@ func selectFile(options []Option) (string, error) { } data.Location, _ = splitDirAndName(opts.filename) - out, err := zenutil.Run("file", data) + out, err := zenutil.Run(opts.ctx, "file", data) if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { return "", nil } @@ -41,8 +41,8 @@ func selectFileMutiple(options []Option) ([]string, error) { data := zenutil.File{ Prompt: opts.title, Invisibles: opts.showHidden, - Multiple: true, Separator: zenutil.Separator, + Multiple: true, } if opts.directory { data.Operation = "chooseFolder" @@ -52,7 +52,7 @@ func selectFileMutiple(options []Option) ([]string, error) { } data.Location, _ = splitDirAndName(opts.filename) - out, err := zenutil.Run("file", data) + out, err := zenutil.Run(opts.ctx, "file", data) if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { return nil, nil } @@ -82,7 +82,7 @@ func selectFileSave(options []Option) (string, error) { } data.Location, data.Name = splitDirAndName(opts.filename) - out, err := zenutil.Run("file", data) + out, err := zenutil.Run(opts.ctx, "file", data) if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { return "", nil } diff --git a/file_unix.go b/file_unix.go index a52c607..55e375f 100644 --- a/file_unix.go +++ b/file_unix.go @@ -24,7 +24,7 @@ func selectFile(options []Option) (string, error) { } args = append(args, initFilters(opts.fileFilters)...) - out, err := zenutil.Run(args) + out, err := zenutil.Run(opts.ctx, args) if err, ok := err.(*exec.ExitError); ok && err.ExitCode() != 255 { return "", nil } @@ -52,7 +52,7 @@ func selectFileMutiple(options []Option) ([]string, error) { } args = append(args, initFilters(opts.fileFilters)...) - out, err := zenutil.Run(args) + out, err := zenutil.Run(opts.ctx, args) if err, ok := err.(*exec.ExitError); ok && err.ExitCode() != 255 { return nil, nil } @@ -83,7 +83,7 @@ func selectFileSave(options []Option) (string, error) { } args = append(args, initFilters(opts.fileFilters)...) - out, err := zenutil.Run(args) + out, err := zenutil.Run(opts.ctx, args) if err, ok := err.(*exec.ExitError); ok && err.ExitCode() != 255 { return "", nil } diff --git a/internal/zenutil/env_darwin.go b/internal/zenutil/env_darwin.go index 097179b..0251d26 100644 --- a/internal/zenutil/env_darwin.go +++ b/internal/zenutil/env_darwin.go @@ -3,4 +3,5 @@ package zenutil const LineBreak = "\n" var Command bool +var Timeout int var Separator = "\x00" diff --git a/internal/zenutil/env_unix.go b/internal/zenutil/env_unix.go index 87f852e..e2bdade 100644 --- a/internal/zenutil/env_unix.go +++ b/internal/zenutil/env_unix.go @@ -6,4 +6,5 @@ package zenutil const LineBreak = "\n" var Command bool +var Timeout int var Separator = "\x1e" diff --git a/internal/zenutil/env_windows.go b/internal/zenutil/env_windows.go index 3e2fa2c..6bd4c22 100644 --- a/internal/zenutil/env_windows.go +++ b/internal/zenutil/env_windows.go @@ -3,4 +3,5 @@ package zenutil const LineBreak = "\r\n" var Command bool +var Timeout int var Separator string diff --git a/internal/zenutil/osa_generated.go b/internal/zenutil/osa_generated.go index 95e6365..45298ad 100644 --- a/internal/zenutil/osa_generated.go +++ b/internal/zenutil/osa_generated.go @@ -68,14 +68,25 @@ opts.withIcon = {{json .Icon}} {{if .Buttons -}} opts.buttons = {{json .Buttons}} {{end -}} -{{if .Default -}} -opts.defaultButton = {{json .Default}} -{{end -}} {{if .Cancel -}} opts.cancelButton = {{json .Cancel}} {{end -}} -var res = app[{{json .Operation}}]({{json .Text}}, opts).buttonReturned -res === {{json .Extra}} ? res : void 0 +{{if .Default -}} +opts.defaultButton = {{json .Default}} +{{end -}} +{{if .Timeout -}} +opts.givingUpAfter = {{json .Timeout}} +{{end -}} +var res = app[{{json .Operation}}]({{json .Text}}, opts) +if (res.gaveUp) { +ObjC.import("stdlib") +$.exit(5) +} +if (res.buttonReturned === {{json .Extra}}) { +res +} else { +void 0 +} {{- end}} {{define "notify" -}}var app = Application.currentApplication() app.includeStandardAdditions = true diff --git a/internal/zenutil/osascripts/msg.js b/internal/zenutil/osascripts/msg.js index a87cd40..a036289 100644 --- a/internal/zenutil/osascripts/msg.js +++ b/internal/zenutil/osascripts/msg.js @@ -19,12 +19,23 @@ var opts = {} {{if .Buttons -}} opts.buttons = {{json .Buttons}} {{end -}} -{{if .Default -}} - opts.defaultButton = {{json .Default}} -{{end -}} {{if .Cancel -}} opts.cancelButton = {{json .Cancel}} {{end -}} +{{if .Default -}} + opts.defaultButton = {{json .Default}} +{{end -}} +{{if .Timeout -}} + opts.givingUpAfter = {{json .Timeout}} +{{end -}} -var res = app[{{json .Operation}}]({{json .Text}}, opts).buttonReturned -res === {{json .Extra}} ? res : void 0 \ No newline at end of file +var res = app[{{json .Operation}}]({{json .Text}}, opts) +if (res.gaveUp) { + ObjC.import("stdlib") + $.exit(5) +} +if (res.buttonReturned === {{json .Extra}}) { + res +} else { + void 0 +} \ No newline at end of file diff --git a/internal/zenutil/run_darwin.go b/internal/zenutil/run_darwin.go index 88564e5..2dbd97a 100644 --- a/internal/zenutil/run_darwin.go +++ b/internal/zenutil/run_darwin.go @@ -1,13 +1,14 @@ package zenutil import ( + "context" "os" "os/exec" "strings" "syscall" ) -func Run(script string, data interface{}) ([]byte, error) { +func Run(ctx context.Context, script string, data interface{}) ([]byte, error) { var buf strings.Builder err := scripts.ExecuteTemplate(&buf, script, data) @@ -29,6 +30,11 @@ func Run(script string, data interface{}) ([]byte, error) { } } + if ctx != nil { + cmd := exec.CommandContext(ctx, "osascript", "-l", lang) + cmd.Stdin = strings.NewReader(script) + return cmd.Output() + } cmd := exec.Command("osascript", "-l", lang) cmd.Stdin = strings.NewReader(script) return cmd.Output() @@ -60,6 +66,7 @@ type Msg struct { Buttons []string Cancel int Default int + Timeout int } type Notify struct { diff --git a/internal/zenutil/run_unix.go b/internal/zenutil/run_unix.go index 22b6f39..5420868 100644 --- a/internal/zenutil/run_unix.go +++ b/internal/zenutil/run_unix.go @@ -3,8 +3,10 @@ package zenutil import ( + "context" "os" "os/exec" + "strconv" "syscall" ) @@ -20,9 +22,16 @@ func init() { tool = "zenity" } -func Run(args []string) ([]byte, error) { +func Run(ctx context.Context, args []string) ([]byte, error) { if Command && path != "" { + if Timeout > 0 { + args = append(args, "--timeout", strconv.Itoa(Timeout)) + } syscall.Exec(path, append([]string{tool}, args...), os.Environ()) } + + if ctx != nil { + return exec.CommandContext(ctx, tool, args...).Output() + } return exec.Command(tool, args...).Output() } diff --git a/msg.go b/msg.go index 2b7b899..383c3e4 100644 --- a/msg.go +++ b/msg.go @@ -2,7 +2,7 @@ package zenity // ErrExtraButton is returned by dialog functions when the extra button is // pressed. -const ErrExtraButton = constError("Extra button pressed.") +const ErrExtraButton = constError("Extra button pressed") // Question displays the question dialog. // diff --git a/msg_darwin.go b/msg_darwin.go index b377531..16bc0ac 100644 --- a/msg_darwin.go +++ b/msg_darwin.go @@ -8,7 +8,10 @@ import ( func message(kind messageKind, text string, options []Option) (bool, error) { opts := applyOptions(options) - data := zenutil.Msg{Text: text} + data := zenutil.Msg{ + Text: text, + Timeout: zenutil.Timeout, + } dialog := kind == questionKind || opts.icon != 0 if dialog { @@ -83,7 +86,7 @@ func message(kind messageKind, text string, options []Option) (bool, error) { } } - out, err := zenutil.Run("msg", data) + out, err := zenutil.Run(opts.ctx, "msg", data) if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { return false, nil } diff --git a/msg_unix.go b/msg_unix.go index f55023b..c29e1ca 100644 --- a/msg_unix.go +++ b/msg_unix.go @@ -57,7 +57,7 @@ func message(kind messageKind, text string, options []Option) (bool, error) { args = append(args, "--window-icon=question", "--icon-name=dialog-question") } - out, err := zenutil.Run(args) + out, err := zenutil.Run(opts.ctx, args) if err, ok := err.(*exec.ExitError); ok && err.ExitCode() != 255 { if len(out) > 0 && string(out[:len(out)-1]) == opts.extraButton { return false, ErrExtraButton diff --git a/notify_darwin.go b/notify_darwin.go index adf52d3..8aa9c9b 100644 --- a/notify_darwin.go +++ b/notify_darwin.go @@ -16,7 +16,7 @@ func notify(text string, options []Option) error { data.Subtitle = text[:i] data.Text = text[i+1:] } - _, err := zenutil.Run("notify", data) + _, err := zenutil.Run(opts.ctx, "notify", data) if err != nil { return err } diff --git a/notify_unix.go b/notify_unix.go index bf2d3ca..a8ac60b 100644 --- a/notify_unix.go +++ b/notify_unix.go @@ -28,7 +28,7 @@ func notify(text string, options []Option) error { args = append(args, "--window-icon=question") } - _, err := zenutil.Run(args) + _, err := zenutil.Run(opts.ctx, args) if err != nil { return err } diff --git a/zenity.go b/zenity.go index 31a400a..684a023 100644 --- a/zenity.go +++ b/zenity.go @@ -10,7 +10,10 @@ // initialization requirements. package zenity -import "image/color" +import ( + "context" + "image/color" +) type constError string @@ -40,6 +43,9 @@ type options struct { noWrap bool ellipsize bool defaultCancel bool + + // Context for timeout + ctx context.Context } // An Option is an argument passed to dialog functions to customize their @@ -79,3 +85,7 @@ const ( func Icon(icon DialogIcon) Option { return funcOption(func(o *options) { o.icon = icon }) } + +func Context(ctx context.Context) Option { + return funcOption(func(o *options) { o.ctx = ctx }) +}