From c4f236bdf9614b52e436e2365b408cfdc3ebdae8 Mon Sep 17 00:00:00 2001 From: Milan Nikolic Date: Wed, 24 Jun 2026 13:16:09 +0200 Subject: [PATCH] Add Command button, issue #33 --- cbconvert.go | 50 +++++++++++++++++++++++++++++++++++++++ cbconvert_test.go | 21 ++++++++++++++++ cmd/cbconvert-gui/main.go | 38 +++++++++++++++++++++++++++-- 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/cbconvert.go b/cbconvert.go index 49a53d5..3540268 100644 --- a/cbconvert.go +++ b/cbconvert.go @@ -153,6 +153,56 @@ func New(o Options) *Converter { return c } +// Args returns the non-default options as cbconvert convert command-line flags. +func (o Options) Args() []string { + def := NewOptions() + + var args []string + + str := func(name, val, dflt string) { + if val != dflt { + args = append(args, "--"+name, val) + } + } + num := func(name string, val, dflt int) { + if val != dflt { + args = append(args, "--"+name, strconv.Itoa(val)) + } + } + flag := func(name string, val bool) { + if val { + args = append(args, "--"+name) + } + } + + num("width", o.Width, def.Width) + num("height", o.Height, def.Height) + flag("fit", o.Fit) + str("format", o.Format, def.Format) + str("archive", o.Archive, def.Archive) + num("zip-level", o.ZipLevel, def.ZipLevel) + num("quality", o.Quality, def.Quality) + num("effort", o.Effort, def.Effort) + flag("lossless", o.Lossless) + flag("combine", o.Combine) + str("outfile", o.OutFile, def.OutFile) + num("filter", o.Filter, def.Filter) + flag("no-cover", o.NoCover) + flag("no-rgb", o.NoRGB) + flag("no-nonimage", o.NoNonImage) + flag("no-convert", o.NoConvert) + str("suffix", o.Suffix, def.Suffix) + flag("grayscale", o.Grayscale) + num("rotate", o.Rotate, def.Rotate) + num("brightness", o.Brightness, def.Brightness) + num("contrast", o.Contrast, def.Contrast) + flag("recursive", o.Recursive) + str("outdir", o.OutDir, def.OutDir) + num("size", o.Size, def.Size) + + return args +} + // Cancel cancels the operation. func (c *Converter) Cancel() { if c.OnCancel != nil { diff --git a/cbconvert_test.go b/cbconvert_test.go index 6a0600b..67b55de 100644 --- a/cbconvert_test.go +++ b/cbconvert_test.go @@ -103,6 +103,27 @@ func TestThumbnail(t *testing.T) { } } +func TestArgs(t *testing.T) { + opts := NewOptions() + if got := opts.Args(); len(got) != 0 { + t.Errorf("defaults should emit no flags, got %v", got) + } + + opts.Format = "webp" + opts.Quality = 90 + opts.Effort = 4 + opts.Lossless = true + opts.Width = 1200 + opts.Grayscale = true + opts.OutDir = "/out" + + got := strings.Join(opts.Args(), " ") + want := "--width 1200 --format webp --quality 90 --effort 4 --lossless --grayscale --outdir /out" + if got != want { + t.Errorf("Args() = %q, want %q", got, want) + } +} + func TestConvertResize(t *testing.T) { tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc") if err != nil { diff --git a/cmd/cbconvert-gui/main.go b/cmd/cbconvert-gui/main.go index 52e63dd..deb7044 100644 --- a/cmd/cbconvert-gui/main.go +++ b/cmd/cbconvert-gui/main.go @@ -310,6 +310,34 @@ func setActive() { } } +// shellArg quotes a command-line argument that contains whitespace. +func shellArg(s string) string { + if strings.ContainsAny(s, " \t") { + return `"` + strings.ReplaceAll(s, `"`, `\"`) + `"` + } + + return s +} + +func commandLine() string { + parts := append([]string{"cbconvert", "convert"}, options().Args()...) + for _, file := range files { + parts = append(parts, file.Path) + } + + for i, p := range parts { + parts[i] = shellArg(p) + } + + return strings.Join(parts, " ") +} + +func onCommand(iup.Ihandle) int { + iup.GetText("Command Line", commandLine(), -1) + + return iup.DEFAULT +} + // zipLevel maps the compression dropdown selection to Options.ZipLevel. func zipLevel(value string) int { switch value { @@ -1083,11 +1111,16 @@ func buttons() iup.Ihandle { SetAttribute("TIP", "Save current settings to a profile"). SetCallback("ACTION", iup.ActionFunc(onSave)) - profile := iup.List().SetAttributes("DROPDOWN=YES, EXPAND=HORIZONTAL").SetHandle("Profile"). + command := iup.Button("Command").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). + SetAttribute("TIP", "Show the equivalent command line"). + SetCallback("ACTION", iup.ActionFunc(onCommand)) + + profile := iup.List().SetAttributes("DROPDOWN=YES").SetHandle("Profile"). SetAttribute("TIP", "Select a settings profile"). SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(onProfileSelect)) - iup.Normalizer(addFiles, addDir, remove, removeAll, thumbnail, cover, convert, reset, save).SetAttribute("NORMALIZE", "BOTH") + iup.Normalizer(addFiles, addDir, remove, removeAll, thumbnail, cover, convert, reset, save, command).SetAttribute("NORMALIZE", "BOTH") + iup.Normalizer(addFiles, addDir, remove, removeAll, thumbnail, cover, convert, reset, save, command, profile).SetAttribute("NORMALIZE", "HORIZONTAL") return iup.Vbox( iup.Vbox( @@ -1111,6 +1144,7 @@ func buttons() iup.Ihandle { profile, reset, save, + command, ).SetAttribute("NGAP", "2"), ).SetHandle("Buttons").SetAttributes("ALIGNMENT=ACENTER") }