Progress.

This commit is contained in:
Nuno Cruces 2021-04-30 19:19:14 +01:00
parent aeaa608758
commit 71541c249a
33 changed files with 464 additions and 177 deletions

View File

@ -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.

View File

@ -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(&notification, "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
}

72
cmd/zenity/progress.go Normal file
View File

@ -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()
}

View File

@ -0,0 +1,12 @@
// +build !windows,!js
package main
import (
"os"
"syscall"
)
func killParent() {
syscall.Kill(os.Getppid(), syscall.SIGHUP)
}

View File

@ -0,0 +1,3 @@
package main
func killParent() {}

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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()

12
internal/zenutil/env.go Normal file
View File

@ -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) }

View File

@ -10,5 +10,4 @@ var (
Command bool
Timeout int
Separator = "\x00"
Canceled error
)

View File

@ -13,5 +13,4 @@ var (
Command bool
Timeout int
Separator = "\x1e"
Canceled error
)

View File

@ -10,5 +10,4 @@ var (
Command bool
Timeout int
Separator string
Canceled error
)

View File

@ -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}}`))

View File

@ -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"))

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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 })
}

View File

@ -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)
}

100
progress_test.go Normal file
View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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()

View File

@ -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

View File

@ -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")

View File

@ -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