Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/image-generation-saving.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,15 @@ zsh, fish and PowerShell 7. In other or unidentified shells, generation remains
available, but command printing is disabled with an explanation in the picker.

After a successful generation, the picker reopens below the saved result with
the active prompt and settings. Choices last until this invocation exits.
the same image settings and an empty prompt. It remembers the last submitted
image settings and save folder for next time; the description starts empty and
is not saved in picker preferences. Canceled edits are discarded. Settings stay
local to your user configuration folder and do not affect commands that supply flags.

Choose **Save to** to use the default folder, current directory, or another
existing folder. Tab completes folder names. Saved settings are optional: if
they cannot be written, a warning appears and generation can still continue.
Unknown or unreadable saved settings are left alone.
Explicit image flags, output formats, piped input and redirected output retain
the direct command behavior described below.

Expand Down
60 changes: 45 additions & 15 deletions pkg/custom/image_picker.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ import (
// imagePickerOptions configures the interactive image settings.
type imagePickerOptions struct {
Prompt string
// PromptSet lets an explicit empty prompt override the active draft.
PromptSet bool
// Shell selects quoting for the displayed command, never execution.
Shell string
initial *imagePickerSettings
resuming bool
Shell string
initial *imagePickerSettings
resuming bool
initialNote string
}

// imagePickerResult contains arguments for the existing images generate command.
Expand All @@ -51,6 +50,7 @@ func runImagePicker(parent context.Context, input, output *os.File, options imag
if err != nil {
return imagePickerResult{}, err
}
defer model.cancelFolderWork()
if input == nil || output == nil || !term.IsTerminal(input.Fd()) || !term.IsTerminal(output.Fd()) {
return imagePickerResult{}, errors.New("the image picker needs terminal input and output")
}
Expand Down Expand Up @@ -189,7 +189,7 @@ func (w *imagePickerOutput) Err() error {
}

type imagePickerSettings struct {
prompt, model, size, quality, background, format, count string
prompt, model, size, quality, background, format, count, outputDir string
}

type imagePicker struct {
Expand All @@ -208,6 +208,7 @@ type imagePicker struct {
dark bool
note string
result imagePickerResult
folder imagePickerFolder
}

type imagePickerRow struct {
Expand All @@ -222,12 +223,12 @@ func newImagePicker(options imagePickerOptions) (*imagePicker, error) {
}
if options.initial != nil {
m.settings = *options.initial
if options.PromptSet || options.Prompt != "" {
m.settings.prompt = options.Prompt
}
m.draft, m.cursor = []rune(m.settings.prompt), len([]rune(m.settings.prompt))
// Carry image settings forward, but require a fresh description for
// each image. Only an explicitly supplied prompt may prefill it.
m.settings.prompt = options.Prompt
}
m.shell = options.Shell
m.note = options.initialNote
return m, nil
}

Expand All @@ -243,10 +244,12 @@ func (m *imagePicker) Update(message tea.Msg) (tea.Model, tea.Cmd) {
// Cancellation can still stop a submitted action before the picker exits.
switch msg := message.(type) {
case imagePickerStopMsg:
m.cancelFolderWork()
m.result = imagePickerResult{Canceled: true, ExitCode: msg.code}
return m, tea.Quit
case tea.KeyPressMsg:
if msg.String() == "ctrl+c" {
m.cancelFolderWork()
m.result = imagePickerResult{Canceled: true}
return m, tea.Quit
}
Expand All @@ -256,24 +259,42 @@ func (m *imagePicker) Update(message tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
switch msg := message.(type) {

case imagePickerFolderMsg:
return m, m.finishFolderWork(msg)
case tea.BackgroundColorMsg:
m.dark = msg.IsDark()
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
m.clampCommandOffset()
case tea.PasteMsg:
if m.focus == "prompt" {
if m.folder.busy == "submit" || m.folder.busy == "select" {
return m, nil
}
if m.focus == "path" {
m.insertFolderPath(msg.Content)
} else if m.focus == "prompt" {
m.insertPrompt(msg.Content)
} else {
m.note = "Tab to Prompt to paste text."
}
case tea.KeyPressMsg:
key := msg.String()
if key == "esc" {
if m.page == "folder" || m.page == "path" || m.folder.busy == "submit" {
return m, m.backFolder()
}
m.focusPrompt()
return m, nil
}
if m.folder.busy == "submit" || m.folder.busy == "select" {
return m, nil
}
if m.focus == "path" {
if m.width >= 40 && m.height >= 12 {
return m, m.editFolderPath(msg)
}
return m, nil
}
// Esc immediately followed by another key can arrive as an Alt key.
// Leave menus first; no Alt shortcuts are assigned
// there. In the prompt itself, Alt+Enter still inserts a newline.
Expand All @@ -292,6 +313,7 @@ func (m *imagePicker) Update(message tea.Msg) (tea.Model, tea.Cmd) {
}
}
if key == "q" && m.focus != "prompt" {
m.cancelFolderWork()
m.result = imagePickerResult{Canceled: true}
return m, tea.Quit
}
Expand Down Expand Up @@ -363,12 +385,17 @@ func (m *imagePicker) Update(message tea.Msg) (tea.Model, tea.Cmd) {
// Esc closes any menu without applying its highlighted choice. Committed
// settings and prompt text remain intact.
func (m *imagePicker) focusPrompt() {
m.cancelFolderWork()
m.page, m.focus, m.selected, m.note = "settings", "prompt", 0, ""
}

func (m *imagePicker) rows() []imagePickerRow {
s := m.settings
switch m.page {
case "folder":
return m.folderRows()
case "path":
return m.folderCompletionRows()
case "choose":
return m.choices(m.field)
case "more":
Expand All @@ -379,7 +406,7 @@ func (m *imagePicker) rows() []imagePickerRow {
{id: "size", label: "Size", value: imagePickerSizeLabel(s.size)},
{id: "quality", label: "Quality", value: imagePickerTitle(s.quality)},
}
return append(rows, imagePickerRow{id: "more", label: "More options", value: strings.ToUpper(s.format) + " / " + s.count + " image(s)"})
return append(rows, m.folderRow(), imagePickerRow{id: "more", label: "More options", value: strings.ToUpper(s.format) + " / " + s.count + " image(s)"})
}

func (m *imagePicker) cycleFocus(backwards bool) {
Expand Down Expand Up @@ -502,6 +529,8 @@ func (m *imagePicker) apply(value string) {

func (m *imagePicker) activate(row imagePickerRow) tea.Cmd {
switch row.id {
case "folder", "folder-default", "folder-current", "folder-path", "folder-back":
return m.activateFolder(row.id)
case "choice":
m.apply(row.value)
m.page, m.selected = m.returnPage, m.returnSelected
Expand All @@ -517,8 +546,7 @@ func (m *imagePicker) activate(row imagePickerRow) tea.Cmd {
return nil
}
if m.validPrompt() {
m.result = imagePickerResult{Args: m.settings.args(), PrintOnly: row.id == "print", settings: m.settings, shell: m.shell}
return tea.Quit
return m.submitWithFolder(row.id == "print")
}
case "back":
return m.back()
Expand Down Expand Up @@ -552,6 +580,8 @@ func (m *imagePicker) validPrompt() bool {
func (m *imagePicker) back() tea.Cmd {
m.note = ""
switch m.page {
case "folder", "path":
return m.backFolder()
case "more":
m.page = "settings"
m.selected = len(m.rows()) - 1
Expand Down
3 changes: 3 additions & 0 deletions pkg/custom/image_picker_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ func (s imagePickerSettings) args() []string {
// setting changes even when the prompt tail needs to be truncated.
args := []string{"images", "generate", "--model", s.model, "--size", s.size,
"--quality", s.quality, "--output-format", s.format, "--background", s.background, "--count", s.count}
if s.outputDir != "" {
args = append(args, "--output-dir", s.outputDir)
}
return append(args, "--prompt", prompt)
}

Expand Down
Loading
Loading