From 71541c249aec3f089764856b606b7f586eab5938 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Fri, 30 Apr 2021 19:19:14 +0100 Subject: [PATCH] Progress. --- README.md | 5 +- cmd/zenity/main.go | 47 +++++- cmd/zenity/progress.go | 72 ++++++++++ cmd/zenity/progress_unix.go | 12 ++ cmd/zenity/progress_windows.go | 3 + color_test.go | 10 +- entry_test.go | 10 +- entry_windows.go | 2 +- file_test.go | 7 +- internal/zenutil/env.go | 12 ++ internal/zenutil/env_darwin.go | 1 - internal/zenutil/env_unix.go | 1 - internal/zenutil/env_windows.go | 1 - internal/zenutil/osa_generated.go | 16 ++- internal/zenutil/osa_generator.go | 27 +--- .../osascripts/{progress.js => progress.gojs} | 8 +- .../{run_progress.go => progress_unix.go} | 20 +-- internal/zenutil/run_darwin.go | 54 ++++--- internal/zenutil/run_unix.go | 18 ++- list_darwin.go | 8 ++ list_test.go | 10 +- list_windows.go | 2 +- msg_test.go | 7 +- notify_test.go | 4 +- progress.go | 4 +- progress_darwin.go | 15 +- progress_test.go | 100 +++++++++++++ progress_unix.go | 2 +- progress_windows.go | 134 ++++++++++-------- pwd_test.go | 10 +- util_unix.go | 4 +- util_windows.go | 1 + zenity.go | 14 +- 33 files changed, 464 insertions(+), 177 deletions(-) create mode 100644 cmd/zenity/progress.go create mode 100644 cmd/zenity/progress_unix.go create mode 100644 cmd/zenity/progress_windows.go create mode 100644 internal/zenutil/env.go rename internal/zenutil/osascripts/{progress.js => progress.gojs} (78%) rename internal/zenutil/{run_progress.go => progress_unix.go} (86%) create mode 100644 progress_test.go diff --git a/README.md b/README.md index cb76318..145e14a 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,9 @@ Implemented dialogs: * [text entry](https://github.com/ncruces/zenity/wiki/Text-Entry-dialog) * [list](https://github.com/ncruces/zenity/wiki/List-dialog) (simple) * [password](https://github.com/ncruces/zenity/wiki/Password-dialog) -* [file selection](https://github.com/ncruces/zenity/wiki/File-Selection-dialog) -* [color selection](https://github.com/ncruces/zenity/wiki/Color-Selection-dialog) +* [file selection](https://github.com/ncruces/zenity/wiki/File-selection-dialog) +* [color selection](https://github.com/ncruces/zenity/wiki/Color-selection-dialog) +* [progress](https://github.com/ncruces/zenity/wiki/Progress-dialog) * [notification](https://github.com/ncruces/zenity/wiki/Notification) Behavior on Windows, macOS and other Unixes might differ slightly. diff --git a/cmd/zenity/main.go b/cmd/zenity/main.go index e9d4087..44b6610 100644 --- a/cmd/zenity/main.go +++ b/cmd/zenity/main.go @@ -34,6 +34,7 @@ var ( passwordDlg bool fileSelectionDlg bool colorSelectionDlg bool + progressDlg bool notification bool // General options @@ -73,6 +74,13 @@ var ( defaultColor string showPalette bool + // Progress options + percentage float64 + pulsate bool + autoClose bool + autoKill bool + noCancel bool + // Windows specific options cygpath bool wslpath bool @@ -95,7 +103,7 @@ func main() { if zenutil.Timeout > 0 { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(zenutil.Timeout)*time.Second) opts = append(opts, zenity.Context(ctx)) - _ = cancel + defer cancel() } switch { @@ -135,11 +143,15 @@ func main() { case colorSelectionDlg: colResult(zenity.SelectColor(opts...)) + case progressDlg: + errResult(progress(opts...)) + case notification: errResult(zenity.Notify(text, opts...)) - } - flag.Usage() + default: + flag.Usage() + } } func setupFlags() { @@ -153,6 +165,7 @@ func setupFlags() { flag.BoolVar(&passwordDlg, "password", false, "Display password dialog") flag.BoolVar(&fileSelectionDlg, "file-selection", false, "Display file selection dialog") flag.BoolVar(&colorSelectionDlg, "color-selection", false, "Display color selection dialog") + flag.BoolVar(&progressDlg, "progress", false, "Display progress indication dialog") flag.BoolVar(¬ification, "notification", false, "Display notification") // General options @@ -165,12 +178,12 @@ func setupFlags() { flag.StringVar(&text, "text", "", "Set the dialog `text`") flag.StringVar(&icon, "window-icon", "", "Set the window `icon` (error, info, question, warning)") flag.BoolVar(&multiple, "multiple", false, "Allow multiple items to be selected") + flag.BoolVar(&defaultCancel, "default-cancel", false, "Give Cancel button focus by default") // Message options flag.StringVar(&icon, "icon-name", "", "Set the dialog `icon` (dialog-error, dialog-information, dialog-question, dialog-warning)") flag.BoolVar(&noWrap, "no-wrap", false, "Do not enable text wrapping") flag.BoolVar(&ellipsize, "ellipsize", false, "Enable ellipsizing in the dialog text") - flag.BoolVar(&defaultCancel, "default-cancel", false, "Give Cancel button focus by default") // Entry options flag.StringVar(&entryText, "entry-text", "", "Set the entry `text`") @@ -194,6 +207,15 @@ func setupFlags() { flag.StringVar(&defaultColor, "color", "", "Set the `color`") flag.BoolVar(&showPalette, "show-palette", false, "Show the palette") + // Progress options + flag.Float64Var(&percentage, "percentage", 0, "Set initial `percentage`") + flag.BoolVar(&pulsate, "pulsate", false, "Pulsate progress bar") + flag.BoolVar(&noCancel, "no-cancel", false, "Hide Cancel button (Windows and Unix only)") + flag.BoolVar(&autoClose, "auto-close", false, "Dismiss the dialog when 100% has been reached") + if runtime.GOOS != "windows" { + flag.BoolVar(&autoKill, "auto-kill", false, "Kill parent process if Cancel button is pressed (macOS and Unix only)") + } + // Windows specific options if runtime.GOOS == "windows" { flag.BoolVar(&cygpath, "cygpath", false, "Use cygpath for path translation (Windows only)") @@ -242,6 +264,9 @@ func validateFlags() { if colorSelectionDlg { n++ } + if progressDlg { + n++ + } if notification { n++ } @@ -297,6 +322,11 @@ func loadFlags() []zenity.Option { setDefault(&icon, "dialog-password") setDefault(&okLabel, "OK") setDefault(&cancelLabel, "Cancel") + case progressDlg: + setDefault(&title, "Progress") + setDefault(&text, "Running...") + setDefault(&okLabel, "OK") + setDefault(&cancelLabel, "Cancel") default: setDefault(&text, "") } @@ -388,6 +418,15 @@ func loadFlags() []zenity.Option { opts = append(opts, zenity.ShowPalette()) } + // Progress options + + if pulsate { + opts = append(opts, zenity.Pulsate()) + } + if noCancel { + opts = append(opts, zenity.NoCancel()) + } + return opts } diff --git a/cmd/zenity/progress.go b/cmd/zenity/progress.go new file mode 100644 index 0000000..4fff0f3 --- /dev/null +++ b/cmd/zenity/progress.go @@ -0,0 +1,72 @@ +package main + +import ( + "bufio" + "math" + "os" + "strconv" + "strings" + + "github.com/ncruces/zenity" +) + +func progress(opts ...zenity.Option) (err error) { + dlg, err := zenity.Progress(opts...) + if err != nil { + return err + } + + if autoKill { + defer func() { + if err == zenity.ErrCanceled { + killParent() + } + }() + } + + if err := dlg.Text(text); err != nil { + return err + } + if err := dlg.Value(int(math.Round(percentage))); err != nil { + return err + } + + lines := make(chan string) + + go func() { + defer close(lines) + for scanner := bufio.NewScanner(os.Stdin); scanner.Scan(); { + lines <- scanner.Text() + } + }() + + for { + select { + case line, ok := <-lines: + if !ok { + break + } + if len(line) > 1 && line[0] == '#' { + if err := dlg.Text(strings.TrimSpace(line[1:])); err != nil { + return err + } + } else if v, err := strconv.ParseFloat(line, 64); err == nil { + if err := dlg.Value(int(math.Round(v))); err != nil { + return err + } + if v >= 100 && autoClose { + return dlg.Close() + } + } + continue + case <-dlg.Done(): + } + break + } + + if err := dlg.Complete(); err != nil { + return err + } + <-dlg.Done() + return dlg.Close() +} diff --git a/cmd/zenity/progress_unix.go b/cmd/zenity/progress_unix.go new file mode 100644 index 0000000..df4cdb7 --- /dev/null +++ b/cmd/zenity/progress_unix.go @@ -0,0 +1,12 @@ +// +build !windows,!js + +package main + +import ( + "os" + "syscall" +) + +func killParent() { + syscall.Kill(os.Getppid(), syscall.SIGHUP) +} diff --git a/cmd/zenity/progress_windows.go b/cmd/zenity/progress_windows.go new file mode 100644 index 0000000..6563f08 --- /dev/null +++ b/cmd/zenity/progress_windows.go @@ -0,0 +1,3 @@ +package main + +func killParent() {} diff --git a/color_test.go b/color_test.go index 1a19091..acf1aa4 100644 --- a/color_test.go +++ b/color_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/ncruces/zenity" + "go.uber.org/goleak" ) func ExampleSelectColor() { @@ -24,18 +25,19 @@ func ExampleSelectColor_palette() { // Output: } -func TestSelectColorTimeout(t *testing.T) { +func TestSelectColor_timeout(t *testing.T) { + defer goleak.VerifyNone(t) ctx, cancel := context.WithTimeout(context.Background(), time.Second/10) + defer cancel() _, err := zenity.SelectColor(zenity.Context(ctx)) if !os.IsTimeout(err) { t.Error("did not timeout:", err) } - - cancel() } -func TestSelectColorCancel(t *testing.T) { +func TestSelectColor_cancel(t *testing.T) { + defer goleak.VerifyNone(t) ctx, cancel := context.WithCancel(context.Background()) cancel() diff --git a/entry_test.go b/entry_test.go index 6395332..1c72a09 100644 --- a/entry_test.go +++ b/entry_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/ncruces/zenity" + "go.uber.org/goleak" ) func ExampleEntry() { @@ -16,18 +17,19 @@ func ExampleEntry() { // Output: } -func TestEntryTimeout(t *testing.T) { +func TestEntry_timeout(t *testing.T) { + defer goleak.VerifyNone(t) ctx, cancel := context.WithTimeout(context.Background(), time.Second/10) + defer cancel() _, err := zenity.Entry("", zenity.Context(ctx)) if !os.IsTimeout(err) { t.Error("did not timeout:", err) } - - cancel() } -func TestEntryCancel(t *testing.T) { +func TestEntry_cancel(t *testing.T) { + defer goleak.VerifyNone(t) ctx, cancel := context.WithCancel(context.Background()) cancel() diff --git a/entry_windows.go b/entry_windows.go index 0c2f604..4e0916d 100644 --- a/entry_windows.go +++ b/entry_windows.go @@ -29,6 +29,7 @@ func entry(text string, opts options) (out string, err error) { sendMessage.Call(editCtl, 0x0030 /* WM_SETFONT */, hfont, 1) sendMessage.Call(okBtn, 0x0030 /* WM_SETFONT */, hfont, 1) sendMessage.Call(cancelBtn, 0x0030 /* WM_SETFONT */, hfont, 1) + sendMessage.Call(extraBtn, 0x0030 /* WM_SETFONT */, hfont, 1) setWindowPos.Call(wnd, 0, 0, 0, dpi.Scale(281), dpi.Scale(141), 0x6) // SWP_NOZORDER|SWP_NOMOVE setWindowPos.Call(textCtl, 0, dpi.Scale(12), dpi.Scale(10), dpi.Scale(241), dpi.Scale(16), 0x4) // SWP_NOZORDER setWindowPos.Call(editCtl, 0, dpi.Scale(12), dpi.Scale(30), dpi.Scale(241), dpi.Scale(24), 0x4) // SWP_NOZORDER @@ -36,7 +37,6 @@ func entry(text string, opts options) (out string, err error) { setWindowPos.Call(okBtn, 0, dpi.Scale(95), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER setWindowPos.Call(cancelBtn, 0, dpi.Scale(178), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER } else { - sendMessage.Call(extraBtn, 0x0030 /* WM_SETFONT */, hfont, 1) setWindowPos.Call(okBtn, 0, dpi.Scale(12), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER setWindowPos.Call(extraBtn, 0, dpi.Scale(95), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER setWindowPos.Call(cancelBtn, 0, dpi.Scale(178), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER diff --git a/file_test.go b/file_test.go index 34a9c03..65d6a8d 100644 --- a/file_test.go +++ b/file_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/ncruces/zenity" + "go.uber.org/goleak" ) const defaultPath = `` @@ -77,7 +78,7 @@ var fileFuncs = []func(...zenity.Option) (string, error){ }, } -func TestFileTimeout(t *testing.T) { +func TestFile_timeout(t *testing.T) { for _, f := range fileFuncs { ctx, cancel := context.WithTimeout(context.Background(), time.Second/10) @@ -87,10 +88,12 @@ func TestFileTimeout(t *testing.T) { } cancel() + goleak.VerifyNone(t) } } -func TestFileCancel(t *testing.T) { +func TestFile_cancel(t *testing.T) { + defer goleak.VerifyNone(t) ctx, cancel := context.WithCancel(context.Background()) cancel() diff --git a/internal/zenutil/env.go b/internal/zenutil/env.go new file mode 100644 index 0000000..12e4d5f --- /dev/null +++ b/internal/zenutil/env.go @@ -0,0 +1,12 @@ +package zenutil + +// These are internal. +const ( + ErrCanceled = stringErr("dialog canceled") + ErrExtraButton = stringErr("extra button pressed") + ErrUnsupported = stringErr("unsupported option") +) + +type stringErr string + +func (e stringErr) Error() string { return string(e) } diff --git a/internal/zenutil/env_darwin.go b/internal/zenutil/env_darwin.go index 7233920..fd9fdda 100644 --- a/internal/zenutil/env_darwin.go +++ b/internal/zenutil/env_darwin.go @@ -10,5 +10,4 @@ var ( Command bool Timeout int Separator = "\x00" - Canceled error ) diff --git a/internal/zenutil/env_unix.go b/internal/zenutil/env_unix.go index 5788b42..9cd9e04 100644 --- a/internal/zenutil/env_unix.go +++ b/internal/zenutil/env_unix.go @@ -13,5 +13,4 @@ var ( Command bool Timeout int Separator = "\x1e" - Canceled error ) diff --git a/internal/zenutil/env_windows.go b/internal/zenutil/env_windows.go index 5c20d99..ee0957a 100644 --- a/internal/zenutil/env_windows.go +++ b/internal/zenutil/env_windows.go @@ -10,5 +10,4 @@ var ( Command bool Timeout int Separator string - Canceled error ) diff --git a/internal/zenutil/osa_generated.go b/internal/zenutil/osa_generated.go index 0b0d788..ed15e73 100644 --- a/internal/zenutil/osa_generated.go +++ b/internal/zenutil/osa_generated.go @@ -48,16 +48,19 @@ res.join({{json .Separator}}) var app=Application.currentApplication() app.includeStandardAdditions=true void app.displayNotification({{json .Text}},{{json .Options}}) -{{- end}}`)) - -var progress = ` +{{- end}} +{{define "progress" -}} var app=Application.currentApplication() app.includeStandardAdditions=true app.activate() ObjC.import('stdlib') ObjC.import('readline') -try{Progress.totalUnitCount=$.getenv('total')}catch{} -try{Progress.description=$.getenv('description')}catch{} +{{- if .Total}} +Progress.totalUnitCount={{.Total}} +{{- end}} +{{- if .Description}} +Progress.description={{json .Description}} +{{- end}} while(true){var s try{s=$.readline('')}catch(e){if(e.errorNumber===-128)$.exit(1) break} @@ -65,4 +68,5 @@ if(s.indexOf('#')===0){Progress.additionalDescription=s.slice(1) continue} var i=parseInt(s) if(i>=0&&Progress.totalUnitCount>0){Progress.completedUnitCount=i -continue}}` +continue}} +{{- end}}`)) diff --git a/internal/zenutil/osa_generator.go b/internal/zenutil/osa_generator.go index a6febc7..f69dfb6 100644 --- a/internal/zenutil/osa_generator.go +++ b/internal/zenutil/osa_generator.go @@ -21,11 +21,6 @@ func main() { log.Fatal(err) } - var args struct { - Templates string - Progress string - } - var str strings.Builder for _, file := range files { @@ -39,16 +34,11 @@ func main() { log.Fatal(err) } - name = strings.TrimSuffix(name, filepath.Ext(name)) - if name == "progress" { - args.Progress = string(data) - } else { - str.WriteString("\n" + `{{define "`) - str.WriteString(name) - str.WriteString(`" -}}` + "\n") - str.Write(data) - str.WriteString("\n{{- end}}") - } + str.WriteString("\n" + `{{define "`) + str.WriteString(strings.TrimSuffix(name, filepath.Ext(name))) + str.WriteString(`" -}}` + "\n") + str.Write(data) + str.WriteString("\n{{- end}}") } out, err := os.Create("osa_generated.go") @@ -56,8 +46,7 @@ func main() { log.Fatal(err) } - args.Templates = str.String() - err = generator.Execute(out, args) + err = generator.Execute(out, str.String()) if err != nil { log.Fatal(err) } @@ -116,6 +105,4 @@ import ( var scripts = template.Must(template.New("").Funcs(template.FuncMap{"json": func(v interface{}) (string, error) { b, err := json.Marshal(v) return string(b), err -}}).Parse(` + "`{{.Templates}}`" + `)) - -var progress = ` + "`\n{{.Progress}}`\n")) +}}).Parse(` + "`{{.}}`))\n")) diff --git a/internal/zenutil/osascripts/progress.js b/internal/zenutil/osascripts/progress.gojs similarity index 78% rename from internal/zenutil/osascripts/progress.js rename to internal/zenutil/osascripts/progress.gojs index 424213e..45c8102 100644 --- a/internal/zenutil/osascripts/progress.js +++ b/internal/zenutil/osascripts/progress.gojs @@ -5,8 +5,12 @@ app.activate() ObjC.import('stdlib') ObjC.import('readline') -try { Progress.totalUnitCount = $.getenv('total') } catch { } -try { Progress.description = $.getenv('description') } catch { } +{{- if .Total}} + Progress.totalUnitCount = {{.Total}} +{{- end}} +{{- if .Description}} + Progress.description = {{json .Description}} +{{- end}} while (true) { var s diff --git a/internal/zenutil/run_progress.go b/internal/zenutil/progress_unix.go similarity index 86% rename from internal/zenutil/run_progress.go rename to internal/zenutil/progress_unix.go index f0d2f3a..5706b23 100644 --- a/internal/zenutil/run_progress.go +++ b/internal/zenutil/progress_unix.go @@ -3,6 +3,7 @@ package zenutil import ( + "bytes" "context" "io" "os" @@ -54,13 +55,9 @@ func (d *progressDialog) Done() <-chan struct{} { } func (d *progressDialog) Complete() error { + err := d.Value(d.max) close(d.lines) - select { - case <-d.done: - return d.err - default: - return nil - } + return err } func (d *progressDialog) Close() error { @@ -70,7 +67,7 @@ func (d *progressDialog) Close() error { return d.err } -func (d *progressDialog) wait() { +func (d *progressDialog) wait(extra *string, out *bytes.Buffer) { err := d.cmd.Wait() if cerr := d.ctx.Err(); cerr != nil { err = cerr @@ -80,7 +77,11 @@ func (d *progressDialog) wait() { case eerr.ExitCode() == -1 && atomic.LoadInt32(&d.closed) != 0: err = nil case eerr.ExitCode() == 1: - err = Canceled + if extra != nil && *extra+"\n" == string(out.Bytes()) { + err = ErrExtraButton + } else { + err = ErrCanceled + } } } d.err = err @@ -103,7 +104,10 @@ func (d *progressDialog) pipe(w io.WriteCloser) { line = s case <-d.ctx.Done(): return + case <-d.done: + return case <-time.After(timeout): + // line = "" } if _, err := w.Write([]byte(line + "\n")); err != nil { return diff --git a/internal/zenutil/run_darwin.go b/internal/zenutil/run_darwin.go index 3112dc7..d7e9b87 100644 --- a/internal/zenutil/run_darwin.go +++ b/internal/zenutil/run_darwin.go @@ -7,27 +7,23 @@ import ( "os" "os/exec" "path/filepath" - "strings" "syscall" - "time" ) // Run is internal. func Run(ctx context.Context, script string, data interface{}) ([]byte, error) { - var buf strings.Builder - + var buf bytes.Buffer err := scripts.ExecuteTemplate(&buf, script, data) if err != nil { return nil, err } - script = buf.String() if Command { // Try to use syscall.Exec, fallback to exec.Command. if path, err := exec.LookPath("osascript"); err != nil { } else if t, err := ioutil.TempFile("", ""); err != nil { } else if err := os.Remove(t.Name()); err != nil { - } else if _, err := t.WriteString(script); err != nil { + } else if _, err := t.Write(buf.Bytes()); err != nil { } else if _, err := t.Seek(0, 0); err != nil { } else if err := syscall.Dup2(int(t.Fd()), syscall.Stdin); err != nil { } else if err := os.Stderr.Close(); err != nil { @@ -38,7 +34,7 @@ func Run(ctx context.Context, script string, data interface{}) ([]byte, error) { if ctx != nil { cmd := exec.CommandContext(ctx, "osascript", "-l", "JavaScript") - cmd.Stdin = strings.NewReader(script) + cmd.Stdin = &buf out, err := cmd.Output() if ctx.Err() != nil { err = ctx.Err() @@ -46,45 +42,57 @@ func Run(ctx context.Context, script string, data interface{}) ([]byte, error) { return out, err } cmd := exec.Command("osascript", "-l", "JavaScript") - cmd.Stdin = strings.NewReader(script) + cmd.Stdin = &buf return cmd.Output() } // RunProgress is internal. -func RunProgress(ctx context.Context, max int, env []string) (*progressDialog, error) { +func RunProgress(ctx context.Context, max int, data Progress) (dlg *progressDialog, err error) { + var buf bytes.Buffer + err = scripts.ExecuteTemplate(&buf, "progress", data) + if err != nil { + return nil, err + } + t, err := ioutil.TempDir("", "") if err != nil { return nil, err } defer func() { if err != nil { + if ctx != nil && ctx.Err() != nil { + err = ctx.Err() + } os.RemoveAll(t) } }() + if ctx == nil { + ctx = context.Background() + } var cmd *exec.Cmd name := filepath.Join(t, "progress.app") - cmd = exec.Command("osacompile", "-l", "JavaScript", "-o", name) - cmd.Stdin = strings.NewReader(progress) + cmd = exec.CommandContext(ctx, "osacompile", "-l", "JavaScript", "-o", name) + cmd.Stdin = &buf if err := cmd.Run(); err != nil { return nil, err } plist := filepath.Join(name, "Contents/Info.plist") - cmd = exec.Command("defaults", "write", plist, "LSUIElement", "true") + cmd = exec.CommandContext(ctx, "defaults", "write", plist, "LSUIElement", "true") if err := cmd.Run(); err != nil { return nil, err } - cmd = exec.Command("defaults", "write", plist, "CFBundleName", "") + cmd = exec.CommandContext(ctx, "defaults", "write", plist, "CFBundleName", "") if err := cmd.Run(); err != nil { return nil, err } var executable string - cmd = exec.Command("defaults", "read", plist, "CFBundleExecutable") + cmd = exec.CommandContext(ctx, "defaults", "read", plist, "CFBundleExecutable") if out, err := cmd.Output(); err != nil { return nil, err } else { @@ -92,8 +100,7 @@ func RunProgress(ctx context.Context, max int, env []string) (*progressDialog, e executable = filepath.Join(name, "Contents/MacOS", string(out)) } - cmd = exec.Command(executable) - cmd.Env = env + cmd = exec.CommandContext(ctx, executable) pipe, err := cmd.StdinPipe() if err != nil { return nil, err @@ -101,11 +108,9 @@ func RunProgress(ctx context.Context, max int, env []string) (*progressDialog, e if err := cmd.Start(); err != nil { return nil, err } - if ctx == nil { - ctx = context.Background() - } - dlg := &progressDialog{ + dlg = &progressDialog{ + ctx: ctx, cmd: cmd, max: max, lines: make(chan string), @@ -114,7 +119,7 @@ func RunProgress(ctx context.Context, max int, env []string) (*progressDialog, e go dlg.pipe(pipe) go func() { defer os.RemoveAll(t) - dlg.wait() + dlg.wait(nil, nil) }() return dlg, nil } @@ -185,7 +190,6 @@ type File struct { Options FileOptions } -// FileOptions is internal. type FileOptions struct { Prompt *string `json:"withPrompt,omitempty"` Type []string `json:"ofType,omitempty"` @@ -206,3 +210,9 @@ type NotifyOptions struct { Title *string `json:"withTitle,omitempty"` Subtitle string `json:"subtitle,omitempty"` } + +// Progress is internal. +type Progress struct { + Description *string + Total *int +} diff --git a/internal/zenutil/run_unix.go b/internal/zenutil/run_unix.go index 5a4b0cd..b270340 100644 --- a/internal/zenutil/run_unix.go +++ b/internal/zenutil/run_unix.go @@ -3,6 +3,7 @@ package zenutil import ( + "bytes" "context" "os" "os/exec" @@ -42,25 +43,30 @@ func Run(ctx context.Context, args []string) ([]byte, error) { } // RunProgress is internal. -func RunProgress(ctx context.Context, max int, args []string) (*progressDialog, error) { +func RunProgress(ctx context.Context, max int, extra *string, args []string) (*progressDialog, 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 { + ctx = context.Background() + } - cmd := exec.Command(tool, args...) + cmd := exec.CommandContext(ctx, tool, args...) pipe, err := cmd.StdinPipe() if err != nil { return nil, err } + var out *bytes.Buffer + if extra != nil { + out = &bytes.Buffer{} + cmd.Stdout = out + } if err := cmd.Start(); err != nil { return nil, err } - if ctx == nil { - ctx = context.Background() - } dlg := &progressDialog{ ctx: ctx, @@ -71,6 +77,6 @@ func RunProgress(ctx context.Context, max int, args []string) (*progressDialog, done: make(chan struct{}), } go dlg.pipe(pipe) - go dlg.wait() + go dlg.wait(extra, out) return dlg, nil } diff --git a/list_darwin.go b/list_darwin.go index 5ccaa7e..1ccb7ea 100644 --- a/list_darwin.go +++ b/list_darwin.go @@ -5,6 +5,10 @@ import ( ) func list(text string, items []string, opts options) (string, error) { + if opts.extraButton != nil { + return "", ErrUnsupported + } + var data zenutil.List data.Items = items data.Options.Prompt = &text @@ -19,6 +23,10 @@ func list(text string, items []string, opts options) (string, error) { } func listMultiple(text string, items []string, opts options) ([]string, error) { + if opts.extraButton != nil { + return nil, ErrUnsupported + } + var data zenutil.List data.Items = items data.Options.Prompt = &text diff --git a/list_test.go b/list_test.go index f7ec686..9f23f29 100644 --- a/list_test.go +++ b/list_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/ncruces/zenity" + "go.uber.org/goleak" ) func ExampleList() { @@ -44,18 +45,19 @@ func ExampleListMultipleItems() { // Output: } -func TestListTimeout(t *testing.T) { +func TestList_timeout(t *testing.T) { + defer goleak.VerifyNone(t) ctx, cancel := context.WithTimeout(context.Background(), time.Second/10) + defer cancel() _, err := zenity.List("", nil, zenity.Context(ctx)) if !os.IsTimeout(err) { t.Error("did not timeout:", err) } - - cancel() } -func TestListCancel(t *testing.T) { +func TestList_cancel(t *testing.T) { + defer goleak.VerifyNone(t) ctx, cancel := context.WithCancel(context.Background()) cancel() diff --git a/list_windows.go b/list_windows.go index f8efece..f8d4576 100644 --- a/list_windows.go +++ b/list_windows.go @@ -42,6 +42,7 @@ func listDlg(text string, items []string, multiple bool, opts options) (out []st sendMessage.Call(listCtl, 0x0030 /* WM_SETFONT */, hfont, 1) sendMessage.Call(okBtn, 0x0030 /* WM_SETFONT */, hfont, 1) sendMessage.Call(cancelBtn, 0x0030 /* WM_SETFONT */, hfont, 1) + sendMessage.Call(extraBtn, 0x0030 /* WM_SETFONT */, hfont, 1) setWindowPos.Call(wnd, 0, 0, 0, dpi.Scale(281), dpi.Scale(281), 0x6) // SWP_NOZORDER|SWP_NOMOVE setWindowPos.Call(textCtl, 0, dpi.Scale(12), dpi.Scale(10), dpi.Scale(241), dpi.Scale(16), 0x4) // SWP_NOZORDER setWindowPos.Call(listCtl, 0, dpi.Scale(12), dpi.Scale(30), dpi.Scale(241), dpi.Scale(164), 0x4) // SWP_NOZORDER @@ -49,7 +50,6 @@ func listDlg(text string, items []string, multiple bool, opts options) (out []st setWindowPos.Call(okBtn, 0, dpi.Scale(95), dpi.Scale(206), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER setWindowPos.Call(cancelBtn, 0, dpi.Scale(178), dpi.Scale(206), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER } else { - sendMessage.Call(extraBtn, 0x0030 /* WM_SETFONT */, hfont, 1) setWindowPos.Call(okBtn, 0, dpi.Scale(12), dpi.Scale(206), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER setWindowPos.Call(extraBtn, 0, dpi.Scale(95), dpi.Scale(206), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER setWindowPos.Call(cancelBtn, 0, dpi.Scale(178), dpi.Scale(206), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER diff --git a/msg_test.go b/msg_test.go index be83c8b..ea649ce 100644 --- a/msg_test.go +++ b/msg_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/ncruces/zenity" + "go.uber.org/goleak" ) func ExampleError() { @@ -45,7 +46,7 @@ var msgFuncs = []func(string, ...zenity.Option) error{ zenity.Question, } -func TestMessageTimeout(t *testing.T) { +func TestMessage_timeout(t *testing.T) { for _, f := range msgFuncs { ctx, cancel := context.WithTimeout(context.Background(), time.Second/10) @@ -55,10 +56,12 @@ func TestMessageTimeout(t *testing.T) { } cancel() + goleak.VerifyNone(t) } } -func TestMessageCancel(t *testing.T) { +func TestMessage_cancel(t *testing.T) { + defer goleak.VerifyNone(t) ctx, cancel := context.WithCancel(context.Background()) cancel() diff --git a/notify_test.go b/notify_test.go index c002d12..015ae61 100644 --- a/notify_test.go +++ b/notify_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/ncruces/zenity" + "go.uber.org/goleak" ) func ExampleNotify() { @@ -15,7 +16,8 @@ func ExampleNotify() { // Output: } -func TestNotifyCancel(t *testing.T) { +func TestNotify_cancel(t *testing.T) { + defer goleak.VerifyNone(t) ctx, cancel := context.WithCancel(context.Background()) cancel() diff --git a/progress.go b/progress.go index 3fd781f..281f56b 100644 --- a/progress.go +++ b/progress.go @@ -29,7 +29,7 @@ type ProgressDialog interface { Done() <-chan struct{} } -// MaxValue returns an Option to set the maximum value (macOS only). +// MaxValue returns an Option to set the maximum value (Windows and macOS only). // The default maximum value is 100. func MaxValue(value int) Option { return funcOption(func(o *options) { o.maxValue = value }) @@ -40,7 +40,7 @@ func Pulsate() Option { return funcOption(func(o *options) { o.maxValue = -1 }) } -// NoCancel returns an Option to hide the Cancel button (Unix only). +// NoCancel returns an Option to hide the Cancel button (Windows and Unix only). func NoCancel() Option { return funcOption(func(o *options) { o.noCancel = true }) } diff --git a/progress_darwin.go b/progress_darwin.go index 90aabb7..5df70f3 100644 --- a/progress_darwin.go +++ b/progress_darwin.go @@ -1,21 +1,22 @@ package zenity import ( - "strconv" - "github.com/ncruces/zenity/internal/zenutil" ) func progress(opts options) (ProgressDialog, error) { - var env []string - if opts.title != nil { - env = append(env, "description="+*opts.title) + if opts.extraButton != nil { + return nil, ErrUnsupported } + + var data zenutil.Progress + data.Description = opts.title if opts.maxValue == 0 { opts.maxValue = 100 } if opts.maxValue >= 0 { - env = append(env, "total="+strconv.Itoa(opts.maxValue)) + data.Total = &opts.maxValue } - return zenutil.RunProgress(opts.ctx, opts.maxValue, env) + + return zenutil.RunProgress(opts.ctx, opts.maxValue, data) } diff --git a/progress_test.go b/progress_test.go new file mode 100644 index 0000000..b1657dd --- /dev/null +++ b/progress_test.go @@ -0,0 +1,100 @@ +package zenity_test + +import ( + "context" + "errors" + "log" + "testing" + "time" + + "github.com/ncruces/zenity" + "go.uber.org/goleak" +) + +func ExampleProgress() { + dlg, err := zenity.Progress( + zenity.Title("Update System Logs")) + if err != nil { + log.Fatal(err) + } + defer dlg.Close() + + dlg.Text("Scanning mail logs...") + dlg.Value(0) + time.Sleep(time.Second) + + dlg.Value(25) + time.Sleep(time.Second) + + dlg.Text("Updating mail logs...") + dlg.Value(50) + time.Sleep(time.Second) + + dlg.Text("Resetting cron jobs...") + dlg.Value(75) + time.Sleep(time.Second) + + dlg.Text("Rebooting system...") + dlg.Value(100) + time.Sleep(time.Second) + + dlg.Complete() + time.Sleep(time.Second) + + // Output: +} + +func ExampleProgress_pulsate() { + dlg, err := zenity.Progress( + zenity.Title("Update System Logs"), + zenity.Pulsate()) + if err != nil { + log.Fatal(err) + } + defer dlg.Close() + + dlg.Text("Scanning mail logs...") + time.Sleep(time.Second) + + dlg.Text("Updating mail logs...") + time.Sleep(time.Second) + + dlg.Text("Resetting cron jobs...") + time.Sleep(time.Second) + + dlg.Text("Rebooting system...") + time.Sleep(time.Second) + + dlg.Complete() + time.Sleep(time.Second) + + // Output: +} + +func TestProgress_cancel(t *testing.T) { + defer goleak.VerifyNone(t) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := zenity.Progress(zenity.Context(ctx)) + if !errors.Is(err, context.Canceled) { + t.Error("was not canceled:", err) + } +} + +func TestProgress_cancelAfter(t *testing.T) { + defer goleak.VerifyNone(t) + ctx, cancel := context.WithCancel(context.Background()) + + dlg, err := zenity.Progress(zenity.Context(ctx)) + if err != nil { + t.Fatal(err) + } + + go cancel() + <-dlg.Done() + err = dlg.Close() + if !errors.Is(err, context.Canceled) { + t.Error("was not canceled:", err) + } +} diff --git a/progress_unix.go b/progress_unix.go index 5c3dd1c..5be2ed2 100644 --- a/progress_unix.go +++ b/progress_unix.go @@ -24,5 +24,5 @@ func progress(opts options) (ProgressDialog, error) { if opts.timeRemaining { args = append(args, "--time-remaining") } - return zenutil.RunProgress(opts.ctx, opts.maxValue, args) + return zenutil.RunProgress(opts.ctx, opts.maxValue, opts.extraButton, args) } diff --git a/progress_windows.go b/progress_windows.go index c0b46ba..8047bb8 100644 --- a/progress_windows.go +++ b/progress_windows.go @@ -48,25 +48,31 @@ func progressDlg(opts options, dlg *progressDialog) (err error) { defer font.Delete() defWindowProc := defWindowProc.Addr() - var wnd, textCtl, progCtl uintptr - var okBtn, cancelBtn, extraBtn uintptr - layout := func(dpi dpi) { hfont := font.ForDPI(dpi) - sendMessage.Call(textCtl, 0x0030 /* WM_SETFONT */, hfont, 1) - sendMessage.Call(okBtn, 0x0030 /* WM_SETFONT */, hfont, 1) - sendMessage.Call(cancelBtn, 0x0030 /* WM_SETFONT */, hfont, 1) - setWindowPos.Call(wnd, 0, 0, 0, dpi.Scale(281), dpi.Scale(141), 0x6) // SWP_NOZORDER|SWP_NOMOVE - setWindowPos.Call(textCtl, 0, dpi.Scale(12), dpi.Scale(10), dpi.Scale(241), dpi.Scale(16), 0x4) // SWP_NOZORDER - setWindowPos.Call(progCtl, 0, dpi.Scale(12), dpi.Scale(30), dpi.Scale(241), dpi.Scale(24), 0x4) // SWP_NOZORDER - if extraBtn == 0 { - setWindowPos.Call(okBtn, 0, dpi.Scale(95), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER - setWindowPos.Call(cancelBtn, 0, dpi.Scale(178), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER + sendMessage.Call(dlg.textCtl, 0x0030 /* WM_SETFONT */, hfont, 1) + sendMessage.Call(dlg.okBtn, 0x0030 /* WM_SETFONT */, hfont, 1) + sendMessage.Call(dlg.cancelBtn, 0x0030 /* WM_SETFONT */, hfont, 1) + sendMessage.Call(dlg.extraBtn, 0x0030 /* WM_SETFONT */, hfont, 1) + setWindowPos.Call(dlg.wnd, 0, 0, 0, dpi.Scale(281), dpi.Scale(141), 0x6) // SWP_NOZORDER|SWP_NOMOVE + setWindowPos.Call(dlg.textCtl, 0, dpi.Scale(12), dpi.Scale(10), dpi.Scale(241), dpi.Scale(16), 0x4) // SWP_NOZORDER + setWindowPos.Call(dlg.progCtl, 0, dpi.Scale(12), dpi.Scale(30), dpi.Scale(241), dpi.Scale(24), 0x4) // SWP_NOZORDER + if dlg.extraBtn == 0 { + if dlg.cancelBtn == 0 { + setWindowPos.Call(dlg.okBtn, 0, dpi.Scale(178), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER + } else { + setWindowPos.Call(dlg.okBtn, 0, dpi.Scale(95), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER + setWindowPos.Call(dlg.cancelBtn, 0, dpi.Scale(178), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER + } } else { - sendMessage.Call(extraBtn, 0x0030 /* WM_SETFONT */, hfont, 1) - setWindowPos.Call(okBtn, 0, dpi.Scale(12), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER - setWindowPos.Call(extraBtn, 0, dpi.Scale(95), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER - setWindowPos.Call(cancelBtn, 0, dpi.Scale(178), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER + if dlg.cancelBtn == 0 { + setWindowPos.Call(dlg.okBtn, 0, dpi.Scale(95), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER + setWindowPos.Call(dlg.extraBtn, 0, dpi.Scale(178), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER + } else { + setWindowPos.Call(dlg.okBtn, 0, dpi.Scale(12), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER + setWindowPos.Call(dlg.extraBtn, 0, dpi.Scale(95), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER + setWindowPos.Call(dlg.cancelBtn, 0, dpi.Scale(178), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER + } } } @@ -118,54 +124,52 @@ func progressDlg(opts options, dlg *progressDialog) (err error) { } defer unregisterClass.Call(cls, instance) - wnd, _, _ = createWindowEx.Call(0x10101, // WS_EX_CONTROLPARENT|WS_EX_WINDOWEDGE|WS_EX_DLGMODALFRAME + dlg.wnd, _, _ = createWindowEx.Call(0x10101, // WS_EX_CONTROLPARENT|WS_EX_WINDOWEDGE|WS_EX_DLGMODALFRAME cls, strptr(*opts.title), 0x84c80000, // WS_POPUPWINDOW|WS_CLIPSIBLINGS|WS_DLGFRAME 0x80000000, // CW_USEDEFAULT 0x80000000, // CW_USEDEFAULT 281, 141, 0, 0, instance, 0) - textCtl, _, _ = createWindowEx.Call(0, + dlg.textCtl, _, _ = createWindowEx.Call(0, strptr("STATIC"), 0, 0x5002e080, // WS_CHILD|WS_VISIBLE|WS_GROUP|SS_WORDELLIPSIS|SS_EDITCONTROL|SS_NOPREFIX - 12, 10, 241, 16, wnd, 0, instance, 0) + 12, 10, 241, 16, dlg.wnd, 0, instance, 0) var flags uintptr = 0x50000001 // WS_CHILD|WS_VISIBLE|PBS_SMOOTH if opts.maxValue < 0 { flags |= 0x8 // PBS_MARQUEE } - progCtl, _, _ = createWindowEx.Call(0, + dlg.progCtl, _, _ = createWindowEx.Call(0, strptr("msctls_progress32"), // PROGRESS_CLASS 0, flags, - 12, 30, 241, 24, wnd, 0, instance, 0) + 12, 30, 241, 24, dlg.wnd, 0, instance, 0) - okBtn, _, _ = createWindowEx.Call(0, + dlg.okBtn, _, _ = createWindowEx.Call(0, strptr("BUTTON"), strptr(*opts.okLabel), 0x58030001, // WS_CHILD|WS_VISIBLE|WS_DISABLED|WS_GROUP|WS_TABSTOP|BS_DEFPUSHBUTTON - 12, 66, 75, 24, wnd, 1 /* IDOK */, instance, 0) - cancelBtn, _, _ = createWindowEx.Call(0, - strptr("BUTTON"), strptr(*opts.cancelLabel), - 0x50010000, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP - 12, 66, 75, 24, wnd, 2 /* IDCANCEL */, instance, 0) + 12, 66, 75, 24, dlg.wnd, 1 /* IDOK */, instance, 0) + if !opts.noCancel { + dlg.cancelBtn, _, _ = createWindowEx.Call(0, + strptr("BUTTON"), strptr(*opts.cancelLabel), + 0x50010000, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP + 12, 66, 75, 24, dlg.wnd, 2 /* IDCANCEL */, instance, 0) + } if opts.extraButton != nil { - extraBtn, _, _ = createWindowEx.Call(0, + dlg.extraBtn, _, _ = createWindowEx.Call(0, strptr("BUTTON"), strptr(*opts.extraButton), 0x50010000, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP - 12, 66, 75, 24, wnd, 7 /* IDNO */, instance, 0) + 12, 66, 75, 24, dlg.wnd, 7 /* IDNO */, instance, 0) } - layout(getDPI(wnd)) - centerWindow(wnd) - showWindow.Call(wnd, 1 /* SW_SHOWNORMAL */, 0) + layout(getDPI(dlg.wnd)) + centerWindow(dlg.wnd) + showWindow.Call(dlg.wnd, 1 /* SW_SHOWNORMAL */, 0) if opts.maxValue < 0 { - sendMessage.Call(progCtl, 0x40a /* PBM_SETMARQUEE */, 1, 0) + sendMessage.Call(dlg.progCtl, 0x40a /* PBM_SETMARQUEE */, 1, 0) } else { - sendMessage.Call(progCtl, 0x406 /* PBM_SETRANGE32 */, 0, uintptr(opts.maxValue)) + sendMessage.Call(dlg.progCtl, 0x406 /* PBM_SETRANGE32 */, 0, uintptr(opts.maxValue)) } - dlg.prog = progCtl - dlg.text = textCtl - dlg.ok = okBtn - dlg.wnd = wnd dlg.init.Done() if opts.ctx != nil { @@ -174,7 +178,7 @@ func progressDlg(opts options, dlg *progressDialog) (err error) { go func() { select { case <-opts.ctx.Done(): - sendMessage.Call(wnd, 0x0112 /* WM_SYSCOMMAND */, 0xf060 /* SC_CLOSE */, 0) + sendMessage.Call(dlg.wnd, 0x0112 /* WM_SYSCOMMAND */, 0xf060 /* SC_CLOSE */, 0) case <-wait: } }() @@ -183,7 +187,7 @@ func progressDlg(opts options, dlg *progressDialog) (err error) { // set default values err = nil - if err := messageLoop(wnd); err != nil { + if err := messageLoop(dlg.wnd); err != nil { return err } if opts.ctx != nil && opts.ctx.Err() != nil { @@ -193,26 +197,22 @@ func progressDlg(opts options, dlg *progressDialog) (err error) { } type progressDialog struct { - err error - done chan struct{} - init sync.WaitGroup - prog uintptr - text uintptr - wnd uintptr - ok uintptr - max int -} - -func (d *progressDialog) Close() error { - sendMessage.Call(d.wnd, 0x0112 /* WM_SYSCOMMAND */, 0xf060 /* SC_CLOSE */, 0) - <-d.done - return d.err + max int + done chan struct{} + init sync.WaitGroup + wnd uintptr + textCtl uintptr + progCtl uintptr + okBtn uintptr + cancelBtn uintptr + extraBtn uintptr + err error } func (d *progressDialog) Text(text string) error { select { default: - setWindowText.Call(d.text, strptr(text)) + setWindowText.Call(d.textCtl, strptr(text)) return nil case <-d.done: return d.err @@ -222,9 +222,9 @@ func (d *progressDialog) Text(text string) error { func (d *progressDialog) Value(value int) error { select { default: - sendMessage.Call(d.prog, 0x402 /* PBM_SETPOS */, uintptr(value), 0) + sendMessage.Call(d.progCtl, 0x402 /* PBM_SETPOS */, uintptr(value), 0) if value >= d.max { - enableWindow.Call(d.ok, 1) + enableWindow.Call(d.okBtn, 1) } return nil case <-d.done: @@ -239,3 +239,23 @@ func (d *progressDialog) MaxValue() int { func (d *progressDialog) Done() <-chan struct{} { return d.done } + +func (d *progressDialog) Complete() error { + select { + default: + setWindowLong.Call(d.progCtl, intptr(-16) /* GWL_STYLE */, 0x50000001 /* WS_CHILD|WS_VISIBLE|PBS_SMOOTH */) + sendMessage.Call(d.progCtl, 0x406 /* PBM_SETRANGE32 */, 0, 1) + sendMessage.Call(d.progCtl, 0x402 /* PBM_SETPOS */, 1, 0) + enableWindow.Call(d.okBtn, 1) + enableWindow.Call(d.cancelBtn, 0) + return nil + case <-d.done: + return d.err + } +} + +func (d *progressDialog) Close() error { + sendMessage.Call(d.wnd, 0x0112 /* WM_SYSCOMMAND */, 0xf060 /* SC_CLOSE */, 0) + <-d.done + return d.err +} diff --git a/pwd_test.go b/pwd_test.go index 81a7067..7ed751b 100644 --- a/pwd_test.go +++ b/pwd_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/ncruces/zenity" + "go.uber.org/goleak" ) func ExamplePassword() { @@ -15,18 +16,19 @@ func ExamplePassword() { // Output: } -func TestPasswordTimeout(t *testing.T) { +func TestPassword_timeout(t *testing.T) { + defer goleak.VerifyNone(t) ctx, cancel := context.WithTimeout(context.Background(), time.Second/10) + defer cancel() _, _, err := zenity.Password(zenity.Context(ctx)) if !os.IsTimeout(err) { t.Error("did not timeout:", err) } - - cancel() } -func TestPasswordCancel(t *testing.T) { +func TestPassword_cancel(t *testing.T) { + defer goleak.VerifyNone(t) ctx, cancel := context.WithCancel(context.Background()) cancel() diff --git a/util_unix.go b/util_unix.go index 3e53728..1bc15e0 100644 --- a/util_unix.go +++ b/util_unix.go @@ -3,7 +3,6 @@ package zenity import ( - "bytes" "os/exec" "strconv" "strings" @@ -56,9 +55,8 @@ func appendIcon(args []string, opts options) []string { } func strResult(opts options, out []byte, err error) (string, error) { - out = bytes.TrimSuffix(out, []byte{'\n'}) if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { - if opts.extraButton != nil && *opts.extraButton == string(out) { + if opts.extraButton != nil && *opts.extraButton+"\n" == string(out) { return "", ErrExtraButton } return "", ErrCanceled diff --git a/util_windows.go b/util_windows.go index 0444c0b..a1cc59a 100644 --- a/util_windows.go +++ b/util_windows.go @@ -66,6 +66,7 @@ var ( systemParametersInfo = user32.NewProc("SystemParametersInfoW") setWindowPos = user32.NewProc("SetWindowPos") getWindowRect = user32.NewProc("GetWindowRect") + setWindowLong = user32.NewProc("SetWindowLongPtrW") getSystemMetrics = user32.NewProc("GetSystemMetrics") unregisterClass = user32.NewProc("UnregisterClassW") registerClassEx = user32.NewProc("RegisterClassExW") diff --git a/zenity.go b/zenity.go index bb7f3ea..f4f6350 100644 --- a/zenity.go +++ b/zenity.go @@ -17,25 +17,17 @@ import ( "github.com/ncruces/zenity/internal/zenutil" ) -type stringErr string - -func (e stringErr) Error() string { return string(e) } - func stringPtr(s string) *string { return &s } // ErrCanceled is returned when the cancel button is pressed, // or window functions are used to close the dialog. -const ErrCanceled = stringErr("dialog canceled") +const ErrCanceled = zenutil.ErrCanceled // ErrExtraButton is returned when the extra button is pressed. -const ErrExtraButton = stringErr("extra button pressed") +const ErrExtraButton = zenutil.ErrExtraButton // ErrUnsupported is returned when a combination of options is not supported. -const ErrUnsupported = stringErr("unsupported option") - -func init() { - zenutil.Canceled = ErrCanceled -} +const ErrUnsupported = zenutil.ErrUnsupported type options struct { // General options