From 02bbc4741c1752c206606dbe5fe348c2640ccbb5 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Wed, 14 Apr 2021 15:18:56 +0100 Subject: [PATCH 01/12] WIP: progress (macOS). --- internal/zenutil/osascripts/progress.js | 37 +++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 internal/zenutil/osascripts/progress.js diff --git a/internal/zenutil/osascripts/progress.js b/internal/zenutil/osascripts/progress.js new file mode 100644 index 0000000..d6509ec --- /dev/null +++ b/internal/zenutil/osascripts/progress.js @@ -0,0 +1,37 @@ +var app = Application.currentApplication() +app.includeStandardAdditions = true +app.activate() + +ObjC.import('stdlib') +ObjC.import('readline') + +function run(args) { + Progress.totalUnitCount = 100 + Progress.completedUnitCount = 0 + Progress.description = args[0] || "Progress" + Progress.additionalDescription = args[1] || "Running..." + + while (true) { + var s + try { + s = $.readline('') + } + catch (e) { + if (e.errorNumber === -128) $.exit(1) + break + } + + if (s.indexOf('#') === 0) { + Progress.additionalDescription = s.slice(1).trim() + continue + } + + var i = parseInt(s) + if (Number.isSafeInteger(i)) { + Progress.completedUnitCount = i + continue + } + } + + Progress.completedUnitCount = 100 +} From c74a75a4ca0f953b9c4c3b3b3e2e9da03f598c15 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Thu, 22 Apr 2021 15:03:08 +0100 Subject: [PATCH 02/12] WIP: progress (macOS). --- internal/zenutil/osa_generated.go | 20 ++++ internal/zenutil/osa_generator.go | 35 ++++--- internal/zenutil/osascripts/progress.js | 3 +- internal/zenutil/run_darwin.go | 121 ++++++++++++++++++++++++ progress.go | 7 ++ 5 files changed, 171 insertions(+), 15 deletions(-) create mode 100644 progress.go diff --git a/internal/zenutil/osa_generated.go b/internal/zenutil/osa_generated.go index 4396954..367e8bc 100644 --- a/internal/zenutil/osa_generated.go +++ b/internal/zenutil/osa_generated.go @@ -49,3 +49,23 @@ var app=Application.currentApplication() app.includeStandardAdditions=true void app.displayNotification({{json .Text}},{{json .Options}}) {{- end}}`)) + +var progress =` +var app=Application.currentApplication() +app.includeStandardAdditions=true +app.activate() +ObjC.import('stdlib') +ObjC.import('readline') +function run(args){Progress.totalUnitCount=100 +Progress.completedUnitCount=0 +Progress.description=args[0]||"Progress" +Progress.additionalDescription=args[1]||"Running..." +while(true){var s +try{s=$.readline('')}catch(e){if(e.errorNumber===-128)$.exit(1) +break} +if(s.indexOf('#')===0){Progress.additionalDescription=s.slice(1).trim() +continue} +var i=parseInt(s) +if(Number.isSafeInteger(i)){Progress.completedUnitCount=i +continue}} +Progress.completedUnitCount=100}` diff --git a/internal/zenutil/osa_generator.go b/internal/zenutil/osa_generator.go index 0270174..79ebd53 100644 --- a/internal/zenutil/osa_generator.go +++ b/internal/zenutil/osa_generator.go @@ -4,7 +4,6 @@ package main import ( "bytes" - "io/ioutil" "log" "os" "path/filepath" @@ -17,21 +16,21 @@ import ( func main() { dir := os.Args[1] - files, err := ioutil.ReadDir(dir) + files, err := os.ReadDir(dir) if err != nil { log.Fatal(err) } + var args struct { + Templates string + Progress string + } + var str strings.Builder for _, file := range files { name := file.Name() - - str.WriteString("\n" + `{{define "`) - str.WriteString(strings.TrimSuffix(name, filepath.Ext(name))) - str.WriteString(`" -}}` + "\n") - - data, err := ioutil.ReadFile(filepath.Join(dir, name)) + data, err := os.ReadFile(filepath.Join(dir, name)) if err != nil { log.Fatal(err) } @@ -40,8 +39,16 @@ func main() { log.Fatal(err) } - str.Write(data) - str.WriteString("\n{{- end}}") + 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}}") + } } out, err := os.Create("osa_generated.go") @@ -49,7 +56,8 @@ func main() { log.Fatal(err) } - err = generator.Execute(out, str.String()) + args.Templates = str.String() + err = generator.Execute(out, args) if err != nil { log.Fatal(err) } @@ -108,5 +116,6 @@ 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(` + "`{{.}}`" + `)) -`)) +}}).Parse(` + "`{{.Templates}}`" + `)) + +var progress =` + "`\n{{.Progress}}`\n")) diff --git a/internal/zenutil/osascripts/progress.js b/internal/zenutil/osascripts/progress.js index d6509ec..08b8a8e 100644 --- a/internal/zenutil/osascripts/progress.js +++ b/internal/zenutil/osascripts/progress.js @@ -15,8 +15,7 @@ function run(args) { var s try { s = $.readline('') - } - catch (e) { + } catch (e) { if (e.errorNumber === -128) $.exit(1) break } diff --git a/internal/zenutil/run_darwin.go b/internal/zenutil/run_darwin.go index 80c862e..0104c18 100644 --- a/internal/zenutil/run_darwin.go +++ b/internal/zenutil/run_darwin.go @@ -1,12 +1,16 @@ package zenutil import ( + "bytes" "context" "io/ioutil" "os" "os/exec" + "path/filepath" + "strconv" "strings" "syscall" + "time" ) // Run is internal. @@ -47,6 +51,123 @@ func Run(ctx context.Context, script string, data interface{}) ([]byte, error) { return cmd.Output() } +func RunProgress(ctx context.Context) (m *progressMonitor, err error) { + t, err := ioutil.TempDir("", "") + if err != nil { + return nil, err + } + defer func() { + if err != nil { + os.RemoveAll(t) + } + }() + + var cmd *exec.Cmd + name := filepath.Join(t, "progress.app") + + cmd = exec.Command("osacompile", "-l", "JavaScript", "-o", name) + cmd.Stdin = strings.NewReader(progress) + if err := cmd.Run(); err != nil { + return nil, err + } + + plist := filepath.Join(name, "Contents/Info.plist") + + cmd = exec.Command("defaults", "write", plist, "LSUIElement", "true") + if err := cmd.Run(); err != nil { + return nil, err + } + + cmd = exec.Command("defaults", "write", plist, "CFBundleName", "") + if err := cmd.Run(); err != nil { + return nil, err + } + + var executable string + cmd = exec.Command("defaults", "read", plist, "CFBundleExecutable") + if out, err := cmd.Output(); err != nil { + return nil, err + } else { + out = bytes.TrimSuffix(out, []byte{'\n'}) + executable = filepath.Join(name, "Contents/MacOS", string(out)) + } + + cmd = exec.Command(executable) + pipe, err := cmd.StdinPipe() + if err != nil { + return nil, err + } + if err = cmd.Start(); err != nil { + return nil, err + } + if ctx == nil { + ctx = context.Background() + } + + m = &progressMonitor{ + done: make(chan struct{}), + lines: make(chan string), + } + go func() { + defer func() { + pipe.Close() + err := cmd.Wait() + if cerr := ctx.Err(); cerr != nil { + err = cerr + } + m.err = err + close(m.done) + os.RemoveAll(t) + }() + for { + var line string + select { + case s, ok := <-m.lines: + if !ok { + return + } + line = s + case <-ctx.Done(): + return + case <-time.After(40 * time.Millisecond): + } + if _, err := pipe.Write([]byte(line + "\n")); err != nil { + return + } + } + }() + return +} + +type progressMonitor struct { + err error + done chan struct{} + lines chan string +} + +func (m *progressMonitor) send(line string) error { + select { + case m.lines <- line: + return nil + case <-m.done: + return m.err + } +} + +func (m *progressMonitor) Close() error { + close(m.lines) + <-m.done + return m.err +} + +func (m *progressMonitor) Message(msg string) error { + return m.send("#" + msg) +} + +func (m *progressMonitor) Progress(progress int) error { + return m.send(strconv.Itoa(progress)) +} + // File is internal. type File struct { Operation string diff --git a/progress.go b/progress.go new file mode 100644 index 0000000..dbbffb0 --- /dev/null +++ b/progress.go @@ -0,0 +1,7 @@ +package zenity + +type ProgressMonitor interface { + Message(string) error + Progress(int) error + Close() error +} From c49aa7990b99293e2b743542aa3b311f6152608e Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Fri, 23 Apr 2021 14:13:13 +0100 Subject: [PATCH 03/12] Documentation. --- file.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/file.go b/file.go index ae98daf..ab52ada 100644 --- a/file.go +++ b/file.go @@ -69,6 +69,9 @@ func Filename(filename string) Option { // // macOS hides filename filters from the user, // and only supports filtering by extension (or "type"). +// +// Patterns may use the GTK syntax on all platforms: +// https://developer.gnome.org/pygtk/stable/class-gtkfilefilter.html#method-gtkfilefilter--add-pattern type FileFilter struct { Name string // display string that describes the filter (optional) Patterns []string // filter patterns for the display string From 374ba8a90a2691d35d2d78340b92b5ca194b549e Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Sun, 25 Apr 2021 18:34:56 +0100 Subject: [PATCH 04/12] WIP: progress (macOS). --- internal/zenutil/osa_generated.go | 13 +++---- internal/zenutil/osascripts/progress.js | 46 +++++++++++-------------- internal/zenutil/run_darwin.go | 19 +++++----- progress.go | 35 +++++++++++++++++-- progress_darwin.go | 21 +++++++++++ zenity.go | 5 +++ 6 files changed, 93 insertions(+), 46 deletions(-) create mode 100644 progress_darwin.go diff --git a/internal/zenutil/osa_generated.go b/internal/zenutil/osa_generated.go index 367e8bc..ea669c5 100644 --- a/internal/zenutil/osa_generated.go +++ b/internal/zenutil/osa_generated.go @@ -56,16 +56,13 @@ app.includeStandardAdditions=true app.activate() ObjC.import('stdlib') ObjC.import('readline') -function run(args){Progress.totalUnitCount=100 -Progress.completedUnitCount=0 -Progress.description=args[0]||"Progress" -Progress.additionalDescription=args[1]||"Running..." +try{Progress.totalUnitCount=$.getenv('total')}catch{} +try{Progress.description=$.getenv('description')}catch{} while(true){var s try{s=$.readline('')}catch(e){if(e.errorNumber===-128)$.exit(1) break} -if(s.indexOf('#')===0){Progress.additionalDescription=s.slice(1).trim() +if(s.indexOf('#')===0){Progress.additionalDescription=s.slice(1) continue} var i=parseInt(s) -if(Number.isSafeInteger(i)){Progress.completedUnitCount=i -continue}} -Progress.completedUnitCount=100}` +if(i>=0&&Progress.totalUnitCount>0){Progress.completedUnitCount=i +continue}}` diff --git a/internal/zenutil/osascripts/progress.js b/internal/zenutil/osascripts/progress.js index 08b8a8e..424213e 100644 --- a/internal/zenutil/osascripts/progress.js +++ b/internal/zenutil/osascripts/progress.js @@ -5,32 +5,26 @@ app.activate() ObjC.import('stdlib') ObjC.import('readline') -function run(args) { - Progress.totalUnitCount = 100 - Progress.completedUnitCount = 0 - Progress.description = args[0] || "Progress" - Progress.additionalDescription = args[1] || "Running..." +try { Progress.totalUnitCount = $.getenv('total') } catch { } +try { Progress.description = $.getenv('description') } catch { } - while (true) { - var s - try { - s = $.readline('') - } catch (e) { - if (e.errorNumber === -128) $.exit(1) - break - } - - if (s.indexOf('#') === 0) { - Progress.additionalDescription = s.slice(1).trim() - continue - } - - var i = parseInt(s) - if (Number.isSafeInteger(i)) { - Progress.completedUnitCount = i - continue - } +while (true) { + var s + try { + s = $.readline('') + } catch (e) { + if (e.errorNumber === -128) $.exit(1) + break } - Progress.completedUnitCount = 100 -} + if (s.indexOf('#') === 0) { + Progress.additionalDescription = s.slice(1) + continue + } + + var i = parseInt(s) + if (i >= 0 && Progress.totalUnitCount > 0) { + Progress.completedUnitCount = i + continue + } +} \ No newline at end of file diff --git a/internal/zenutil/run_darwin.go b/internal/zenutil/run_darwin.go index 0104c18..78decbf 100644 --- a/internal/zenutil/run_darwin.go +++ b/internal/zenutil/run_darwin.go @@ -51,7 +51,7 @@ func Run(ctx context.Context, script string, data interface{}) ([]byte, error) { return cmd.Output() } -func RunProgress(ctx context.Context) (m *progressMonitor, err error) { +func RunProgress(ctx context.Context, env []string) (m *progressDialog, err error) { t, err := ioutil.TempDir("", "") if err != nil { return nil, err @@ -93,6 +93,7 @@ func RunProgress(ctx context.Context) (m *progressMonitor, err error) { } cmd = exec.Command(executable) + cmd.Env = env pipe, err := cmd.StdinPipe() if err != nil { return nil, err @@ -104,7 +105,7 @@ func RunProgress(ctx context.Context) (m *progressMonitor, err error) { ctx = context.Background() } - m = &progressMonitor{ + m = &progressDialog{ done: make(chan struct{}), lines: make(chan string), } @@ -139,13 +140,13 @@ func RunProgress(ctx context.Context) (m *progressMonitor, err error) { return } -type progressMonitor struct { +type progressDialog struct { err error done chan struct{} lines chan string } -func (m *progressMonitor) send(line string) error { +func (m *progressDialog) send(line string) error { select { case m.lines <- line: return nil @@ -154,18 +155,18 @@ func (m *progressMonitor) send(line string) error { } } -func (m *progressMonitor) Close() error { +func (m *progressDialog) Close() error { close(m.lines) <-m.done return m.err } -func (m *progressMonitor) Message(msg string) error { - return m.send("#" + msg) +func (m *progressDialog) Text(text string) error { + return m.send("#" + text) } -func (m *progressMonitor) Progress(progress int) error { - return m.send(strconv.Itoa(progress)) +func (m *progressDialog) Value(value int) error { + return m.send(strconv.Itoa(value)) } // File is internal. diff --git a/progress.go b/progress.go index dbbffb0..45fa4e5 100644 --- a/progress.go +++ b/progress.go @@ -1,7 +1,36 @@ package zenity -type ProgressMonitor interface { - Message(string) error - Progress(int) error +// Progress displays the progress indication dialog. +// +// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton, +// Icon, MaxValue, Pulsate, NoCancel, TimeRemaining. +func Progress(options ...Option) (ProgressDialog, error) { + return progress(applyOptions(options)) +} + +type ProgressDialog interface { + Text(string) error + Value(int) error Close() error } + +// MaxValue returns an Option to set the maximum value (macOS only). +// The default value is 100. +func MaxValue(value int) Option { + return funcOption(func(o *options) { o.maxValue = value }) +} + +// Pulsate returns an Option to pulsate the progress bar. +func Pulsate() Option { + return funcOption(func(o *options) { o.maxValue = -1 }) +} + +// NoCancel returns an Option to hide the Cancel button (Unix only). +func NoCancel() Option { + return funcOption(func(o *options) { o.noCancel = true }) +} + +// TimeRemaining returns an Option to estimate when progress will reach 100% (Unix only). +func TimeRemaining() Option { + return funcOption(func(o *options) { o.timeRemaining = true }) +} diff --git a/progress_darwin.go b/progress_darwin.go new file mode 100644 index 0000000..044176e --- /dev/null +++ b/progress_darwin.go @@ -0,0 +1,21 @@ +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.maxValue == 0 { + opts.maxValue = 100 + } + if opts.maxValue >= 0 { + env = append(env, "total="+strconv.Itoa(opts.maxValue)) + } + return zenutil.RunProgress(opts.ctx, env) +} diff --git a/zenity.go b/zenity.go index 0b872e9..b125364 100644 --- a/zenity.go +++ b/zenity.go @@ -57,6 +57,11 @@ type options struct { color color.Color showPalette bool + // Progress indication options + maxValue int + noCancel bool + timeRemaining bool + // Context for timeout ctx context.Context } From 10c3d63ca52efce7b5c9dd92c3a464d1628f045f Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Mon, 26 Apr 2021 00:36:15 +0100 Subject: [PATCH 05/12] WIP: progress (unix). --- color_windows.go | 2 +- internal/zenutil/osa_generated.go | 2 +- internal/zenutil/osa_generator.go | 2 +- internal/zenutil/run_darwin.go | 124 ++++++++++++------------------ internal/zenutil/run_progress.go | 50 ++++++++++++ internal/zenutil/run_unix.go | 63 +++++++++++++++ progress.go | 14 +++- progress_darwin.go | 2 +- progress_unix.go | 28 +++++++ util_darwin.go | 4 +- 10 files changed, 209 insertions(+), 82 deletions(-) create mode 100644 internal/zenutil/run_progress.go create mode 100644 progress_unix.go diff --git a/color_windows.go b/color_windows.go index b422929..ce9dcd3 100644 --- a/color_windows.go +++ b/color_windows.go @@ -32,7 +32,7 @@ func selectColor(opts options) (color.Color, error) { if opts.color != nil { args.Flags |= 0x1 // CC_RGBINIT n := color.NRGBAModel.Convert(opts.color).(color.NRGBA) - args.RgbResult = uint32(n.R) | (uint32(n.G) << 8) | (uint32(n.B) << 16) + args.RgbResult = uint32(n.R) | uint32(n.G)<<8 | uint32(n.B)<<16 } if opts.showPalette { args.Flags |= 0x4 // CC_PREVENTFULLOPEN diff --git a/internal/zenutil/osa_generated.go b/internal/zenutil/osa_generated.go index ea669c5..0b0d788 100644 --- a/internal/zenutil/osa_generated.go +++ b/internal/zenutil/osa_generated.go @@ -50,7 +50,7 @@ app.includeStandardAdditions=true void app.displayNotification({{json .Text}},{{json .Options}}) {{- end}}`)) -var progress =` +var progress = ` var app=Application.currentApplication() app.includeStandardAdditions=true app.activate() diff --git a/internal/zenutil/osa_generator.go b/internal/zenutil/osa_generator.go index 79ebd53..a6febc7 100644 --- a/internal/zenutil/osa_generator.go +++ b/internal/zenutil/osa_generator.go @@ -118,4 +118,4 @@ var scripts = template.Must(template.New("").Funcs(template.FuncMap{"json": func return string(b), err }}).Parse(` + "`{{.Templates}}`" + `)) -var progress =` + "`\n{{.Progress}}`\n")) +var progress = ` + "`\n{{.Progress}}`\n")) diff --git a/internal/zenutil/run_darwin.go b/internal/zenutil/run_darwin.go index 78decbf..dead84e 100644 --- a/internal/zenutil/run_darwin.go +++ b/internal/zenutil/run_darwin.go @@ -7,7 +7,6 @@ import ( "os" "os/exec" "path/filepath" - "strconv" "strings" "syscall" "time" @@ -51,7 +50,8 @@ func Run(ctx context.Context, script string, data interface{}) ([]byte, error) { return cmd.Output() } -func RunProgress(ctx context.Context, env []string) (m *progressDialog, err error) { +// RunProgress is internal. +func RunProgress(ctx context.Context, max int, env []string) (m *progressDialog, err error) { t, err := ioutil.TempDir("", "") if err != nil { return nil, err @@ -108,18 +108,19 @@ func RunProgress(ctx context.Context, env []string) (m *progressDialog, err erro m = &progressDialog{ done: make(chan struct{}), lines: make(chan string), + max: max, } go func() { - defer func() { - pipe.Close() - err := cmd.Wait() - if cerr := ctx.Err(); cerr != nil { - err = cerr - } - m.err = err - close(m.done) - os.RemoveAll(t) - }() + err := cmd.Wait() + if cerr := ctx.Err(); cerr != nil { + err = cerr + } + m.err = err + close(m.done) + os.RemoveAll(t) + }() + go func() { + defer pipe.Close() for { var line string select { @@ -140,52 +141,6 @@ func RunProgress(ctx context.Context, env []string) (m *progressDialog, err erro return } -type progressDialog struct { - err error - done chan struct{} - lines chan string -} - -func (m *progressDialog) send(line string) error { - select { - case m.lines <- line: - return nil - case <-m.done: - return m.err - } -} - -func (m *progressDialog) Close() error { - close(m.lines) - <-m.done - return m.err -} - -func (m *progressDialog) Text(text string) error { - return m.send("#" + text) -} - -func (m *progressDialog) Value(value int) error { - return m.send(strconv.Itoa(value)) -} - -// File is internal. -type File struct { - Operation string - Separator string - Options FileOptions -} - -// FileOptions is internal. -type FileOptions struct { - Prompt *string `json:"withPrompt,omitempty"` - Type []string `json:"ofType,omitempty"` - Name string `json:"defaultName,omitempty"` - Location string `json:"defaultLocation,omitempty"` - Multiple bool `json:"multipleSelectionsAllowed,omitempty"` - Invisibles bool `json:"invisibles,omitempty"` -} - // Dialog is internal. type Dialog struct { Operation string @@ -208,6 +163,25 @@ type DialogOptions struct { Timeout int `json:"givingUpAfter,omitempty"` } +// DialogButtons is internal. +type DialogButtons struct { + Buttons []string + Default int + Cancel int + Extra int +} + +// SetButtons is internal. +func (d *Dialog) SetButtons(btns DialogButtons) { + d.Options.Buttons = btns.Buttons + d.Options.Default = btns.Default + d.Options.Cancel = btns.Cancel + if btns.Extra > 0 { + name := btns.Buttons[btns.Extra-1] + d.Extra = &name + } +} + // List is internal. type List struct { Items []string @@ -226,6 +200,23 @@ type ListOptions struct { Empty bool `json:"emptySelectionAllowed,omitempty"` } +// File is internal. +type File struct { + Operation string + Separator string + Options FileOptions +} + +// FileOptions is internal. +type FileOptions struct { + Prompt *string `json:"withPrompt,omitempty"` + Type []string `json:"ofType,omitempty"` + Name string `json:"defaultName,omitempty"` + Location string `json:"defaultLocation,omitempty"` + Multiple bool `json:"multipleSelectionsAllowed,omitempty"` + Invisibles bool `json:"invisibles,omitempty"` +} + // Notify is internal. type Notify struct { Text string @@ -237,20 +228,3 @@ type NotifyOptions struct { Title *string `json:"withTitle,omitempty"` Subtitle string `json:"subtitle,omitempty"` } - -type Buttons struct { - Buttons []string - Default int - Cancel int - Extra int -} - -func (d *Dialog) SetButtons(btns Buttons) { - d.Options.Buttons = btns.Buttons - d.Options.Default = btns.Default - d.Options.Cancel = btns.Cancel - if btns.Extra > 0 { - name := btns.Buttons[btns.Extra-1] - d.Extra = &name - } -} diff --git a/internal/zenutil/run_progress.go b/internal/zenutil/run_progress.go new file mode 100644 index 0000000..e8e793a --- /dev/null +++ b/internal/zenutil/run_progress.go @@ -0,0 +1,50 @@ +// +build !windows,!js + +package zenutil + +import ( + "strconv" +) + +type progressDialog struct { + err error + done chan struct{} + lines chan string + percent bool + max int +} + +func (m *progressDialog) send(line string) error { + select { + case m.lines <- line: + return nil + case <-m.done: + return m.err + } +} + +func (m *progressDialog) Close() error { + close(m.lines) + <-m.done + return m.err +} + +func (m *progressDialog) Text(text string) error { + return m.send("#" + text) +} + +func (m *progressDialog) Value(value int) error { + if m.percent { + return m.send(strconv.FormatFloat(100*float64(value)/float64(m.max), 'f', -1, 64)) + } else { + return m.send(strconv.Itoa(value)) + } +} + +func (m *progressDialog) MaxValue() int { + return m.max +} + +func (m *progressDialog) Done() <-chan struct{} { + return m.done +} diff --git a/internal/zenutil/run_unix.go b/internal/zenutil/run_unix.go index a648e94..8d92acb 100644 --- a/internal/zenutil/run_unix.go +++ b/internal/zenutil/run_unix.go @@ -40,3 +40,66 @@ func Run(ctx context.Context, args []string) ([]byte, error) { } return exec.Command(tool, args...).Output() } + +// RunProgress is internal. +func RunProgress(ctx context.Context, max int, args []string) (m *progressDialog, err error) { + if Command && path != "" { + if Timeout > 0 { + args = append(args, "--timeout", strconv.Itoa(Timeout)) + } + syscall.Exec(path, append([]string{tool}, args...), os.Environ()) + } + + cmd := exec.Command(tool, args...) + pipe, err := cmd.StdinPipe() + if err != nil { + return nil, err + } + if err = cmd.Start(); err != nil { + return nil, err + } + if ctx == nil { + ctx = context.Background() + } + + m = &progressDialog{ + done: make(chan struct{}), + lines: make(chan string), + percent: true, + max: max, + } + go func() { + err := cmd.Wait() + select { + case _, ok := <-m.lines: + if !ok { + err = nil + } + default: + } + if cerr := ctx.Err(); cerr != nil { + err = cerr + } + m.err = err + close(m.done) + }() + go func() { + defer cmd.Process.Signal(syscall.SIGTERM) + for { + var line string + select { + case s, ok := <-m.lines: + if !ok { + return + } + line = s + case <-ctx.Done(): + return + } + if _, err := pipe.Write([]byte(line + "\n")); err != nil { + return + } + } + }() + return +} diff --git a/progress.go b/progress.go index 45fa4e5..5f5dffe 100644 --- a/progress.go +++ b/progress.go @@ -8,14 +8,26 @@ func Progress(options ...Option) (ProgressDialog, error) { return progress(applyOptions(options)) } +// ProgressDialog allows you to interact with the progress indication dialog. type ProgressDialog interface { + // Text sets the dialog text. Text(string) error + + // Value sets how much of the task has been completed. Value(int) error + + // MaxValue gets how much work the task requires in total. + MaxValue() int + + // Close closes the dialog. Close() error + + // Done returns a channel that's closed when the dialog is closed. + Done() <-chan struct{} } // MaxValue returns an Option to set the maximum value (macOS only). -// The default value is 100. +// The default maximum value is 100. func MaxValue(value int) Option { return funcOption(func(o *options) { o.maxValue = value }) } diff --git a/progress_darwin.go b/progress_darwin.go index 044176e..90aabb7 100644 --- a/progress_darwin.go +++ b/progress_darwin.go @@ -17,5 +17,5 @@ func progress(opts options) (ProgressDialog, error) { if opts.maxValue >= 0 { env = append(env, "total="+strconv.Itoa(opts.maxValue)) } - return zenutil.RunProgress(opts.ctx, env) + return zenutil.RunProgress(opts.ctx, opts.maxValue, env) } diff --git a/progress_unix.go b/progress_unix.go new file mode 100644 index 0000000..5c3dd1c --- /dev/null +++ b/progress_unix.go @@ -0,0 +1,28 @@ +// +build !windows,!darwin,!js + +package zenity + +import ( + "github.com/ncruces/zenity/internal/zenutil" +) + +func progress(opts options) (ProgressDialog, error) { + args := []string{"--progress"} + args = appendTitle(args, opts) + args = appendButtons(args, opts) + args = appendWidthHeight(args, opts) + args = appendIcon(args, opts) + if opts.maxValue == 0 { + opts.maxValue = 100 + } + if opts.maxValue < 0 { + args = append(args, "--pulsate") + } + if opts.noCancel { + args = append(args, "--no-cancel") + } + if opts.timeRemaining { + args = append(args, "--time-remaining") + } + return zenutil.RunProgress(opts.ctx, opts.maxValue, args) +} diff --git a/util_darwin.go b/util_darwin.go index 8a79755..b05c1c6 100644 --- a/util_darwin.go +++ b/util_darwin.go @@ -2,13 +2,13 @@ package zenity import "github.com/ncruces/zenity/internal/zenutil" -func getButtons(dialog, okcancel bool, opts options) (btns zenutil.Buttons) { +func getButtons(dialog, okcancel bool, opts options) (btns zenutil.DialogButtons) { if !okcancel { opts.cancelLabel = nil opts.defaultCancel = false } - if opts.okLabel != nil || opts.cancelLabel != nil || opts.extraButton != nil || (dialog != okcancel) { + if opts.okLabel != nil || opts.cancelLabel != nil || opts.extraButton != nil || dialog != okcancel { if opts.okLabel == nil { opts.okLabel = stringPtr("OK") } From de7c119f33bf0cb57471b2c30fb44f8884b690be Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Tue, 27 Apr 2021 14:05:04 +0100 Subject: [PATCH 06/12] WIP: progress (windows). --- entry_windows.go | 22 +++--- list_windows.go | 38 ++++------- progress_windows.go | 160 ++++++++++++++++++++++++++++++++++++++++++++ util_windows.go | 32 +++++++++ 4 files changed, 215 insertions(+), 37 deletions(-) create mode 100644 progress_windows.go diff --git a/entry_windows.go b/entry_windows.go index c128420..dcda663 100644 --- a/entry_windows.go +++ b/entry_windows.go @@ -5,9 +5,8 @@ import ( ) func entry(text string, opts options) (out string, ok bool, err error) { - var title string - if opts.title != nil { - title = *opts.title + if opts.title == nil { + opts.title = stringPtr("") } if opts.okLabel == nil { opts.okLabel = stringPtr("OK") @@ -15,10 +14,7 @@ func entry(text string, opts options) (out string, ok bool, err error) { if opts.cancelLabel == nil { opts.cancelLabel = stringPtr("Cancel") } - return entryDlg(title, text, opts) -} -func entryDlg(title, text string, opts options) (out string, ok bool, err error) { defer setup()() font := getFont() defer font.Delete() @@ -95,16 +91,16 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error) defer unregisterClass.Call(cls, instance) wnd, _, _ = createWindowEx.Call(0x10101, // WS_EX_CONTROLPARENT|WS_EX_WINDOWEDGE|WS_EX_DLGMODALFRAME - cls, strptr(title), + cls, strptr(*opts.title), 0x84c80000, // WS_POPUPWINDOW|WS_CLIPSIBLINGS|WS_DLGFRAME 0x80000000, // CW_USEDEFAULT 0x80000000, // CW_USEDEFAULT - 281, 141, 0, 0, instance) + 281, 141, 0, 0, instance, 0) textCtl, _, _ = createWindowEx.Call(0, strptr("STATIC"), strptr(text), 0x5002e080, // WS_CHILD|WS_VISIBLE|WS_GROUP|SS_WORDELLIPSIS|SS_EDITCONTROL|SS_NOPREFIX - 12, 10, 241, 16, wnd, 0, instance) + 12, 10, 241, 16, wnd, 0, instance, 0) var flags uintptr = 0x50030080 // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP|ES_AUTOHSCROLL if opts.hideText { @@ -113,21 +109,21 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error) editCtl, _, _ = createWindowEx.Call(0x200, // WS_EX_CLIENTEDGE strptr("EDIT"), strptr(opts.entryText), flags, - 12, 30, 241, 24, wnd, 0, instance) + 12, 30, 241, 24, wnd, 0, instance, 0) okBtn, _, _ = createWindowEx.Call(0, strptr("BUTTON"), strptr(*opts.okLabel), 0x50030001, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP|BS_DEFPUSHBUTTON - 12, 66, 75, 24, wnd, 1 /* IDOK */, instance) + 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) + 12, 66, 75, 24, wnd, 2 /* IDCANCEL */, instance, 0) if opts.extraButton != nil { 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) + 12, 66, 75, 24, wnd, 7 /* IDNO */, instance, 0) } layout(getDPI(wnd)) diff --git a/list_windows.go b/list_windows.go index 16a7201..67aecbe 100644 --- a/list_windows.go +++ b/list_windows.go @@ -6,17 +6,7 @@ import ( ) func list(text string, items []string, opts options) (string, bool, error) { - var title string - if opts.title != nil { - title = *opts.title - } - if opts.okLabel == nil { - opts.okLabel = stringPtr("OK") - } - if opts.cancelLabel == nil { - opts.cancelLabel = stringPtr("Cancel") - } - items, err := listDlg(title, text, items, false, opts) + items, err := listDlg(text, items, false, opts) if len(items) == 1 { return items[0], true, err } @@ -24,9 +14,12 @@ func list(text string, items []string, opts options) (string, bool, error) { } func listMultiple(text string, items []string, opts options) ([]string, error) { - var title string - if opts.title != nil { - title = *opts.title + return listDlg(text, items, true, opts) +} + +func listDlg(text string, items []string, multiple bool, opts options) (out []string, err error) { + if opts.title == nil { + opts.title = stringPtr("") } if opts.okLabel == nil { opts.okLabel = stringPtr("OK") @@ -34,10 +27,7 @@ func listMultiple(text string, items []string, opts options) ([]string, error) { if opts.cancelLabel == nil { opts.cancelLabel = stringPtr("Cancel") } - return listDlg(title, text, items, true, opts) -} -func listDlg(title, text string, items []string, multiple bool, opts options) (out []string, err error) { defer setup()() font := getFont() defer font.Delete() @@ -130,16 +120,16 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o defer unregisterClass.Call(cls, instance) wnd, _, _ = createWindowEx.Call(0x10101, // WS_EX_CONTROLPARENT|WS_EX_WINDOWEDGE|WS_EX_DLGMODALFRAME - cls, strptr(title), + cls, strptr(*opts.title), 0x84c80000, // WS_POPUPWINDOW|WS_CLIPSIBLINGS|WS_DLGFRAME 0x80000000, // CW_USEDEFAULT 0x80000000, // CW_USEDEFAULT - 281, 281, 0, 0, instance) + 281, 281, 0, 0, instance, 0) textCtl, _, _ = createWindowEx.Call(0, strptr("STATIC"), strptr(text), 0x5002e080, // WS_CHILD|WS_VISIBLE|WS_GROUP|SS_WORDELLIPSIS|SS_EDITCONTROL|SS_NOPREFIX - 12, 10, 241, 16, wnd, 0, instance) + 12, 10, 241, 16, wnd, 0, instance, 0) var flags uintptr = 0x50320000 // WS_CHILD|WS_VISIBLE|WS_VSCROLL|WS_GROUP|WS_TABSTOP if multiple { @@ -148,21 +138,21 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o listCtl, _, _ = createWindowEx.Call(0x200, // WS_EX_CLIENTEDGE strptr("LISTBOX"), strptr(opts.entryText), flags, - 12, 30, 241, 164, wnd, 0, instance) + 12, 30, 241, 164, wnd, 0, instance, 0) okBtn, _, _ = createWindowEx.Call(0, strptr("BUTTON"), strptr(*opts.okLabel), 0x50030001, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP|BS_DEFPUSHBUTTON - 12, 206, 75, 24, wnd, 1 /* IDOK */, instance) + 12, 206, 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, 206, 75, 24, wnd, 2 /* IDCANCEL */, instance) + 12, 206, 75, 24, wnd, 2 /* IDCANCEL */, instance, 0) if opts.extraButton != nil { extraBtn, _, _ = createWindowEx.Call(0, strptr("BUTTON"), strptr(*opts.extraButton), 0x50010000, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP - 12, 206, 75, 24, wnd, 7 /* IDNO */, instance) + 12, 206, 75, 24, wnd, 7 /* IDNO */, instance, 0) } for _, item := range items { diff --git a/progress_windows.go b/progress_windows.go new file mode 100644 index 0000000..30213f6 --- /dev/null +++ b/progress_windows.go @@ -0,0 +1,160 @@ +package zenity + +import ( + "syscall" +) + +func progress(opts options) (ProgressDialog, error) { + if opts.title == nil { + opts.title = stringPtr("") + } + if opts.okLabel == nil { + opts.okLabel = stringPtr("OK") + } + if opts.cancelLabel == nil { + opts.cancelLabel = stringPtr("Cancel") + } + if opts.maxValue == 0 { + opts.maxValue = 100 + } + + defer setup()() + font := getFont() + 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 + } 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 + } + } + + proc := func(wnd uintptr, msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + case 0x0002: // WM_DESTROY + postQuitMessage.Call(0) + + case 0x0010: // WM_CLOSE + destroyWindow.Call(wnd) + + case 0x0111: // WM_COMMAND + switch wparam { + default: + return 1 + case 1, 6: // IDOK, IDYES + case 2: // IDCANCEL + case 7: // IDNO + } + destroyWindow.Call(wnd) + + case 0x02e0: // WM_DPICHANGED + layout(dpi(uint32(wparam) >> 16)) + + default: + ret, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0) + return ret + } + + return 0 + } + + if opts.ctx != nil && opts.ctx.Err() != nil { + return nil, opts.ctx.Err() + } + + instance, _, err := getModuleHandle.Call(0) + if instance == 0 { + return nil, err + } + + cls, err := registerClass(instance, syscall.NewCallback(proc)) + if cls == 0 { + return nil, err + } + defer unregisterClass.Call(cls, instance) + + 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, + strptr("STATIC"), 0, + 0x5002e080, // WS_CHILD|WS_VISIBLE|WS_GROUP|SS_WORDELLIPSIS|SS_EDITCONTROL|SS_NOPREFIX + 12, 10, 241, 16, 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, + strptr("msctls_progress32"), // PROGRESS_CLASS + 0, flags, + 12, 30, 241, 24, wnd, 0, instance, 0) + + okBtn, _, _ = createWindowEx.Call(0, + strptr("BUTTON"), strptr(*opts.okLabel), + 0x50030001, // WS_CHILD|WS_VISIBLE|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) + if opts.extraButton != nil { + 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) + } + + layout(getDPI(wnd)) + centerWindow(wnd) + showWindow.Call(wnd, 1 /* SW_SHOWNORMAL */, 0) + if opts.maxValue < 0 { + sendMessage.Call(progCtl, 0x410 /* PBM_SETMARQUEE */, 1, 0) + } else { + sendMessage.Call(progCtl, 0x402 /* PBM_SETPOS */, 33, 0) + sendMessage.Call(progCtl, 0x406 /* PBM_SETRANGE32 */, 0, uintptr(opts.maxValue)) + } + + if opts.ctx != nil { + wait := make(chan struct{}) + defer close(wait) + go func() { + select { + case <-opts.ctx.Done(): + sendMessage.Call(wnd, 0x0112 /* WM_SYSCOMMAND */, 0xf060 /* SC_CLOSE */, 0) + case <-wait: + } + }() + } + + // set default values + // out, ok, err = "", false, nil + + if err := messageLoop(wnd); err != nil { + return nil, err + } + if opts.ctx != nil && opts.ctx.Err() != nil { + return nil, opts.ctx.Err() + } + return nil, err +} diff --git a/util_windows.go b/util_windows.go index c199bd7..076f164 100644 --- a/util_windows.go +++ b/util_windows.go @@ -13,6 +13,7 @@ import ( ) var ( + comctl32 = syscall.NewLazyDLL("comctl32.dll") comdlg32 = syscall.NewLazyDLL("comdlg32.dll") gdi32 = syscall.NewLazyDLL("gdi32.dll") kernel32 = syscall.NewLazyDLL("kernel32.dll") @@ -21,6 +22,7 @@ var ( user32 = syscall.NewLazyDLL("user32.dll") wtsapi32 = syscall.NewLazyDLL("wtsapi32.dll") + initCommonControlsEx = comctl32.NewProc("InitCommonControlsEx") commDlgExtendedError = comdlg32.NewProc("CommDlgExtendedError") deleteObject = gdi32.NewProc("DeleteObject") @@ -111,6 +113,10 @@ func setup() context.CancelFunc { } } + var icc _INITCOMMONCONTROLSEX + icc.Size = uint32(unsafe.Sizeof(icc)) + icc.ICC = 0x00004020 // ICC_STANDARD_CLASSES|ICC_PROGRESS_CLASS + return func() { if old != 0 { setThreadDpiAwarenessContext.Call(old) @@ -284,6 +290,26 @@ func registerClass(instance, proc uintptr) (uintptr, error) { return ret, err } +// https://stackoverflow.com/questions/4308503/how-to-enable-visual-styles-without-a-manifest +// ULONG_PTR EnableVisualStyles(VOID) +// { +// TCHAR dir[MAX_PATH]; +// ULONG_PTR ulpActivationCookie = FALSE; +// ACTCTX actCtx = +// { +// sizeof(actCtx), +// ACTCTX_FLAG_RESOURCE_NAME_VALID +// | ACTCTX_FLAG_SET_PROCESS_DEFAULT +// | ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID, +// TEXT("shell32.dll"), 0, 0, dir, (LPCTSTR)124 +// }; +// UINT cch = GetSystemDirectory(dir, sizeof(dir) / sizeof(*dir)); +// if (cch >= sizeof(dir) / sizeof(*dir)) { return FALSE; /*shouldn't happen*/ } +// dir[cch] = TEXT('\0'); +// ActivateActCtx(CreateActCtx(&actCtx), &ulpActivationCookie); +// return ulpActivationCookie; +// } + // https://docs.microsoft.com/en-us/windows/win32/winmsg/using-messages-and-message-queues func messageLoop(wnd uintptr) error { getMessage := getMessage.Addr() @@ -394,6 +420,12 @@ type _WNDCLASSEX struct { IconSm uintptr } +// https://docs.microsoft.com/en-us/windows/win32/api/commctrl/ns-commctrl-initcommoncontrolsex +type _INITCOMMONCONTROLSEX struct { + Size uint32 + ICC uint32 +} + // https://github.com/wine-mirror/wine/blob/master/include/unknwn.idl type _IUnknownVtbl struct { From 7b98716a2046e4c3481ed9e9c76f2acd00e18a16 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Wed, 28 Apr 2021 01:27:28 +0100 Subject: [PATCH 07/12] WIP: progress (windows). --- README.md | 6 ++-- color_windows.go | 2 +- file_windows.go | 8 ++--- msg_windows.go | 2 +- progress_windows.go | 2 +- util_windows.go | 85 ++++++++++++++++++++++++++++----------------- 6 files changed, 64 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 6f7fd93..cb76318 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,7 @@ Implemented dialogs: Behavior on Windows, macOS and other Unixes might differ slightly. Some of that is intended (reflecting platform differences), -other bits are unfortunate limitations, -others still are open to be fixed. +other bits are unfortunate limitations. ## Why? @@ -37,7 +36,8 @@ Why reinvent this particular wheel? * Explorer shell not required * works in Server Core * Unicode support - * High DPI support (no manifest required) + * High DPI (no manifest required) + * Visual Styles (no manifest required) * WSL/Cygwin/MSYS2 [support](https://github.com/ncruces/zenity/wiki/Zenity-for-WSL,-Cygwin,-MSYS2) * on macOS: * only dependency is `osascript` diff --git a/color_windows.go b/color_windows.go index ce9dcd3..a678f4a 100644 --- a/color_windows.go +++ b/color_windows.go @@ -9,7 +9,7 @@ import ( var ( chooseColor = comdlg32.NewProc("ChooseColorW") - savedColors = [16]uint32{} + savedColors [16]uint32 colorsMutex sync.Mutex ) diff --git a/file_windows.go b/file_windows.go index 8727206..e5a9902 100644 --- a/file_windows.go +++ b/file_windows.go @@ -37,7 +37,7 @@ func selectFile(opts options) (string, error) { args.Filter = &initFilters(opts.fileFilters)[0] } - res := [32768]uint16{} + var res [32768]uint16 args.File = &res[0] args.MaxFile = uint32(len(res)) args.InitialDir, args.DefExt = initDirNameExt(opts.filename, res[:]) @@ -82,7 +82,7 @@ func selectFileMutiple(opts options) ([]string, error) { args.Filter = &initFilters(opts.fileFilters)[0] } - res := [32768 + 1024*256]uint16{} + var res [32768 + 1024*256]uint16 args.File = &res[0] args.MaxFile = uint32(len(res)) args.InitialDir, args.DefExt = initDirNameExt(opts.filename, res[:]) @@ -158,7 +158,7 @@ func selectFileSave(opts options) (string, error) { args.Filter = &initFilters(opts.fileFilters)[0] } - res := [32768]uint16{} + var res [32768]uint16 args.File = &res[0] args.MaxFile = uint32(len(res)) args.InitialDir, args.DefExt = initDirNameExt(opts.filename, res[:]) @@ -339,7 +339,7 @@ func browseForFolder(opts options) (string, []string, error) { } defer coTaskMemFree.Call(ptr) - res := [32768]uint16{} + var res [32768]uint16 shGetPathFromIDListEx.Call(ptr, uintptr(unsafe.Pointer(&res[0])), uintptr(len(res)), 0) str := syscall.UTF16ToString(res[:]) diff --git a/msg_windows.go b/msg_windows.go index 0f4214e..ede68f4 100644 --- a/msg_windows.go +++ b/msg_windows.go @@ -80,7 +80,7 @@ func hookMessageLabels(kind messageKind, opts options) (unhook context.CancelFun return hookDialog(opts.ctx, func(wnd uintptr) { enumChildWindows.Call(wnd, syscall.NewCallback(func(wnd, lparam uintptr) uintptr { - name := [8]uint16{} + var name [8]uint16 getClassName.Call(wnd, uintptr(unsafe.Pointer(&name)), uintptr(len(name))) if syscall.UTF16ToString(name[:]) == "Button" { ctl, _, _ := getDlgCtrlID.Call(wnd) diff --git a/progress_windows.go b/progress_windows.go index 30213f6..16830c6 100644 --- a/progress_windows.go +++ b/progress_windows.go @@ -129,7 +129,7 @@ func progress(opts options) (ProgressDialog, error) { centerWindow(wnd) showWindow.Call(wnd, 1 /* SW_SHOWNORMAL */, 0) if opts.maxValue < 0 { - sendMessage.Call(progCtl, 0x410 /* PBM_SETMARQUEE */, 1, 0) + sendMessage.Call(progCtl, 0x40a /* PBM_SETMARQUEE */, 1, 0) } else { sendMessage.Call(progCtl, 0x402 /* PBM_SETPOS */, 33, 0) sendMessage.Call(progCtl, 0x406 /* PBM_SETRANGE32 */, 0, uintptr(opts.maxValue)) diff --git a/util_windows.go b/util_windows.go index 076f164..1b5bf6f 100644 --- a/util_windows.go +++ b/util_windows.go @@ -32,6 +32,10 @@ var ( getModuleHandle = kernel32.NewProc("GetModuleHandleW") getCurrentThreadId = kernel32.NewProc("GetCurrentThreadId") getConsoleWindow = kernel32.NewProc("GetConsoleWindow") + getSystemDirectory = kernel32.NewProc("GetSystemDirectoryW") + createActCtx = kernel32.NewProc("CreateActCtxW") + activateActCtx = kernel32.NewProc("ActivateActCtx") + deactivateActCtx = kernel32.NewProc("DeactivateActCtx") coInitializeEx = ole32.NewProc("CoInitializeEx") coUninitialize = ole32.NewProc("CoUninitialize") @@ -98,15 +102,17 @@ func setup() context.CancelFunc { setForegroundWindow.Call(hwnd) } - var old uintptr runtime.LockOSThread() + + var restore uintptr + cookie := enableVisualStyles() if setThreadDpiAwarenessContext.Find() == nil { // try: // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE // DPI_AWARENESS_CONTEXT_SYSTEM_AWARE for i := -4; i <= -2; i++ { - restore, _, _ := setThreadDpiAwarenessContext.Call(uintptr(i)) + restore, _, _ = setThreadDpiAwarenessContext.Call(uintptr(i)) if restore != 0 { break } @@ -118,8 +124,11 @@ func setup() context.CancelFunc { icc.ICC = 0x00004020 // ICC_STANDARD_CLASSES|ICC_PROGRESS_CLASS return func() { - if old != 0 { - setThreadDpiAwarenessContext.Call(old) + if restore != 0 { + setThreadDpiAwarenessContext.Call(restore) + } + if cookie != 0 { + deactivateActCtx.Call(cookie) } runtime.UnlockOSThread() } @@ -145,7 +154,7 @@ func hookDialog(ctx context.Context, initDialog func(wnd uintptr)) (unhook conte hook, _, err = setWindowsHookEx.Call(12, // WH_CALLWNDPROCRET syscall.NewCallback(func(code int32, wparam uintptr, lparam *_CWPRETSTRUCT) uintptr { if lparam.Message == 0x0110 { // WM_INITDIALOG - name := [8]uint16{} + var name [8]uint16 getClassName.Call(lparam.Wnd, uintptr(unsafe.Pointer(&name)), uintptr(len(name))) if syscall.UTF16ToString(name[:]) == "#32770" { // The class for a dialog box var close bool @@ -290,26 +299,6 @@ func registerClass(instance, proc uintptr) (uintptr, error) { return ret, err } -// https://stackoverflow.com/questions/4308503/how-to-enable-visual-styles-without-a-manifest -// ULONG_PTR EnableVisualStyles(VOID) -// { -// TCHAR dir[MAX_PATH]; -// ULONG_PTR ulpActivationCookie = FALSE; -// ACTCTX actCtx = -// { -// sizeof(actCtx), -// ACTCTX_FLAG_RESOURCE_NAME_VALID -// | ACTCTX_FLAG_SET_PROCESS_DEFAULT -// | ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID, -// TEXT("shell32.dll"), 0, 0, dir, (LPCTSTR)124 -// }; -// UINT cch = GetSystemDirectory(dir, sizeof(dir) / sizeof(*dir)); -// if (cch >= sizeof(dir) / sizeof(*dir)) { return FALSE; /*shouldn't happen*/ } -// dir[cch] = TEXT('\0'); -// ActivateActCtx(CreateActCtx(&actCtx), &ulpActivationCookie); -// return ulpActivationCookie; -// } - // https://docs.microsoft.com/en-us/windows/win32/winmsg/using-messages-and-message-queues func messageLoop(wnd uintptr) error { getMessage := getMessage.Addr() @@ -335,6 +324,46 @@ func messageLoop(wnd uintptr) error { } } +// https://stackoverflow.com/questions/4308503/how-to-enable-visual-styles-without-a-manifest +func enableVisualStyles() (cookie uintptr) { + var dir [260]uint16 + n, _, _ := getSystemDirectory.Call(uintptr(unsafe.Pointer(&dir[0])), uintptr(len(dir))) + if n == 0 || int(n) >= len(dir) { + return + } + + var ctx _ACTCTX + ctx.Size = uint32(unsafe.Sizeof(ctx)) + ctx.Flags = 0x01c // ACTCTX_FLAG_RESOURCE_NAME_VALID|ACTCTX_FLAG_SET_PROCESS_DEFAULT|ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID + ctx.Source = syscall.StringToUTF16Ptr("shell32.dll") + ctx.AssemblyDirectory = &dir[0] + ctx.ResourceName = 124 + + if h, _, _ := createActCtx.Call(uintptr(unsafe.Pointer(&ctx))); h != 0 { + activateActCtx.Call(h, uintptr(unsafe.Pointer(&cookie))) + } + return +} + +// https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-actctxw +type _ACTCTX struct { + Size uint32 + Flags uint32 + Source *uint16 + ProcessorArchitecture uint16 + LangId uint16 + AssemblyDirectory *uint16 + ResourceName uintptr + ApplicationName *uint16 + Module uintptr +} + +// https://docs.microsoft.com/en-us/windows/win32/api/commctrl/ns-commctrl-initcommoncontrolsex +type _INITCOMMONCONTROLSEX struct { + Size uint32 + ICC uint32 +} + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-cwpretstruct type _CWPRETSTRUCT struct { Result uintptr @@ -420,12 +449,6 @@ type _WNDCLASSEX struct { IconSm uintptr } -// https://docs.microsoft.com/en-us/windows/win32/api/commctrl/ns-commctrl-initcommoncontrolsex -type _INITCOMMONCONTROLSEX struct { - Size uint32 - ICC uint32 -} - // https://github.com/wine-mirror/wine/blob/master/include/unknwn.idl type _IUnknownVtbl struct { From 8db7926394f4dd02520a1fb6c04e1c227c5a4a07 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Thu, 29 Apr 2021 16:05:28 +0100 Subject: [PATCH 08/12] Canceled error. --- cmd/zenity/main.go | 60 +++++++++----------------------- color.go | 2 -- color_darwin.go | 8 ++--- color_unix.go | 8 ++--- entry.go | 4 +-- entry_darwin.go | 2 +- entry_test.go | 4 +-- entry_unix.go | 2 +- entry_windows.go | 19 +++++----- file.go | 6 ---- file_darwin.go | 6 ++-- file_unix.go | 6 ++-- file_windows.go | 4 +-- internal/zenutil/run_darwin.go | 14 ++++---- internal/zenutil/run_progress.go | 36 +++++++++---------- internal/zenutil/run_unix.go | 16 ++++----- list.go | 12 ++----- list_darwin.go | 2 +- list_test.go | 4 +-- list_unix.go | 2 +- list_windows.go | 8 +++-- msg.go | 20 +++-------- msg_darwin.go | 6 ++-- msg_test.go | 6 ++-- msg_unix.go | 6 ++-- msg_windows.go | 20 +++++------ pwd.go | 4 +-- pwd_stub.go | 6 ++-- pwd_test.go | 4 +-- pwd_unix.go | 10 +++--- util_unix.go | 14 ++++---- util_windows.go | 2 +- zenity.go | 10 ++++++ 33 files changed, 139 insertions(+), 194 deletions(-) diff --git a/cmd/zenity/main.go b/cmd/zenity/main.go index 4d4e6bc..e9d4087 100644 --- a/cmd/zenity/main.go +++ b/cmd/zenity/main.go @@ -100,27 +100,27 @@ func main() { switch { case errorDlg: - okResult(zenity.Error(text, opts...)) + errResult(zenity.Error(text, opts...)) case infoDlg: - okResult(zenity.Info(text, opts...)) + errResult(zenity.Info(text, opts...)) case warningDlg: - okResult(zenity.Warning(text, opts...)) + errResult(zenity.Warning(text, opts...)) case questionDlg: - okResult(zenity.Question(text, opts...)) + errResult(zenity.Question(text, opts...)) case entryDlg: - strOKResult(zenity.Entry(text, opts...)) + strResult(zenity.Entry(text, opts...)) case listDlg: if multiple { - listResult(zenity.ListMultiple(text, flag.Args(), opts...)) + lstResult(zenity.ListMultiple(text, flag.Args(), opts...)) } else { - strOKResult(zenity.List(text, flag.Args(), opts...)) + strResult(zenity.List(text, flag.Args(), opts...)) } case passwordDlg: - _, pw, ok, err := zenity.Password(opts...) - strOKResult(pw, ok, err) + _, pw, err := zenity.Password(opts...) + strResult(pw, err) case fileSelectionDlg: switch { @@ -129,11 +129,11 @@ func main() { case save: strResult(egestPath(zenity.SelectFileSave(opts...))) case multiple: - listResult(egestPaths(zenity.SelectFileMutiple(opts...))) + lstResult(egestPaths(zenity.SelectFileMutiple(opts...))) } case colorSelectionDlg: - colorResult(zenity.SelectColor(opts...)) + colResult(zenity.SelectColor(opts...)) case notification: errResult(zenity.Notify(text, opts...)) @@ -395,6 +395,9 @@ func errResult(err error) { if os.IsTimeout(err) { os.Exit(5) } + if err == zenity.ErrCanceled { + os.Exit(1) + } if err == zenity.ErrExtraButton { os.Stdout.WriteString(extraButton) os.Stdout.WriteString(zenutil.LineBreak) @@ -408,64 +411,33 @@ func errResult(err error) { os.Exit(0) } -func okResult(ok bool, err error) { - if err != nil { - errResult(err) - } - if ok { - os.Exit(0) - } - os.Exit(1) -} - func strResult(s string, err error) { if err != nil { errResult(err) } - if s == "" { - os.Exit(1) - } os.Stdout.WriteString(s) os.Stdout.WriteString(zenutil.LineBreak) os.Exit(0) } -func listResult(l []string, err error) { +func lstResult(l []string, err error) { if err != nil { errResult(err) } - if l == nil { - os.Exit(1) - } os.Stdout.WriteString(strings.Join(l, zenutil.Separator)) os.Stdout.WriteString(zenutil.LineBreak) os.Exit(0) } -func colorResult(c color.Color, err error) { +func colResult(c color.Color, err error) { if err != nil { errResult(err) } - if c == nil { - os.Exit(1) - } os.Stdout.WriteString(zenutil.UnparseColor(c)) os.Stdout.WriteString(zenutil.LineBreak) os.Exit(0) } -func strOKResult(s string, ok bool, err error) { - if err != nil { - errResult(err) - } - if !ok { - os.Exit(1) - } - os.Stdout.WriteString(s) - os.Stdout.WriteString(zenutil.LineBreak) - os.Exit(0) -} - func ingestPath(path string) string { if runtime.GOOS == "windows" && path != "" { var args []string diff --git a/color.go b/color.go index f9978de..781e6a5 100644 --- a/color.go +++ b/color.go @@ -4,8 +4,6 @@ import "image/color" // SelectColor displays the color selection dialog. // -// Returns nil on cancel. -// // Valid options: Title, Color, ShowPalette. func SelectColor(options ...Option) (color.Color, error) { return selectColor(applyOptions(options)) diff --git a/color_darwin.go b/color_darwin.go index 4b745a2..1abc599 100644 --- a/color_darwin.go +++ b/color_darwin.go @@ -20,9 +20,9 @@ func selectColor(opts options) (color.Color, error) { float32(g) / 0xffff, float32(b) / 0xffff, }) - str, ok, err := strResult(opts, out, err) - if ok { - return zenutil.ParseColor(str), nil + str, err := strResult(opts, out, err) + if err != nil { + return nil, err } - return nil, err + return zenutil.ParseColor(str), nil } diff --git a/color_unix.go b/color_unix.go index b8efc1f..8a2fc1e 100644 --- a/color_unix.go +++ b/color_unix.go @@ -20,9 +20,9 @@ func selectColor(opts options) (color.Color, error) { } out, err := zenutil.Run(opts.ctx, args) - str, ok, err := strResult(opts, out, err) - if ok { - return zenutil.ParseColor(str), nil + str, err := strResult(opts, out, err) + if err != nil { + return nil, err } - return nil, err + return zenutil.ParseColor(str), nil } diff --git a/entry.go b/entry.go index 4a96d80..ca571d8 100644 --- a/entry.go +++ b/entry.go @@ -2,11 +2,9 @@ package zenity // Entry displays the text entry dialog. // -// Returns false on cancel, or ErrExtraButton. -// // Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton, // Icon, EntryText, HideText. -func Entry(text string, options ...Option) (string, bool, error) { +func Entry(text string, options ...Option) (string, error) { return entry(text, applyOptions(options)) } diff --git a/entry_darwin.go b/entry_darwin.go index 6de4e61..039a296 100644 --- a/entry_darwin.go +++ b/entry_darwin.go @@ -4,7 +4,7 @@ import ( "github.com/ncruces/zenity/internal/zenutil" ) -func entry(text string, opts options) (string, bool, error) { +func entry(text string, opts options) (string, error) { var data zenutil.Dialog data.Text = text data.Operation = "displayDialog" diff --git a/entry_test.go b/entry_test.go index ee3cbc2..6395332 100644 --- a/entry_test.go +++ b/entry_test.go @@ -19,7 +19,7 @@ func ExampleEntry() { func TestEntryTimeout(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second/10) - _, _, err := zenity.Entry("", zenity.Context(ctx)) + _, err := zenity.Entry("", zenity.Context(ctx)) if !os.IsTimeout(err) { t.Error("did not timeout:", err) } @@ -31,7 +31,7 @@ func TestEntryCancel(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() - _, _, err := zenity.Entry("", zenity.Context(ctx)) + _, err := zenity.Entry("", zenity.Context(ctx)) if !errors.Is(err, context.Canceled) { t.Error("was not canceled:", err) } diff --git a/entry_unix.go b/entry_unix.go index 05b2a70..9f51e88 100644 --- a/entry_unix.go +++ b/entry_unix.go @@ -6,7 +6,7 @@ import ( "github.com/ncruces/zenity/internal/zenutil" ) -func entry(text string, opts options) (string, bool, error) { +func entry(text string, opts options) (string, error) { args := []string{"--entry", "--text", text} args = appendTitle(args, opts) args = appendButtons(args, opts) diff --git a/entry_windows.go b/entry_windows.go index dcda663..f216270 100644 --- a/entry_windows.go +++ b/entry_windows.go @@ -4,7 +4,7 @@ import ( "syscall" ) -func entry(text string, opts options) (out string, ok bool, err error) { +func entry(text string, opts options) (out string, err error) { if opts.title == nil { opts.title = stringPtr("") } @@ -49,6 +49,7 @@ func entry(text string, opts options) (out string, ok bool, err error) { postQuitMessage.Call(0) case 0x0010: // WM_CLOSE + err = ErrCanceled destroyWindow.Call(wnd) case 0x0111: // WM_COMMAND @@ -57,8 +58,8 @@ func entry(text string, opts options) (out string, ok bool, err error) { return 1 case 1, 6: // IDOK, IDYES out = getWindowString(editCtl) - ok = true case 2: // IDCANCEL + err = ErrCanceled case 7: // IDNO err = ErrExtraButton } @@ -76,17 +77,17 @@ func entry(text string, opts options) (out string, ok bool, err error) { } if opts.ctx != nil && opts.ctx.Err() != nil { - return "", false, opts.ctx.Err() + return "", opts.ctx.Err() } instance, _, err := getModuleHandle.Call(0) if instance == 0 { - return "", false, err + return "", err } cls, err := registerClass(instance, syscall.NewCallback(proc)) if cls == 0 { - return "", false, err + return "", err } defer unregisterClass.Call(cls, instance) @@ -145,13 +146,13 @@ func entry(text string, opts options) (out string, ok bool, err error) { } // set default values - out, ok, err = "", false, nil + out, err = "", nil if err := messageLoop(wnd); err != nil { - return "", false, err + return "", err } if opts.ctx != nil && opts.ctx.Err() != nil { - return "", false, opts.ctx.Err() + return "", opts.ctx.Err() } - return out, ok, err + return out, err } diff --git a/file.go b/file.go index ab52ada..fb2ca03 100644 --- a/file.go +++ b/file.go @@ -8,8 +8,6 @@ import ( // SelectFile displays the file selection dialog. // -// Returns an empty string on cancel. -// // Valid options: Title, Directory, Filename, ShowHidden, FileFilter(s). func SelectFile(options ...Option) (string, error) { return selectFile(applyOptions(options)) @@ -17,8 +15,6 @@ func SelectFile(options ...Option) (string, error) { // SelectFileMutiple displays the multiple file selection dialog. // -// Returns a nil slice on cancel. -// // Valid options: Title, Directory, Filename, ShowHidden, FileFilter(s). func SelectFileMutiple(options ...Option) ([]string, error) { return selectFileMutiple(applyOptions(options)) @@ -26,8 +22,6 @@ func SelectFileMutiple(options ...Option) ([]string, error) { // SelectFileSave displays the save file selection dialog. // -// Returns an empty string on cancel. -// // Valid options: Title, Filename, ConfirmOverwrite, ConfirmCreate, ShowHidden, // FileFilter(s). func SelectFileSave(options ...Option) (string, error) { diff --git a/file_darwin.go b/file_darwin.go index dc73507..668424d 100644 --- a/file_darwin.go +++ b/file_darwin.go @@ -18,8 +18,7 @@ func selectFile(opts options) (string, error) { } out, err := zenutil.Run(opts.ctx, "file", data) - str, _, err := strResult(opts, out, err) - return str, err + return strResult(opts, out, err) } func selectFileMutiple(opts options) ([]string, error) { @@ -54,6 +53,5 @@ func selectFileSave(opts options) (string, error) { } out, err := zenutil.Run(opts.ctx, "file", data) - str, _, err := strResult(opts, out, err) - return str, err + return strResult(opts, out, err) } diff --git a/file_unix.go b/file_unix.go index a200562..c1a5074 100644 --- a/file_unix.go +++ b/file_unix.go @@ -14,8 +14,7 @@ func selectFile(opts options) (string, error) { args = appendFileArgs(args, opts) out, err := zenutil.Run(opts.ctx, args) - str, _, err := strResult(opts, out, err) - return str, err + return strResult(opts, out, err) } func selectFileMutiple(opts options) ([]string, error) { @@ -33,8 +32,7 @@ func selectFileSave(opts options) (string, error) { args = appendFileArgs(args, opts) out, err := zenutil.Run(opts.ctx, args) - str, _, err := strResult(opts, out, err) - return str, err + return strResult(opts, out, err) } func initFilters(filters []FileFilter) []string { diff --git a/file_windows.go b/file_windows.go index e5a9902..305fd10 100644 --- a/file_windows.go +++ b/file_windows.go @@ -251,7 +251,7 @@ func pickFolders(opts options, multi bool) (str string, lst []string, err error) return "", nil, opts.ctx.Err() } if hr == 0x800704c7 { // ERROR_CANCELLED - return "", nil, nil + return "", nil, ErrCanceled } if int32(hr) < 0 { return "", nil, syscall.Errno(hr) @@ -335,7 +335,7 @@ func browseForFolder(opts options) (string, []string, error) { return "", nil, opts.ctx.Err() } if ptr == 0 { - return "", nil, nil + return "", nil, ErrCanceled } defer coTaskMemFree.Call(ptr) diff --git a/internal/zenutil/run_darwin.go b/internal/zenutil/run_darwin.go index dead84e..eb24d99 100644 --- a/internal/zenutil/run_darwin.go +++ b/internal/zenutil/run_darwin.go @@ -51,7 +51,7 @@ func Run(ctx context.Context, script string, data interface{}) ([]byte, error) { } // RunProgress is internal. -func RunProgress(ctx context.Context, max int, env []string) (m *progressDialog, err error) { +func RunProgress(ctx context.Context, max int, env []string) (*progressDialog, error) { t, err := ioutil.TempDir("", "") if err != nil { return nil, err @@ -98,14 +98,14 @@ func RunProgress(ctx context.Context, max int, env []string) (m *progressDialog, if err != nil { return nil, err } - if err = cmd.Start(); err != nil { + if err := cmd.Start(); err != nil { return nil, err } if ctx == nil { ctx = context.Background() } - m = &progressDialog{ + dlg := &progressDialog{ done: make(chan struct{}), lines: make(chan string), max: max, @@ -115,8 +115,8 @@ func RunProgress(ctx context.Context, max int, env []string) (m *progressDialog, if cerr := ctx.Err(); cerr != nil { err = cerr } - m.err = err - close(m.done) + dlg.err = err + close(dlg.done) os.RemoveAll(t) }() go func() { @@ -124,7 +124,7 @@ func RunProgress(ctx context.Context, max int, env []string) (m *progressDialog, for { var line string select { - case s, ok := <-m.lines: + case s, ok := <-dlg.lines: if !ok { return } @@ -138,7 +138,7 @@ func RunProgress(ctx context.Context, max int, env []string) (m *progressDialog, } } }() - return + return dlg, nil } // Dialog is internal. diff --git a/internal/zenutil/run_progress.go b/internal/zenutil/run_progress.go index e8e793a..23c51ff 100644 --- a/internal/zenutil/run_progress.go +++ b/internal/zenutil/run_progress.go @@ -14,37 +14,37 @@ type progressDialog struct { max int } -func (m *progressDialog) send(line string) error { +func (d *progressDialog) send(line string) error { select { - case m.lines <- line: + case d.lines <- line: return nil - case <-m.done: - return m.err + case <-d.done: + return d.err } } -func (m *progressDialog) Close() error { - close(m.lines) - <-m.done - return m.err +func (d *progressDialog) Close() error { + close(d.lines) + <-d.done + return d.err } -func (m *progressDialog) Text(text string) error { - return m.send("#" + text) +func (d *progressDialog) Text(text string) error { + return d.send("#" + text) } -func (m *progressDialog) Value(value int) error { - if m.percent { - return m.send(strconv.FormatFloat(100*float64(value)/float64(m.max), 'f', -1, 64)) +func (d *progressDialog) Value(value int) error { + if d.percent { + return d.send(strconv.FormatFloat(100*float64(value)/float64(d.max), 'f', -1, 64)) } else { - return m.send(strconv.Itoa(value)) + return d.send(strconv.Itoa(value)) } } -func (m *progressDialog) MaxValue() int { - return m.max +func (d *progressDialog) MaxValue() int { + return d.max } -func (m *progressDialog) Done() <-chan struct{} { - return m.done +func (d *progressDialog) Done() <-chan struct{} { + return d.done } diff --git a/internal/zenutil/run_unix.go b/internal/zenutil/run_unix.go index 8d92acb..96ed624 100644 --- a/internal/zenutil/run_unix.go +++ b/internal/zenutil/run_unix.go @@ -42,7 +42,7 @@ func Run(ctx context.Context, args []string) ([]byte, error) { } // RunProgress is internal. -func RunProgress(ctx context.Context, max int, args []string) (m *progressDialog, err error) { +func RunProgress(ctx context.Context, max int, args []string) (*progressDialog, error) { if Command && path != "" { if Timeout > 0 { args = append(args, "--timeout", strconv.Itoa(Timeout)) @@ -55,14 +55,14 @@ func RunProgress(ctx context.Context, max int, args []string) (m *progressDialog if err != nil { return nil, err } - if err = cmd.Start(); err != nil { + if err := cmd.Start(); err != nil { return nil, err } if ctx == nil { ctx = context.Background() } - m = &progressDialog{ + dlg := &progressDialog{ done: make(chan struct{}), lines: make(chan string), percent: true, @@ -71,7 +71,7 @@ func RunProgress(ctx context.Context, max int, args []string) (m *progressDialog go func() { err := cmd.Wait() select { - case _, ok := <-m.lines: + case _, ok := <-dlg.lines: if !ok { err = nil } @@ -80,15 +80,15 @@ func RunProgress(ctx context.Context, max int, args []string) (m *progressDialog if cerr := ctx.Err(); cerr != nil { err = cerr } - m.err = err - close(m.done) + dlg.err = err + close(dlg.done) }() go func() { defer cmd.Process.Signal(syscall.SIGTERM) for { var line string select { - case s, ok := <-m.lines: + case s, ok := <-dlg.lines: if !ok { return } @@ -101,5 +101,5 @@ func RunProgress(ctx context.Context, max int, args []string) (m *progressDialog } } }() - return + return dlg, nil } diff --git a/list.go b/list.go index d3acebd..2c78313 100644 --- a/list.go +++ b/list.go @@ -2,25 +2,19 @@ package zenity // List displays the list dialog. // -// Returns false on cancel, or ErrExtraButton. -// // Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton, // Icon, DefaultItems, DisallowEmpty. -func List(text string, items []string, options ...Option) (string, bool, error) { +func List(text string, items []string, options ...Option) (string, error) { return list(text, items, applyOptions(options)) } // ListItems displays the list dialog. -// -// Returns false on cancel, or ErrExtraButton. -func ListItems(text string, items ...string) (string, bool, error) { +func ListItems(text string, items ...string) (string, error) { return List(text, items) } // ListMultiple displays the list dialog, allowing multiple items to be selected. // -// Returns a nil slice on cancel, or ErrExtraButton. -// // Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton, // Icon, DefaultItems, DisallowEmpty. func ListMultiple(text string, items []string, options ...Option) ([]string, error) { @@ -28,8 +22,6 @@ func ListMultiple(text string, items []string, options ...Option) ([]string, err } // ListMultipleItems displays the list dialog, allowing multiple items to be selected. -// -// Returns a nil slice on cancel, or ErrExtraButton. func ListMultipleItems(text string, items ...string) ([]string, error) { return ListMultiple(text, items) } diff --git a/list_darwin.go b/list_darwin.go index a6b2be0..5ccaa7e 100644 --- a/list_darwin.go +++ b/list_darwin.go @@ -4,7 +4,7 @@ import ( "github.com/ncruces/zenity/internal/zenutil" ) -func list(text string, items []string, opts options) (string, bool, error) { +func list(text string, items []string, opts options) (string, error) { var data zenutil.List data.Items = items data.Options.Prompt = &text diff --git a/list_test.go b/list_test.go index 4967020..f7ec686 100644 --- a/list_test.go +++ b/list_test.go @@ -47,7 +47,7 @@ func ExampleListMultipleItems() { func TestListTimeout(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second/10) - _, _, err := zenity.List("", nil, zenity.Context(ctx)) + _, err := zenity.List("", nil, zenity.Context(ctx)) if !os.IsTimeout(err) { t.Error("did not timeout:", err) } @@ -59,7 +59,7 @@ func TestListCancel(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() - _, _, err := zenity.List("", nil, zenity.Context(ctx)) + _, err := zenity.List("", nil, zenity.Context(ctx)) if !errors.Is(err, context.Canceled) { t.Error("was not canceled:", err) } diff --git a/list_unix.go b/list_unix.go index ce905a4..c7de987 100644 --- a/list_unix.go +++ b/list_unix.go @@ -6,7 +6,7 @@ import ( "github.com/ncruces/zenity/internal/zenutil" ) -func list(text string, items []string, opts options) (string, bool, error) { +func list(text string, items []string, opts options) (string, error) { args := []string{"--list", "--column=", "--hide-header", "--text", text} args = appendTitle(args, opts) args = appendButtons(args, opts) diff --git a/list_windows.go b/list_windows.go index 67aecbe..63a82f2 100644 --- a/list_windows.go +++ b/list_windows.go @@ -5,12 +5,12 @@ import ( "unsafe" ) -func list(text string, items []string, opts options) (string, bool, error) { +func list(text string, items []string, opts options) (string, error) { items, err := listDlg(text, items, false, opts) if len(items) == 1 { - return items[0], true, err + return items[0], err } - return "", false, err + return "", err } func listMultiple(text string, items []string, opts options) ([]string, error) { @@ -62,6 +62,7 @@ func listDlg(text string, items []string, multiple bool, opts options) (out []st postQuitMessage.Call(0) case 0x0010: // WM_CLOSE + err = ErrCanceled destroyWindow.Call(wnd) case 0x0111: // WM_COMMAND @@ -88,6 +89,7 @@ func listDlg(text string, items []string, multiple bool, opts options) (out []st } } case 2: // IDCANCEL + err = ErrCanceled case 7: // IDNO err = ErrExtraButton } diff --git a/msg.go b/msg.go index d054c9f..48e20b1 100644 --- a/msg.go +++ b/msg.go @@ -1,46 +1,34 @@ package zenity -// ErrExtraButton is returned by dialog functions when the extra button is -// pressed. -const ErrExtraButton = stringErr("extra button pressed") - // Question displays the question dialog. // -// Returns true on OK, false on Cancel, or ErrExtraButton. -// // Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton, // Icon, NoWrap, Ellipsize, DefaultCancel. -func Question(text string, options ...Option) (bool, error) { +func Question(text string, options ...Option) error { return message(questionKind, text, applyOptions(options)) } // Info displays the info dialog. // -// Returns true on OK, false on dismiss, or ErrExtraButton. -// // Valid options: Title, Width, Height, OKLabel, ExtraButton, Icon, // NoWrap, Ellipsize. -func Info(text string, options ...Option) (bool, error) { +func Info(text string, options ...Option) error { return message(infoKind, text, applyOptions(options)) } // Warning displays the warning dialog. // -// Returns true on OK, false on dismiss, or ErrExtraButton. -// // Valid options: Title, Width, Height, OKLabel, ExtraButton, Icon, // NoWrap, Ellipsize. -func Warning(text string, options ...Option) (bool, error) { +func Warning(text string, options ...Option) error { return message(warningKind, text, applyOptions(options)) } // Error displays the error dialog. // -// Returns true on OK, false on dismiss, or ErrExtraButton. -// // Valid options: Title, Width, Height, OKLabel, ExtraButton, Icon, // NoWrap, Ellipsize. -func Error(text string, options ...Option) (bool, error) { +func Error(text string, options ...Option) error { return message(errorKind, text, applyOptions(options)) } diff --git a/msg_darwin.go b/msg_darwin.go index 03a1827..27836cf 100644 --- a/msg_darwin.go +++ b/msg_darwin.go @@ -4,7 +4,7 @@ import ( "github.com/ncruces/zenity/internal/zenutil" ) -func message(kind messageKind, text string, opts options) (bool, error) { +func message(kind messageKind, text string, opts options) error { var data zenutil.Dialog data.Text = text data.Options.Timeout = zenutil.Timeout @@ -33,6 +33,6 @@ func message(kind messageKind, text string, opts options) (bool, error) { data.SetButtons(getButtons(dialog, kind == questionKind, opts)) out, err := zenutil.Run(opts.ctx, "dialog", data) - _, ok, err := strResult(opts, out, err) - return ok, err + _, err = strResult(opts, out, err) + return err } diff --git a/msg_test.go b/msg_test.go index 984669f..be83c8b 100644 --- a/msg_test.go +++ b/msg_test.go @@ -38,7 +38,7 @@ func ExampleQuestion() { // Output: } -var msgFuncs = []func(string, ...zenity.Option) (bool, error){ +var msgFuncs = []func(string, ...zenity.Option) error{ zenity.Error, zenity.Info, zenity.Warning, @@ -49,7 +49,7 @@ func TestMessageTimeout(t *testing.T) { for _, f := range msgFuncs { ctx, cancel := context.WithTimeout(context.Background(), time.Second/10) - _, err := f("text", zenity.Context(ctx)) + err := f("text", zenity.Context(ctx)) if !os.IsTimeout(err) { t.Error("did not timeout:", err) } @@ -63,7 +63,7 @@ func TestMessageCancel(t *testing.T) { cancel() for _, f := range msgFuncs { - _, err := f("text", zenity.Context(ctx)) + err := f("text", zenity.Context(ctx)) if !errors.Is(err, context.Canceled) { t.Error("was not canceled:", err) } diff --git a/msg_unix.go b/msg_unix.go index 9ba81e1..12f647d 100644 --- a/msg_unix.go +++ b/msg_unix.go @@ -6,7 +6,7 @@ import ( "github.com/ncruces/zenity/internal/zenutil" ) -func message(kind messageKind, text string, opts options) (bool, error) { +func message(kind messageKind, text string, opts options) error { args := []string{"--text", text, "--no-markup"} switch kind { case questionKind: @@ -47,6 +47,6 @@ func message(kind messageKind, text string, opts options) (bool, error) { } out, err := zenutil.Run(opts.ctx, args) - _, ok, err := strResult(opts, out, err) - return ok, err + _, err = strResult(opts, out, err) + return err } diff --git a/msg_windows.go b/msg_windows.go index ede68f4..26a740c 100644 --- a/msg_windows.go +++ b/msg_windows.go @@ -12,7 +12,7 @@ var ( getDlgCtrlID = user32.NewProc("GetDlgCtrlID") ) -func message(kind messageKind, text string, opts options) (bool, error) { +func message(kind messageKind, text string, opts options) error { var flags uintptr switch { @@ -48,7 +48,7 @@ func message(kind messageKind, text string, opts options) (bool, error) { if opts.ctx != nil || opts.okLabel != nil || opts.cancelLabel != nil || opts.extraButton != nil { unhook, err := hookMessageLabels(kind, opts) if err != nil { - return false, err + return err } defer unhook() } @@ -62,17 +62,17 @@ func message(kind messageKind, text string, opts options) (bool, error) { s, _, err := messageBox.Call(0, strptr(text), title, flags) if opts.ctx != nil && opts.ctx.Err() != nil { - return false, opts.ctx.Err() + return opts.ctx.Err() } switch s { case 1, 6: // IDOK, IDYES - return true, nil + return nil case 2: // IDCANCEL - return false, nil + return ErrCanceled case 7: // IDNO - return false, ErrExtraButton + return ErrExtraButton default: - return false, err + return err } } @@ -89,11 +89,7 @@ func hookMessageLabels(kind messageKind, opts options) (unhook context.CancelFun case 1, 6: // IDOK, IDYES text = opts.okLabel case 2: // IDCANCEL - if kind == questionKind { - text = opts.cancelLabel - } else { - text = opts.okLabel - } + text = opts.cancelLabel case 7: // IDNO text = opts.extraButton } diff --git a/pwd.go b/pwd.go index 685c129..4c3b7aa 100644 --- a/pwd.go +++ b/pwd.go @@ -2,10 +2,8 @@ package zenity // Password displays the password dialog. // -// Returns false on cancel, or ErrExtraButton. -// // Valid options: Title, OKLabel, CancelLabel, ExtraButton, Icon, Username. -func Password(options ...Option) (usr string, pw string, ok bool, err error) { +func Password(options ...Option) (usr string, pw string, err error) { return password(applyOptions(options)) } diff --git a/pwd_stub.go b/pwd_stub.go index 63bc24a..62c3ab7 100644 --- a/pwd_stub.go +++ b/pwd_stub.go @@ -2,8 +2,8 @@ package zenity -func password(opts options) (string, string, bool, error) { +func password(opts options) (string, string, error) { opts.hideText = true - str, ok, err := entry("Password:", opts) - return "", str, ok, err + str, err := entry("Password:", opts) + return "", str, err } diff --git a/pwd_test.go b/pwd_test.go index 7b6df89..81a7067 100644 --- a/pwd_test.go +++ b/pwd_test.go @@ -18,7 +18,7 @@ func ExamplePassword() { func TestPasswordTimeout(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second/10) - _, _, _, err := zenity.Password(zenity.Context(ctx)) + _, _, err := zenity.Password(zenity.Context(ctx)) if !os.IsTimeout(err) { t.Error("did not timeout:", err) } @@ -30,7 +30,7 @@ func TestPasswordCancel(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() - _, _, _, err := zenity.Password(zenity.Context(ctx)) + _, _, err := zenity.Password(zenity.Context(ctx)) if !errors.Is(err, context.Canceled) { t.Error("was not canceled:", err) } diff --git a/pwd_unix.go b/pwd_unix.go index e97f3ed..135ffdc 100644 --- a/pwd_unix.go +++ b/pwd_unix.go @@ -8,7 +8,7 @@ import ( "github.com/ncruces/zenity/internal/zenutil" ) -func password(opts options) (string, string, bool, error) { +func password(opts options) (string, string, error) { args := []string{"--password"} args = appendTitle(args, opts) args = appendButtons(args, opts) @@ -17,11 +17,11 @@ func password(opts options) (string, string, bool, error) { } out, err := zenutil.Run(opts.ctx, args) - str, ok, err := strResult(opts, out, err) - if ok && opts.username { + str, err := strResult(opts, out, err) + if err == nil && opts.username { if split := strings.SplitN(string(out), "|", 2); len(split) == 2 { - return split[0], split[1], true, nil + return split[0], split[1], nil } } - return "", str, ok, err + return "", str, err } diff --git a/util_unix.go b/util_unix.go index 2348a28..3e53728 100644 --- a/util_unix.go +++ b/util_unix.go @@ -55,23 +55,23 @@ func appendIcon(args []string, opts options) []string { return args } -func strResult(opts options, out []byte, err error) (string, bool, error) { +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) { - return "", false, ErrExtraButton + return "", ErrExtraButton } - return "", false, nil + return "", ErrCanceled } if err != nil { - return "", false, err + return "", err } - return string(out), true, nil + return string(out), nil } func lstResult(opts options, out []byte, err error) ([]string, error) { - str, ok, err := strResult(opts, out, err) - if ok { + str, err := strResult(opts, out, err) + if err == nil { return strings.Split(str, zenutil.Separator), nil } return nil, err diff --git a/util_windows.go b/util_windows.go index 1b5bf6f..fa2ca3c 100644 --- a/util_windows.go +++ b/util_windows.go @@ -137,7 +137,7 @@ func setup() context.CancelFunc { func commDlgError() error { s, _, _ := commDlgExtendedError.Call() if s == 0 { - return nil + return ErrCanceled } else { return fmt.Errorf("Common Dialog error: %x", s) } diff --git a/zenity.go b/zenity.go index b125364..7f7763f 100644 --- a/zenity.go +++ b/zenity.go @@ -21,6 +21,16 @@ 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") + +// ErrExtraButton is returned when the extra button is pressed. +const ErrExtraButton = stringErr("extra button pressed") + +// ErrUnsupported is returned when a combination of options is not supported. +const ErrUnsupported = stringErr("unsupported option") + type options struct { // General options title *string From e6ec7f896280e3ebc3fb621cc32ab6a768da1be1 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Thu, 29 Apr 2021 16:38:59 +0100 Subject: [PATCH 09/12] WIP: progress (windows). --- entry_windows.go | 4 +- list_windows.go | 4 +- progress_windows.go | 103 +++++++++++++++++++++++++++++++++++++++----- util_windows.go | 19 ++++---- 4 files changed, 106 insertions(+), 24 deletions(-) diff --git a/entry_windows.go b/entry_windows.go index f216270..0c2f604 100644 --- a/entry_windows.go +++ b/entry_windows.go @@ -69,8 +69,8 @@ func entry(text string, opts options) (out string, err error) { layout(dpi(uint32(wparam) >> 16)) default: - ret, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0) - return ret + res, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0) + return res } return 0 diff --git a/list_windows.go b/list_windows.go index 63a82f2..f8efece 100644 --- a/list_windows.go +++ b/list_windows.go @@ -99,8 +99,8 @@ func listDlg(text string, items []string, multiple bool, opts options) (out []st layout(dpi(uint32(wparam) >> 16)) default: - ret, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0) - return ret + res, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0) + return res } return 0 diff --git a/progress_windows.go b/progress_windows.go index 16830c6..c0b46ba 100644 --- a/progress_windows.go +++ b/progress_windows.go @@ -1,6 +1,8 @@ package zenity import ( + "context" + "sync" "syscall" ) @@ -17,7 +19,30 @@ func progress(opts options) (ProgressDialog, error) { if opts.maxValue == 0 { opts.maxValue = 100 } + if opts.ctx == nil { + opts.ctx = context.Background() + } + dlg := &progressDialog{ + done: make(chan struct{}), + max: opts.maxValue, + } + dlg.init.Add(1) + + go func() { + err := progressDlg(opts, dlg) + if cerr := opts.ctx.Err(); cerr != nil { + err = cerr + } + dlg.err = err + close(dlg.done) + }() + + dlg.init.Wait() + return dlg, nil +} + +func progressDlg(opts options, dlg *progressDialog) (err error) { defer setup()() font := getFont() defer font.Delete() @@ -51,6 +76,7 @@ func progress(opts options) (ProgressDialog, error) { postQuitMessage.Call(0) case 0x0010: // WM_CLOSE + err = ErrCanceled destroyWindow.Call(wnd) case 0x0111: // WM_COMMAND @@ -58,8 +84,11 @@ func progress(opts options) (ProgressDialog, error) { default: return 1 case 1, 6: // IDOK, IDYES + // case 2: // IDCANCEL + err = ErrCanceled case 7: // IDNO + err = ErrExtraButton } destroyWindow.Call(wnd) @@ -67,25 +96,25 @@ func progress(opts options) (ProgressDialog, error) { layout(dpi(uint32(wparam) >> 16)) default: - ret, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0) - return ret + res, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0) + return res } return 0 } if opts.ctx != nil && opts.ctx.Err() != nil { - return nil, opts.ctx.Err() + return opts.ctx.Err() } instance, _, err := getModuleHandle.Call(0) if instance == 0 { - return nil, err + return err } cls, err := registerClass(instance, syscall.NewCallback(proc)) if cls == 0 { - return nil, err + return err } defer unregisterClass.Call(cls, instance) @@ -112,7 +141,7 @@ func progress(opts options) (ProgressDialog, error) { okBtn, _, _ = createWindowEx.Call(0, strptr("BUTTON"), strptr(*opts.okLabel), - 0x50030001, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP|BS_DEFPUSHBUTTON + 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), @@ -131,9 +160,13 @@ func progress(opts options) (ProgressDialog, error) { if opts.maxValue < 0 { sendMessage.Call(progCtl, 0x40a /* PBM_SETMARQUEE */, 1, 0) } else { - sendMessage.Call(progCtl, 0x402 /* PBM_SETPOS */, 33, 0) sendMessage.Call(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 { wait := make(chan struct{}) @@ -148,13 +181,61 @@ func progress(opts options) (ProgressDialog, error) { } // set default values - // out, ok, err = "", false, nil + err = nil if err := messageLoop(wnd); err != nil { - return nil, err + return err } if opts.ctx != nil && opts.ctx.Err() != nil { - return nil, opts.ctx.Err() + return opts.ctx.Err() } - return nil, err + return err +} + +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 +} + +func (d *progressDialog) Text(text string) error { + select { + default: + setWindowText.Call(d.text, strptr(text)) + return nil + case <-d.done: + return d.err + } +} + +func (d *progressDialog) Value(value int) error { + select { + default: + sendMessage.Call(d.prog, 0x402 /* PBM_SETPOS */, uintptr(value), 0) + if value >= d.max { + enableWindow.Call(d.ok, 1) + } + return nil + case <-d.done: + return d.err + } +} + +func (d *progressDialog) MaxValue() int { + return d.max +} + +func (d *progressDialog) Done() <-chan struct{} { + return d.done } diff --git a/util_windows.go b/util_windows.go index fa2ca3c..0444c0b 100644 --- a/util_windows.go +++ b/util_windows.go @@ -72,6 +72,7 @@ var ( destroyWindow = user32.NewProc("DestroyWindow") createWindowEx = user32.NewProc("CreateWindowExW") showWindow = user32.NewProc("ShowWindow") + enableWindow = user32.NewProc("EnableWindow") setFocus = user32.NewProc("SetFocus") defWindowProc = user32.NewProc("DefWindowProcW") ) @@ -267,8 +268,8 @@ func (f *font) Delete() { func centerWindow(wnd uintptr) { getMetric := func(i uintptr) int32 { - ret, _, _ := getSystemMetrics.Call(i) - return int32(ret) + n, _, _ := getSystemMetrics.Call(i) + return int32(n) } var rect _RECT @@ -295,8 +296,8 @@ func registerClass(instance, proc uintptr) (uintptr, error) { wcx.Background = 5 // COLOR_WINDOW wcx.ClassName = syscall.StringToUTF16Ptr(name) - ret, _, err := registerClassEx.Call(uintptr(unsafe.Pointer(&wcx))) - return ret, err + atom, _, err := registerClassEx.Call(uintptr(unsafe.Pointer(&wcx))) + return atom, err } // https://docs.microsoft.com/en-us/windows/win32/winmsg/using-messages-and-message-queues @@ -308,16 +309,16 @@ func messageLoop(wnd uintptr) error { for { var msg _MSG - ret, _, err := syscall.Syscall6(getMessage, 4, uintptr(unsafe.Pointer(&msg)), 0, 0, 0, 0, 0) - if int32(ret) == -1 { + s, _, err := syscall.Syscall6(getMessage, 4, uintptr(unsafe.Pointer(&msg)), 0, 0, 0, 0, 0) + if int32(s) == -1 { return err } - if ret == 0 { + if s == 0 { return nil } - ret, _, _ = syscall.Syscall(isDialogMessage, 2, wnd, uintptr(unsafe.Pointer(&msg)), 0) - if ret == 0 { + s, _, _ = syscall.Syscall(isDialogMessage, 2, wnd, uintptr(unsafe.Pointer(&msg)), 0) + if s == 0 { syscall.Syscall(translateMessage, 1, uintptr(unsafe.Pointer(&msg)), 0, 0) syscall.Syscall(dispatchMessage, 1, uintptr(unsafe.Pointer(&msg)), 0, 0) } From 0a67c8f6987d6e016e163b82cd82ce278d7d7af0 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Thu, 29 Apr 2021 16:55:44 +0100 Subject: [PATCH 10/12] Unsupported options. --- pwd_stub.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pwd_stub.go b/pwd_stub.go index 62c3ab7..d204940 100644 --- a/pwd_stub.go +++ b/pwd_stub.go @@ -3,6 +3,9 @@ package zenity func password(opts options) (string, string, error) { + if opts.username { + return "", "", ErrUnsupported + } opts.hideText = true str, err := entry("Password:", opts) return "", str, err From aeaa60875848a26b0f71eb31d439624ba8196c92 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Fri, 30 Apr 2021 19:05:49 +0100 Subject: [PATCH 11/12] WIP: progress. --- internal/zenutil/env_darwin.go | 1 + internal/zenutil/env_unix.go | 1 + internal/zenutil/env_windows.go | 1 + internal/zenutil/run_darwin.go | 34 +++---------- internal/zenutil/run_progress.go | 82 ++++++++++++++++++++++++++++---- internal/zenutil/run_unix.go | 43 +++-------------- progress.go | 3 ++ zenity.go | 6 +++ 8 files changed, 97 insertions(+), 74 deletions(-) diff --git a/internal/zenutil/env_darwin.go b/internal/zenutil/env_darwin.go index fd9fdda..7233920 100644 --- a/internal/zenutil/env_darwin.go +++ b/internal/zenutil/env_darwin.go @@ -10,4 +10,5 @@ var ( Command bool Timeout int Separator = "\x00" + Canceled error ) diff --git a/internal/zenutil/env_unix.go b/internal/zenutil/env_unix.go index 9cd9e04..5788b42 100644 --- a/internal/zenutil/env_unix.go +++ b/internal/zenutil/env_unix.go @@ -13,4 +13,5 @@ var ( Command bool Timeout int Separator = "\x1e" + Canceled error ) diff --git a/internal/zenutil/env_windows.go b/internal/zenutil/env_windows.go index ee0957a..5c20d99 100644 --- a/internal/zenutil/env_windows.go +++ b/internal/zenutil/env_windows.go @@ -10,4 +10,5 @@ var ( Command bool Timeout int Separator string + Canceled error ) diff --git a/internal/zenutil/run_darwin.go b/internal/zenutil/run_darwin.go index eb24d99..3112dc7 100644 --- a/internal/zenutil/run_darwin.go +++ b/internal/zenutil/run_darwin.go @@ -106,37 +106,15 @@ func RunProgress(ctx context.Context, max int, env []string) (*progressDialog, e } dlg := &progressDialog{ - done: make(chan struct{}), - lines: make(chan string), + cmd: cmd, max: max, + lines: make(chan string), + done: make(chan struct{}), } + go dlg.pipe(pipe) go func() { - err := cmd.Wait() - if cerr := ctx.Err(); cerr != nil { - err = cerr - } - dlg.err = err - close(dlg.done) - os.RemoveAll(t) - }() - go func() { - defer pipe.Close() - for { - var line string - select { - case s, ok := <-dlg.lines: - if !ok { - return - } - line = s - case <-ctx.Done(): - return - case <-time.After(40 * time.Millisecond): - } - if _, err := pipe.Write([]byte(line + "\n")); err != nil { - return - } - } + defer os.RemoveAll(t) + dlg.wait() }() return dlg, nil } diff --git a/internal/zenutil/run_progress.go b/internal/zenutil/run_progress.go index 23c51ff..f0d2f3a 100644 --- a/internal/zenutil/run_progress.go +++ b/internal/zenutil/run_progress.go @@ -3,15 +3,25 @@ package zenutil import ( + "context" + "io" + "os" + "os/exec" + "runtime" "strconv" + "sync/atomic" + "time" ) type progressDialog struct { - err error - done chan struct{} - lines chan string - percent bool + ctx context.Context + cmd *exec.Cmd max int + percent bool + closed int32 + lines chan string + done chan struct{} + err error } func (d *progressDialog) send(line string) error { @@ -23,12 +33,6 @@ func (d *progressDialog) send(line string) error { } } -func (d *progressDialog) Close() error { - close(d.lines) - <-d.done - return d.err -} - func (d *progressDialog) Text(text string) error { return d.send("#" + text) } @@ -48,3 +52,61 @@ func (d *progressDialog) MaxValue() int { func (d *progressDialog) Done() <-chan struct{} { return d.done } + +func (d *progressDialog) Complete() error { + close(d.lines) + select { + case <-d.done: + return d.err + default: + return nil + } +} + +func (d *progressDialog) Close() error { + atomic.StoreInt32(&d.closed, 1) + d.cmd.Process.Signal(os.Interrupt) + <-d.done + return d.err +} + +func (d *progressDialog) wait() { + err := d.cmd.Wait() + if cerr := d.ctx.Err(); cerr != nil { + err = cerr + } + if eerr, ok := err.(*exec.ExitError); ok { + switch { + case eerr.ExitCode() == -1 && atomic.LoadInt32(&d.closed) != 0: + err = nil + case eerr.ExitCode() == 1: + err = Canceled + } + } + d.err = err + close(d.done) +} + +func (d *progressDialog) pipe(w io.WriteCloser) { + defer w.Close() + var timeout = time.Second + if runtime.GOOS == "darwin" { + timeout = 40 * time.Millisecond + } + for { + var line string + select { + case s, ok := <-d.lines: + if !ok { + return + } + line = s + case <-d.ctx.Done(): + return + case <-time.After(timeout): + } + if _, err := w.Write([]byte(line + "\n")); err != nil { + return + } + } +} diff --git a/internal/zenutil/run_unix.go b/internal/zenutil/run_unix.go index 96ed624..5a4b0cd 100644 --- a/internal/zenutil/run_unix.go +++ b/internal/zenutil/run_unix.go @@ -63,43 +63,14 @@ func RunProgress(ctx context.Context, max int, args []string) (*progressDialog, } dlg := &progressDialog{ - done: make(chan struct{}), - lines: make(chan string), - percent: true, + ctx: ctx, + cmd: cmd, max: max, + percent: true, + lines: make(chan string), + done: make(chan struct{}), } - go func() { - err := cmd.Wait() - select { - case _, ok := <-dlg.lines: - if !ok { - err = nil - } - default: - } - if cerr := ctx.Err(); cerr != nil { - err = cerr - } - dlg.err = err - close(dlg.done) - }() - go func() { - defer cmd.Process.Signal(syscall.SIGTERM) - for { - var line string - select { - case s, ok := <-dlg.lines: - if !ok { - return - } - line = s - case <-ctx.Done(): - return - } - if _, err := pipe.Write([]byte(line + "\n")); err != nil { - return - } - } - }() + go dlg.pipe(pipe) + go dlg.wait() return dlg, nil } diff --git a/progress.go b/progress.go index 5f5dffe..3fd781f 100644 --- a/progress.go +++ b/progress.go @@ -19,6 +19,9 @@ type ProgressDialog interface { // MaxValue gets how much work the task requires in total. MaxValue() int + // Complete marks the task completed. + Complete() error + // Close closes the dialog. Close() error diff --git a/zenity.go b/zenity.go index 7f7763f..bb7f3ea 100644 --- a/zenity.go +++ b/zenity.go @@ -13,6 +13,8 @@ package zenity import ( "context" "image/color" + + "github.com/ncruces/zenity/internal/zenutil" ) type stringErr string @@ -31,6 +33,10 @@ const ErrExtraButton = stringErr("extra button pressed") // ErrUnsupported is returned when a combination of options is not supported. const ErrUnsupported = stringErr("unsupported option") +func init() { + zenutil.Canceled = ErrCanceled +} + type options struct { // General options title *string From 71541c249aec3f089764856b606b7f586eab5938 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Fri, 30 Apr 2021 19:19:14 +0100 Subject: [PATCH 12/12] 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