package zenity
import "image/color"
// SelectColor displays the color selection dialog.
//
// Valid options: Title, WindowIcon, Attach, Modal, Color, ShowPalette.
//
// May return: ErrCanceled.
func SelectColor(options ...Option) (color.Color, error) {
return selectColor(applyOptions(options))
}
// Color returns an Option to set the color.
func Color(c color.Color) Option {
return funcOption(func(o *options) { o.color = c })
}
// ShowPalette returns an Option to show the palette.
func ShowPalette() Option {
return funcOption(func(o *options) { o.showPalette = true })
}
//go:build !windows && !darwin
package zenity
import (
"image/color"
"github.com/ncruces/zenity/internal/zenutil"
)
func selectColor(opts options) (color.Color, error) {
args := []string{"--color-selection"}
args = appendGeneral(args, opts)
if opts.color != nil {
args = append(args, "--color", zenutil.UnparseColor(opts.color))
}
if opts.showPalette {
args = append(args, "--show-palette")
}
out, err := zenutil.Run(opts.ctx, args)
str, err := strResult(opts, out, err)
if err != nil {
return nil, err
}
return zenutil.ParseColor(str), nil
}
package zenity
import "time"
// Calendar displays the calendar dialog.
//
// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
// WindowIcon, Attach, Modal, DefaultDate.
//
// May return: ErrCanceled, ErrExtraButton.
func Calendar(text string, options ...Option) (time.Time, error) {
return calendar(text, applyOptions(options))
}
// DefaultDate returns an Option to set the date.
func DefaultDate(year int, month time.Month, day int) Option {
return funcOption(func(o *options) {
o.time = ptr(time.Date(year, month, day, 0, 0, 0, 0, time.Local))
})
}
//go:build !windows && !darwin
package zenity
import (
"strconv"
"time"
"github.com/ncruces/zenity/internal/zenutil"
)
func calendar(text string, opts options) (time.Time, error) {
args := []string{"--calendar", "--text", quoteMarkup(text), "--date-format", zenutil.DateFormat}
args = appendGeneral(args, opts)
args = appendButtons(args, opts)
args = appendWidthHeight(args, opts)
args = appendWindowIcon(args, opts)
if opts.time != nil {
year, month, day := opts.time.Date()
args = append(args, "--month", strconv.Itoa(int(month)))
args = append(args, "--day", strconv.Itoa(day))
args = append(args, "--year", strconv.Itoa(year))
}
out, err := zenutil.Run(opts.ctx, args)
str, err := strResult(opts, out, err)
if err != nil {
return time.Time{}, err
}
return zenutil.DateParse(str)
}
package zenity
// Entry displays the text entry dialog.
//
// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
// WindowIcon, Attach, Modal, EntryText, HideText.
//
// May return: ErrCanceled, ErrExtraButton.
func Entry(text string, options ...Option) (string, error) {
return entry(text, applyOptions(options))
}
// EntryText returns an Option to set the entry text.
func EntryText(text string) Option {
return funcOption(func(o *options) { o.entryText = text })
}
// HideText returns an Option to hide the entry text.
func HideText() Option {
return funcOption(func(o *options) { o.hideText = true })
}
//go:build !windows && !darwin
package zenity
import "github.com/ncruces/zenity/internal/zenutil"
func entry(text string, opts options) (string, error) {
args := []string{"--entry", "--text", quoteMnemonics(text)}
args = appendGeneral(args, opts)
args = appendButtons(args, opts)
args = appendWidthHeight(args, opts)
args = appendWindowIcon(args, opts)
if opts.entryText != "" {
args = append(args, "--entry-text", opts.entryText)
}
if opts.hideText {
args = append(args, "--hide-text")
}
out, err := zenutil.Run(opts.ctx, args)
return strResult(opts, out, err)
}
package zenity
import (
"os"
"path/filepath"
"strings"
"unicode"
)
// SelectFile displays the file selection dialog.
//
// Valid options: Title, WindowIcon, Attach, Modal, Directory, Filename,
// ShowHidden, FileFilter(s).
//
// May return: ErrCanceled.
func SelectFile(options ...Option) (string, error) {
return selectFile(applyOptions(options))
}
// SelectFileMultiple displays the multiple file selection dialog.
//
// Valid options: Title, WindowIcon, Attach, Modal, Directory, Filename,
// ShowHidden, FileFilter(s).
//
// May return: ErrCanceled, ErrUnsupported.
func SelectFileMultiple(options ...Option) ([]string, error) {
return selectFileMultiple(applyOptions(options))
}
// SelectFileSave displays the save file selection dialog.
//
// Valid options: Title, WindowIcon, Attach, Modal, Filename,
// ConfirmOverwrite, ConfirmCreate, ShowHidden, FileFilter(s).
//
// May return: ErrCanceled.
func SelectFileSave(options ...Option) (string, error) {
return selectFileSave(applyOptions(options))
}
// Directory returns an Option to activate directory-only selection.
func Directory() Option {
return funcOption(func(o *options) { o.directory = true })
}
// ConfirmOverwrite returns an Option to confirm file selection if the file
// already exists.
func ConfirmOverwrite() Option {
return funcOption(func(o *options) { o.confirmOverwrite = true })
}
// ConfirmCreate returns an Option to confirm file selection if the file
// does not yet exist (Windows only).
func ConfirmCreate() Option {
return funcOption(func(o *options) { o.confirmCreate = true })
}
// ShowHidden returns an Option to show hidden files (Windows and macOS only).
func ShowHidden() Option {
return funcOption(func(o *options) { o.showHidden = true })
}
// Filename returns an Option to set the filename.
//
// You can specify a file name, a directory path, or both.
// Specifying a file name, makes it the default selected file.
// Specifying a directory path, makes it the default dialog location.
func Filename(filename string) Option {
return funcOption(func(o *options) { o.filename = filename })
}
// FileFilter is an Option that sets a filename filter.
//
// On Windows and macOS filtering is always case-insensitive.
//
// macOS hides filename filters from the user,
// and only supports filtering by extension
// (or "uniform type identifiers").
//
// Patterns may use the fnmatch syntax on all platforms:
// https://docs.python.org/3/library/fnmatch.html
type FileFilter struct {
Name string // display string that describes the filter (optional)
Patterns []string // filter patterns for the display string
CaseFold bool // if set patterns are matched case-insensitively
}
func (f FileFilter) apply(o *options) {
o.fileFilters = append(o.fileFilters, f)
}
// FileFilters is an Option that sets multiple filename filters.
type FileFilters []FileFilter
func (f FileFilters) apply(o *options) {
o.fileFilters = append(o.fileFilters, f...)
}
// Windows patterns need a name.
func (f FileFilters) name() {
for i, filter := range f {
if filter.Name == "" {
f[i].Name = strings.Join(filter.Patterns, " ")
}
}
}
// Windows patterns are case-insensitive, don't support character classes or escaping.
//
// First we remove character classes, then escaping. Patterns with literal wildcards are invalid.
// The semicolon is a separator, so we replace it with the single character wildcard.
func (f FileFilters) simplify() {
for i := range f {
var j = 0
for _, pattern := range f[i].Patterns {
var escape, invalid bool
var buf strings.Builder
for _, b := range []byte(removeClasses(pattern)) {
if !escape && b == '\\' {
escape = true
continue
}
if escape && (b == '*' || b == '?') {
invalid = true
break
}
if b == ';' {
b = '?'
}
buf.WriteByte(b)
escape = false
}
if buf.Len() > 0 && !invalid {
f[i].Patterns[j] = buf.String()
j++
}
}
if j != 0 {
f[i].Patterns = f[i].Patterns[:j]
} else {
f[i].Patterns = nil
}
}
}
// macOS types may be specified as extension strings without the leading period,
// or as uniform type identifiers:
// https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/PromptforaFileorFolder.html
//
// First check for uniform type identifiers.
// Then we extract the extension from each pattern, remove character classes, then escaping.
// If an extension contains a wildcard, any type is accepted.
func (f FileFilters) types() []string {
var res []string
for _, filter := range f {
for _, pattern := range filter.Patterns {
if isUniformTypeIdentifier(pattern) {
res = append(res, pattern)
continue
}
dot := strings.LastIndexByte(pattern, '.')
if dot < 0 {
continue
}
var escape bool
var buf strings.Builder
for _, b := range []byte(removeClasses(pattern[dot+1:])) {
switch {
case escape:
escape = false
case b == '\\':
escape = true
continue
case b == '*' || b == '?':
return nil
}
buf.WriteByte(b)
}
res = append(res, buf.String())
}
}
if res == nil {
return nil
}
// Workaround for macOS bug: first type cannot be a four letter extension, so prepend dot string.
return append([]string{"."}, res...)
}
// Unix patterns are case-sensitive. Fold them if requested.
func (f FileFilters) casefold() {
for i := range f {
if !f[i].CaseFold {
continue
}
for j, pattern := range f[i].Patterns {
var class = -1
var escape bool
var buf strings.Builder
for i, r := range pattern {
switch {
case escape:
escape = false
case r == '\\':
escape = true
case class < 0:
if r == '[' {
class = i
}
case class < i-1:
if r == ']' {
class = -1
}
}
nr := unicode.SimpleFold(r)
if r == nr {
buf.WriteRune(r)
continue
}
if class < 0 {
buf.WriteByte('[')
}
buf.WriteRune(r)
for r != nr {
buf.WriteRune(nr)
nr = unicode.SimpleFold(nr)
}
if class < 0 {
buf.WriteByte(']')
}
}
f[i].Patterns[j] = buf.String()
}
}
}
// Remove character classes from pattern, assuming case insensitivity.
// Classes of one character (case-insensitive) are replaced by the character.
// Others are replaced by the single character wildcard.
func removeClasses(pattern string) string {
var res strings.Builder
for {
i, j := findClass(pattern)
if i < 0 {
res.WriteString(pattern)
return res.String()
}
res.WriteString(pattern[:i])
var char string
var escape, many bool
for _, r := range pattern[i+1 : j-1] {
if escape {
escape = false
} else if r == '\\' {
escape = true
continue
}
if char == "" {
char = string(r)
} else if !strings.EqualFold(char, string(r)) {
many = true
break
}
}
if many {
res.WriteByte('?')
} else {
res.WriteByte('\\')
res.WriteString(char)
}
pattern = pattern[j:]
}
}
// Find a character class in the pattern.
func findClass(pattern string) (start, end int) {
start = -1
var escape bool
for i, b := range []byte(pattern) {
switch {
case escape:
escape = false
case b == '\\':
escape = true
case start < 0:
if b == '[' {
start = i
}
case start < i-1:
if b == ']' {
return start, i + 1
}
}
}
return -1, -1
}
// Uniform type identifiers use the reverse-DNS format:
// https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/understanding_utis/understand_utis_conc/understand_utis_conc.html
func isUniformTypeIdentifier(pattern string) bool {
labels := strings.Split(pattern, ".")
if len(labels) < 2 {
return false
}
for _, label := range labels {
if len := len(label); len == 0 || label[0] == '-' || label[len-1] == '-' {
return false
}
for _, r := range label {
switch {
case r == '-' || r > '\x7f' ||
'a' <= r && r <= 'z' ||
'A' <= r && r <= 'Z' ||
'0' <= r && r <= '9':
continue
default:
return false
}
}
}
return true
}
func splitDirAndName(path string) (dir, name string, err error) {
if path == "" {
return "", "", nil
}
fi, err := os.Stat(path)
if err == nil && fi.IsDir() {
return path, "", nil
}
dir, name = filepath.Split(path)
if dir == "" {
return "", name, nil
}
_, err = os.Stat(dir)
return dir, name, err
}
//go:build !windows && !darwin
package zenity
import (
"strings"
"github.com/ncruces/zenity/internal/zenutil"
)
func selectFile(opts options) (string, error) {
args := []string{"--file-selection"}
args = appendGeneral(args, opts)
args = appendFileArgs(args, opts)
out, err := zenutil.Run(opts.ctx, args)
return strResult(opts, out, err)
}
func selectFileMultiple(opts options) ([]string, error) {
args := []string{"--file-selection", "--multiple", "--separator", zenutil.Separator}
args = appendGeneral(args, opts)
args = appendFileArgs(args, opts)
out, err := zenutil.Run(opts.ctx, args)
return lstResult(opts, out, err)
}
func selectFileSave(opts options) (string, error) {
args := []string{"--file-selection", "--save"}
args = appendGeneral(args, opts)
args = appendFileArgs(args, opts)
out, err := zenutil.Run(opts.ctx, args)
return strResult(opts, out, err)
}
func initFilters(filters FileFilters) []string {
var res []string
filters.casefold()
for _, f := range filters {
var buf strings.Builder
buf.WriteString("--file-filter=")
if f.Name != "" {
buf.WriteString(f.Name)
buf.WriteByte('|')
}
for i, p := range f.Patterns {
if i != 0 {
buf.WriteByte(' ')
}
buf.WriteString(p)
}
res = append(res, buf.String())
}
return res
}
func appendFileArgs(args []string, opts options) []string {
if opts.directory {
args = append(args, "--directory")
}
if opts.filename != "" {
args = append(args, "--filename", opts.filename)
}
if opts.confirmOverwrite {
args = append(args, "--confirm-overwrite")
}
args = append(args, initFilters(opts.fileFilters)...)
return args
}
package zencmd
import (
"encoding/xml"
"io"
"strings"
)
// StripMarkup is internal.
func StripMarkup(s string) string {
// Strips XML markup described in:
// https://docs.gtk.org/Pango/pango_markup.html
dec := xml.NewDecoder(strings.NewReader(s))
var res strings.Builder
for {
t, err := dec.Token()
if err == io.EOF {
return res.String()
}
if err != nil {
return s
}
if t, ok := t.(xml.CharData); ok {
res.Write(t)
}
}
}
package zencmd
import "strings"
// StripMnemonic is internal.
func StripMnemonic(s string) string {
// Strips mnemonics described in:
// https: //docs.gtk.org/gtk4/class.Label.html#mnemonics
var res strings.Builder
underscore := false
for _, b := range []byte(s) {
switch {
case underscore:
underscore = false
case b == '_':
underscore = true
continue
}
res.WriteByte(b)
}
return res.String()
}
package zencmd
import "strings"
// Unescape is internal.
func Unescape(s string) string {
// Apply rules described in:
// https://docs.gtk.org/glib/func.strescape.html
const (
initial = iota
escape1
escape2
escape3
)
var oct byte
var res strings.Builder
state := initial
for _, b := range []byte(s) {
switch state {
default:
switch b {
case '\\':
state = escape1
default:
res.WriteByte(b)
state = initial
}
case escape1:
switch b {
case '0', '1', '2', '3', '4', '5', '6', '7':
oct = b - '0'
state = escape2
case 'b':
res.WriteByte('\b')
state = initial
case 'f':
res.WriteByte('\f')
state = initial
case 'n':
res.WriteByte('\n')
state = initial
case 'r':
res.WriteByte('\r')
state = initial
case 't':
res.WriteByte('\t')
state = initial
case 'v':
res.WriteByte('\v')
state = initial
default:
res.WriteByte(b)
state = initial
}
case escape2:
switch b {
case '0', '1', '2', '3', '4', '5', '6', '7':
oct = oct<<3 | (b - '0')
state = escape3
case '\\':
res.WriteByte(oct)
state = escape1
default:
res.WriteByte(oct)
res.WriteByte(b)
state = initial
}
case escape3:
switch b {
case '0', '1', '2', '3', '4', '5', '6', '7':
oct = oct<<3 | (b - '0')
res.WriteByte(oct)
state = initial
case '\\':
res.WriteByte(oct)
state = escape1
default:
res.WriteByte(oct)
res.WriteByte(b)
state = initial
}
}
}
if state == escape2 || state == escape3 {
res.WriteByte(oct)
}
return res.String()
}
//go:build !windows && !darwin
package zencmd
import (
"bytes"
"fmt"
"io"
"math"
"os/exec"
"strconv"
"strings"
)
// ParseWindowId is internal.
func ParseWindowId(id string) int {
wid, _ := strconv.ParseUint(id, 0, 64)
return int(wid & math.MaxInt)
}
// GetParentWindowId is internal.
func GetParentWindowId(pid int) int {
winids, err := getPidToWindowMap()
if err != nil {
return 0
}
ppids, err := getPidToPpidMap()
if err != nil {
return 0
}
for {
if winid, ok := winids[pid]; ok {
id, _ := strconv.Atoi(winid)
return id
}
if ppid, ok := ppids[pid]; ok {
pid = ppid
} else {
return 0
}
}
}
func getPidToPpidMap() (map[int]int, error) {
out, err := exec.Command("ps", "-xo", "pid=,ppid=").Output()
if err != nil {
return nil, err
}
ppids := map[int]int{}
reader := bytes.NewReader(out)
for {
var pid, ppid int
_, err := fmt.Fscan(reader, &pid, &ppid)
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
ppids[pid] = ppid
}
return ppids, nil
}
func getPidToWindowMap() (map[int]string, error) {
ids, err := getWindowIDs()
if err != nil {
return nil, err
}
var pid int
winids := map[int]string{}
for _, id := range ids {
pid, err = getWindowPid(id)
if err != nil {
continue
}
winids[pid] = id
}
if err != nil && len(winids) == 0 {
return nil, err
}
return winids, nil
}
func getWindowIDs() ([]string, error) {
out, err := exec.Command("xprop", "-root", "0i", "\t$0+", "_NET_CLIENT_LIST").Output()
if err != nil {
return nil, err
}
if _, out, cut := bytes.Cut(out, []byte("\t")); cut {
return strings.Split(string(out), ", "), nil
}
return nil, fmt.Errorf("xprop: unexpected output: %q", out)
}
func getWindowPid(id string) (int, error) {
out, err := exec.Command("xprop", "-id", id, "0i", "\t$0", "_NET_WM_PID").Output()
if err != nil {
return 0, err
}
if _, out, cut := bytes.Cut(out, []byte("\t")); cut {
return strconv.Atoi(string(out))
}
return 0, fmt.Errorf("xprop: unexpected output: %q", out)
}
package zenutil
import (
"fmt"
"image/color"
"strings"
"golang.org/x/image/colornames"
)
// ParseColor is internal.
func ParseColor(s string) color.Color {
if len(s) == 4 || len(s) == 5 {
c := color.NRGBA{A: 0xf}
n, _ := fmt.Sscanf(s, "#%1x%1x%1x%1x", &c.R, &c.G, &c.B, &c.A)
c.R, c.G, c.B, c.A = c.R*0x11, c.G*0x11, c.B*0x11, c.A*0x11
if n >= 3 {
return c
}
}
if len(s) == 7 || len(s) == 9 {
c := color.NRGBA{A: 0xff}
n, _ := fmt.Sscanf(s, "#%02x%02x%02x%02x", &c.R, &c.G, &c.B, &c.A)
if n >= 3 {
return c
}
}
if len(s) >= 10 && "rgb" == s[:3] {
c := color.NRGBA{A: 0xff}
if _, err := fmt.Sscanf(s, "rgb(%d,%d,%d)", &c.R, &c.G, &c.B); err == nil {
return c
}
var a float32
if _, err := fmt.Sscanf(s, "rgba(%d,%d,%d,%f)", &c.R, &c.G, &c.B, &a); err == nil {
switch {
case a <= 0:
c.A = 0
case a >= 1:
c.A = 255
default:
c.A = uint8(255*a + 0.5)
}
return c
}
}
c, ok := colornames.Map[strings.ToLower(s)]
if ok {
return c
}
return nil
}
// UnparseColor is internal.
func UnparseColor(c color.Color) string {
n := color.NRGBAModel.Convert(c).(color.NRGBA)
if n.A == 255 {
return fmt.Sprintf("rgb(%d,%d,%d)", n.R, n.G, n.B)
} else {
return fmt.Sprintf("rgba(%d,%d,%d,%f)", n.R, n.G, n.B, float32(n.A)/255)
}
}
// ColorEquals is internal.
func ColorEquals(c1, c2 color.Color) bool {
if c1 == nil || c2 == nil {
return c1 == c2
}
r1, g1, b1, a1 := c1.RGBA()
r2, g2, b2, a2 := c2.RGBA()
return r1 == r2 && g1 == g2 && b1 == b2 && a1 == a2
}
// Package zenutil is internal. DO NOT USE.
package zenutil
import "time"
// These are internal.
const (
ErrCanceled = stringErr("dialog canceled")
ErrExtraButton = stringErr("extra button pressed")
ErrUnsupported = stringErr("unsupported option")
)
// These are internal.
var (
Command bool
Timeout int
DateFormat = "%Y-%m-%d"
DateUTS35 = func() (string, error) { return "yyyy-MM-dd", nil }
DateParse = func(s string) (time.Time, error) { return time.Parse("2006-01-02", s) }
)
type stringErr string
func (e stringErr) Error() string { return string(e) }
//go:build !windows
package zenutil
import (
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"sync/atomic"
"time"
)
type progressDialog struct {
ctx context.Context
cmd *exec.Cmd
max int
close bool
percent bool
closed int32
lines chan string
done chan struct{}
err error
}
func (d *progressDialog) send(line string) error {
select {
case d.lines <- line:
return nil
case <-d.done:
return d.err
}
}
func (d *progressDialog) Text(text string) error {
return d.send("#" + text)
}
func (d *progressDialog) Value(value int) error {
if value >= d.max && d.close {
return d.Close()
}
if d.percent {
return d.send(strconv.FormatFloat(100*float64(value)/float64(d.max), 'f', -1, 64))
} else {
return d.send(strconv.Itoa(value))
}
}
func (d *progressDialog) MaxValue() int {
return d.max
}
func (d *progressDialog) Done() <-chan struct{} {
return d.done
}
func (d *progressDialog) Complete() error {
err := d.Value(d.max)
close(d.lines)
return err
}
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(extra *string, out *bytes.Buffer) {
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:
if extra != nil && *extra == strings.TrimSuffix(out.String(), "\n") {
err = ErrExtraButton
} else {
err = ErrCanceled
}
default:
err = fmt.Errorf("%w: %s", eerr, eerr.Stderr)
}
}
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 <-d.done:
return
case <-time.After(timeout):
// line = ""
}
if _, err := w.Write([]byte(line + "\n")); err != nil {
return
}
}
}
//go:build !windows && !darwin
package zenutil
import (
"bytes"
"context"
"os"
"os/exec"
"strconv"
"sync"
"syscall"
)
var (
tool, path string
pathOnce sync.Once
)
func initPath() {
for _, tool = range [3]string{"qarma", "zenity", "matedialog"} {
path, _ = exec.LookPath(tool)
if path != "" {
return
}
}
tool = "zenity"
}
// IsAvailable is internal.
func IsAvailable() bool {
pathOnce.Do(initPath)
return path != ""
}
// Run is internal.
func Run(ctx context.Context, args []string) ([]byte, error) {
pathOnce.Do(initPath)
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 {
out, err := exec.CommandContext(ctx, tool, args...).Output()
if ctx.Err() != nil {
err = ctx.Err()
}
return out, err
}
return exec.Command(tool, args...).Output()
}
// RunProgress is internal.
func RunProgress(ctx context.Context, max int, close bool, extra *string, args []string) (*progressDialog, error) {
pathOnce.Do(initPath)
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.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
}
dlg := &progressDialog{
ctx: ctx,
cmd: cmd,
max: max,
percent: true,
close: close,
lines: make(chan string),
done: make(chan struct{}),
}
go dlg.pipe(pipe)
go dlg.wait(extra, out)
return dlg, nil
}
package zenity
// List displays the list dialog.
//
// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
// WindowIcon, Attach, Modal, RadioList, DefaultItems, DisallowEmpty.
//
// May return: ErrCanceled, ErrExtraButton, ErrUnsupported.
func List(text string, items []string, options ...Option) (string, error) {
return list(text, items, applyOptions(options))
}
// ListItems displays the list dialog.
//
// May return: ErrCanceled, ErrUnsupported.
func ListItems(text string, items ...string) (string, error) {
return List(text, items)
}
// ListMultiple displays the list dialog, allowing multiple items to be selected.
//
// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
// WindowIcon, Attach, Modal, CheckList, DefaultItems, DisallowEmpty.
//
// May return: ErrCanceled, ErrExtraButton, ErrUnsupported.
func ListMultiple(text string, items []string, options ...Option) ([]string, error) {
return listMultiple(text, items, applyOptions(options))
}
// ListMultipleItems displays the list dialog, allowing multiple items to be selected.
//
// May return: ErrCanceled, ErrUnsupported.
func ListMultipleItems(text string, items ...string) ([]string, error) {
return ListMultiple(text, items, CheckList())
}
// CheckList returns an Option to show check boxes (Unix only).
func CheckList() Option {
return funcOption(func(o *options) { o.listKind = checkListKind })
}
// RadioList returns an Option to show radio boxes (Unix only).
func RadioList() Option {
return funcOption(func(o *options) { o.listKind = radioListKind })
}
type listKind int
const (
basicListKind listKind = iota
checkListKind
radioListKind
)
// MidSearch returns an Option to change list search to find text in the middle,
// not on the beginning (Unix only).
func MidSearch() Option {
return funcOption(func(o *options) { o.midSearch = true })
}
// DefaultItems returns an Option to set the items to initially select (Windows and macOS only).
func DefaultItems(items ...string) Option {
return funcOption(func(o *options) { o.defaultItems = items })
}
// DisallowEmpty returns an Option to not allow zero items to be selected (Windows and macOS only).
func DisallowEmpty() Option {
return funcOption(func(o *options) { o.disallowEmpty = true })
}
//go:build !windows && !darwin
package zenity
import "github.com/ncruces/zenity/internal/zenutil"
func list(text string, items []string, opts options) (string, error) {
args := []string{"--list", "--hide-header", "--text", text}
args = appendGeneral(args, opts)
args = appendButtons(args, opts)
args = appendWidthHeight(args, opts)
args = appendWindowIcon(args, opts)
if opts.listKind == radioListKind {
args = append(args, "--radiolist", "--column=", "--column=")
for _, i := range items {
args = append(args, "", i)
}
} else {
args = append(args, "--column=")
args = append(args, items...)
}
if opts.midSearch {
args = append(args, "--mid-search")
}
out, err := zenutil.Run(opts.ctx, args)
return strResult(opts, out, err)
}
func listMultiple(text string, items []string, opts options) ([]string, error) {
args := []string{"--list", "--hide-header", "--text", text, "--multiple", "--separator", zenutil.Separator}
args = appendGeneral(args, opts)
args = appendButtons(args, opts)
args = appendWidthHeight(args, opts)
args = appendWindowIcon(args, opts)
if opts.listKind == checkListKind {
args = append(args, "--checklist", "--column=", "--column=")
for _, i := range items {
args = append(args, "", i)
}
} else {
args = append(args, "--column=")
args = append(args, items...)
}
out, err := zenutil.Run(opts.ctx, args)
return lstResult(opts, out, err)
}
package zenity
// Question displays the question dialog.
//
// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
// Icon, WindowIcon, Attach, Modal, NoWrap, Ellipsize, DefaultCancel.
//
// May return: ErrCanceled, ErrExtraButton.
func Question(text string, options ...Option) error {
return message(questionKind, text, applyOptions(options))
}
// Info displays the info dialog.
//
// Valid options: Title, Width, Height, OKLabel, ExtraButton,
// Icon, WindowIcon, Attach, Modal, NoWrap, Ellipsize.
//
// May return: ErrCanceled, ErrExtraButton.
func Info(text string, options ...Option) error {
return message(infoKind, text, applyOptions(options))
}
// Warning displays the warning dialog.
//
// Valid options: Title, Width, Height, OKLabel, ExtraButton,
// Icon, WindowIcon, Attach, Modal, NoWrap, Ellipsize.
//
// May return: ErrCanceled, ErrExtraButton.
func Warning(text string, options ...Option) error {
return message(warningKind, text, applyOptions(options))
}
// Error displays the error dialog.
//
// Valid options: Title, Width, Height, OKLabel, ExtraButton,
// Icon, WindowIcon, Attach, Modal, NoWrap, Ellipsize.
//
// May return: ErrCanceled, ErrExtraButton.
func Error(text string, options ...Option) error {
return message(errorKind, text, applyOptions(options))
}
type messageKind int
const (
questionKind messageKind = iota
infoKind
warningKind
errorKind
)
// NoWrap returns an Option to disable text wrapping (Unix only).
func NoWrap() Option {
return funcOption(func(o *options) { o.noWrap = true })
}
// Ellipsize returns an Option to enable ellipsizing in the dialog text (Unix only).
func Ellipsize() Option {
return funcOption(func(o *options) { o.ellipsize = true })
}
//go:build !windows && !darwin
package zenity
import "github.com/ncruces/zenity/internal/zenutil"
func message(kind messageKind, text string, opts options) error {
args := []string{"--text", text, "--no-markup"}
switch kind {
case questionKind:
args = append(args, "--question")
case infoKind:
args = append(args, "--info")
case warningKind:
args = append(args, "--warning")
case errorKind:
args = append(args, "--error")
}
args = appendGeneral(args, opts)
args = appendButtons(args, opts)
args = appendWidthHeight(args, opts)
args = appendWindowIcon(args, opts)
if opts.noWrap {
args = append(args, "--no-wrap")
}
if opts.ellipsize {
args = append(args, "--ellipsize")
}
if opts.defaultCancel {
args = append(args, "--default-cancel")
}
switch opts.icon {
case ErrorIcon:
args = append(args, "--icon-name=dialog-error")
case WarningIcon:
args = append(args, "--icon-name=dialog-warning")
case InfoIcon:
args = append(args, "--icon-name=dialog-information")
case QuestionIcon:
args = append(args, "--icon-name=dialog-question")
case PasswordIcon:
args = append(args, "--icon-name=dialog-password")
case NoIcon:
args = append(args, "--icon-name=")
}
if i, ok := opts.icon.(string); ok {
args = append(args, "--icon-name", i)
}
out, err := zenutil.Run(opts.ctx, args)
_, err = strResult(opts, out, err)
return err
}
package zenity
// Notify displays a notification.
//
// Valid options: Title, Icon.
func Notify(text string, options ...Option) error {
return notify(text, applyOptions(options))
}
//go:build !windows && !darwin
package zenity
import "github.com/ncruces/zenity/internal/zenutil"
func notify(text string, opts options) error {
args := []string{"--notification", "--text", text}
args = appendGeneral(args, opts)
switch opts.icon {
case ErrorIcon:
args = append(args, "--window-icon=dialog-error")
case WarningIcon:
args = append(args, "--window-icon=dialog-warning")
case InfoIcon:
args = append(args, "--window-icon=dialog-information")
case QuestionIcon:
args = append(args, "--window-icon=dialog-question")
case PasswordIcon:
args = append(args, "--window-icon=dialog-password")
case NoIcon:
args = append(args, "--window-icon=")
}
if i, ok := opts.icon.(string); ok {
args = append(args, "--window-icon", i)
}
_, err := zenutil.Run(opts.ctx, args)
if err != nil {
return err
}
return nil
}
package zenity
// Progress displays the progress indication dialog.
//
// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
// Icon, WindowIcon, Attach, Modal, MaxValue, Pulsate, NoCancel, TimeRemaining.
//
// May return: ErrUnsupported.
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
// Complete marks the task completed.
Complete() error
// Close closes the dialog.
Close() error
// Done returns a channel that is closed when the dialog is closed.
Done() <-chan struct{}
}
// MaxValue returns an Option to set the maximum value.
// The default maximum 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 (Windows and Unix only).
func NoCancel() Option {
return funcOption(func(o *options) { o.noCancel = true })
}
// AutoClose returns an Option to dismiss the dialog when 100% has been reached.
func AutoClose() Option {
return funcOption(func(o *options) { o.autoClose = 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 })
}
//go:build !windows && !darwin
package zenity
import "github.com/ncruces/zenity/internal/zenutil"
func progress(opts options) (ProgressDialog, error) {
args := []string{"--progress"}
args = appendGeneral(args, opts)
args = appendButtons(args, opts)
args = appendWidthHeight(args, opts)
args = appendWindowIcon(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.autoClose {
args = append(args, "--auto-close")
}
if opts.timeRemaining {
args = append(args, "--time-remaining")
}
return zenutil.RunProgress(opts.ctx, opts.maxValue, opts.autoClose, opts.extraButton, args)
}
package zenity
// Password displays the password dialog.
//
// Valid options: Title, OKLabel, CancelLabel, ExtraButton,
// WindowIcon, Attach, Modal, Username.
//
// May return: ErrCanceled, ErrExtraButton.
func Password(options ...Option) (usr string, pwd string, err error) {
return password(applyOptions(options))
}
// Username returns an Option to display the username.
func Username() Option {
return funcOption(func(o *options) { o.username = true })
}
//go:build !windows && !darwin
package zenity
import "github.com/ncruces/zenity/internal/zenutil"
func password(opts options) (string, string, error) {
args := []string{"--password"}
args = appendGeneral(args, opts)
args = appendButtons(args, opts)
if opts.username {
args = append(args, "--username")
}
out, err := zenutil.Run(opts.ctx, args)
return pwdResult("|", opts, out, err)
}
package zenity
import (
"bytes"
"encoding/xml"
"fmt"
"os/exec"
"strconv"
"strings"
"github.com/ncruces/zenity/internal/zenutil"
)
func quoteAccelerators(text string) string {
return strings.ReplaceAll(text, "&", "&&")
}
func quoteMnemonics(text string) string {
return strings.ReplaceAll(text, "_", "__")
}
func quoteMarkup(text string) string {
var res strings.Builder
err := xml.EscapeText(&res, []byte(text))
if err != nil {
return text
}
return res.String()
}
func appendGeneral(args []string, opts options) []string {
if opts.title != nil {
args = append(args, "--title", *opts.title)
}
if id, ok := opts.attach.(int); ok {
args = append(args, "--attach", strconv.Itoa(id))
}
if opts.modal {
args = append(args, "--modal")
}
if opts.display != "" {
args = append(args, "--display", opts.display)
}
if opts.class != "" {
args = append(args, "--class", opts.class)
}
if opts.name != "" {
args = append(args, "--name", opts.name)
}
return args
}
func appendButtons(args []string, opts options) []string {
if opts.okLabel != nil {
args = append(args, "--ok-label", *opts.okLabel)
}
if opts.cancelLabel != nil {
args = append(args, "--cancel-label", *opts.cancelLabel)
}
if opts.extraButton != nil {
args = append(args, "--extra-button", *opts.extraButton)
}
return args
}
func appendWidthHeight(args []string, opts options) []string {
if opts.width > 0 {
args = append(args, "--width", strconv.FormatUint(uint64(opts.width), 10))
}
if opts.height > 0 {
args = append(args, "--height", strconv.FormatUint(uint64(opts.height), 10))
}
return args
}
func appendWindowIcon(args []string, opts options) []string {
switch opts.windowIcon {
case ErrorIcon:
args = append(args, "--window-icon=error")
case WarningIcon:
args = append(args, "--window-icon=warning")
case InfoIcon:
args = append(args, "--window-icon=info")
case QuestionIcon:
args = append(args, "--window-icon=question")
}
if i, ok := opts.windowIcon.(string); ok {
args = append(args, "--window-icon", i)
}
return args
}
func strResult(opts options, out []byte, err error) (string, error) {
out = bytes.TrimSuffix(out, []byte{'\n'})
if eerr, ok := err.(*exec.ExitError); ok {
if eerr.ExitCode() == 1 {
if opts.extraButton != nil && *opts.extraButton == string(out) {
return "", ErrExtraButton
}
return "", ErrCanceled
}
return "", fmt.Errorf("%w: %s", eerr, eerr.Stderr)
}
if err != nil {
return "", err
}
return string(out), nil
}
func lstResult(opts options, out []byte, err error) ([]string, error) {
str, err := strResult(opts, out, err)
if err != nil {
return nil, err
}
if len(out) == 0 {
return []string{}, nil
}
return strings.Split(str, zenutil.Separator), nil
}
func pwdResult(sep string, opts options, out []byte, err error) (string, string, error) {
str, err := strResult(opts, out, err)
if opts.username {
usr, pwd, _ := strings.Cut(str, sep)
return usr, pwd, err
}
return "", str, err
}
// Package zenity provides cross-platform access to simple dialogs that interact
// graphically with the user.
//
// It is inspired by, and closely follows the API of, the zenity program, which
// it uses to provide the functionality on various Unixes. See:
//
// https://help.gnome.org/users/zenity/stable/
//
// This package does not require cgo, and it does not impose any threading or
// initialization requirements.
package zenity
import (
"context"
"image/color"
"time"
"github.com/ncruces/zenity/internal/zenutil"
)
func ptr[T any](v T) *T { return &v }
// ErrCanceled is returned when the cancel button is pressed,
// or window functions are used to close the dialog.
const ErrCanceled = zenutil.ErrCanceled
// ErrExtraButton is returned when the extra button is pressed.
const ErrExtraButton = zenutil.ErrExtraButton
// ErrUnsupported is returned when a combination of options is not supported.
const ErrUnsupported = zenutil.ErrUnsupported
// IsAvailable reports whether dependencies of the package are installed.
// It always returns true on Windows and macOS.
func IsAvailable() bool {
return isAvailable()
}
type options struct {
// General options
title *string
width uint
height uint
okLabel *string
cancelLabel *string
extraButton *string
defaultCancel bool
icon any
windowIcon any
attach any
modal bool
display string
class string
name string
// Message options
noWrap bool
ellipsize bool
// Entry options
entryText string
hideText bool
username bool
// List options
listKind listKind
midSearch bool
disallowEmpty bool
defaultItems []string
// Calendar options
time *time.Time
// File selection options
directory bool
confirmOverwrite bool
confirmCreate bool
showHidden bool
filename string
fileFilters FileFilters
// Color selection options
color color.Color
showPalette bool
// Progress indication options
maxValue int
noCancel bool
autoClose bool
timeRemaining bool
// Context for timeout
ctx context.Context
}
// An Option is an argument passed to dialog functions to customize their
// behavior.
type Option interface {
apply(*options)
}
type funcOption func(*options)
func (f funcOption) apply(o *options) { f(o) }
func applyOptions(options []Option) (res options) {
for _, o := range options {
o.apply(&res)
}
return
}
// Title returns an Option to set the dialog title.
func Title(title string) Option {
return funcOption(func(o *options) { o.title = &title })
}
// Width returns an Option to set the dialog width (Unix only).
func Width(width uint) Option {
return funcOption(func(o *options) {
o.width = width
})
}
// Height returns an Option to set the dialog height (Unix only).
func Height(height uint) Option {
return funcOption(func(o *options) {
o.height = height
})
}
// OKLabel returns an Option to set the label of the OK button.
func OKLabel(ok string) Option {
return funcOption(func(o *options) { o.okLabel = &ok })
}
// CancelLabel returns an Option to set the label of the Cancel button.
func CancelLabel(cancel string) Option {
return funcOption(func(o *options) { o.cancelLabel = &cancel })
}
// ExtraButton returns an Option to add one extra button.
func ExtraButton(extra string) Option {
return funcOption(func(o *options) { o.extraButton = &extra })
}
// DefaultCancel returns an Option to give the Cancel button focus by default.
func DefaultCancel() Option {
return funcOption(func(o *options) { o.defaultCancel = true })
}
// DialogIcon is an Option that sets the dialog icon.
type DialogIcon int
func (i DialogIcon) apply(o *options) {
o.icon = i
}
// The stock dialog icons.
const (
ErrorIcon DialogIcon = iota
WarningIcon
InfoIcon
QuestionIcon
PasswordIcon
NoIcon
)
// Icon returns an Option to set the dialog icon.
//
// Icon accepts a DialogIcon, or a string.
// The string can be a GTK icon name (Unix), or a file path (Windows and macOS).
// Supported file formats depend on the plaftorm, but PNG should be cross-platform.
func Icon(icon any) Option {
switch icon.(type) {
case DialogIcon, string:
default:
panic("interface conversion: expected string or DialogIcon")
}
return funcOption(func(o *options) { o.icon = icon })
}
// WindowIcon returns an Option to set the window icon.
//
// WindowIcon accepts a DialogIcon, or a string file path.
// Supported file formats depend on the plaftorm, but PNG should be cross-platform.
func WindowIcon(icon any) Option {
switch icon.(type) {
case DialogIcon, string:
default:
panic("interface conversion: expected string or DialogIcon")
}
return funcOption(func(o *options) { o.windowIcon = icon })
}
// Attach returns an Option to set the parent window to attach to.
//
// Attach accepts:
// - a window id (int) on Unix
// - a window handle (~uintptr) on Windows
// - an application name (string) or process id (int) on macOS
func Attach(id any) Option {
return attach(id)
}
// Modal returns an Option to set the modal hint.
func Modal() Option {
return funcOption(func(o *options) { o.modal = true })
}
// Display returns an Option to set the X display to use (Unix only).
func Display(display string) Option {
return funcOption(func(o *options) { o.display = display })
}
// ClassHint returns an Option to set the program name and class
// as used by the window manager (Unix only).
func ClassHint(name, class string) Option {
return funcOption(func(o *options) {
if name != "" {
o.name = name
}
if class != "" {
o.class = class
}
})
}
// Context returns an Option to set a Context that can dismiss the dialog.
//
// Dialogs dismissed by ctx return ctx.Err().
func Context(ctx context.Context) Option {
return funcOption(func(o *options) { o.ctx = ctx })
}
//go:build !windows && !darwin
package zenity
import "github.com/ncruces/zenity/internal/zenutil"
func isAvailable() bool { return zenutil.IsAvailable() }
func attach(id any) Option {
return funcOption(func(o *options) { o.attach = id.(int) })
}