From 10c3d63ca52efce7b5c9dd92c3a464d1628f045f Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Mon, 26 Apr 2021 00:36:15 +0100 Subject: [PATCH] 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") }