chore: cleanup image component

Kujtim Hoxha created

Change summary

cspell.json                                              |   2 
internal/tui/components/dialog/filepicker.go             | 468 ----------
internal/tui/components/dialogs/filepicker/filepicker.go |  65 
internal/tui/components/image/image.go                   |  15 
internal/tui/components/image/load.go                    |  32 
5 files changed, 58 insertions(+), 524 deletions(-)

Detailed changes

cspell.json 🔗

@@ -1 +1 @@
-{"words":["opencode","charmbracelet","lipgloss","bubbletea","textinput","Focusable","lsps","Sourcegraph"],"version":"0.2","language":"en","flagWords":[]}
+{"flagWords":[],"language":"en","words":["opencode","charmbracelet","lipgloss","bubbletea","textinput","Focusable","lsps","Sourcegraph","filepicker","imageorient","rasterx","oksvg","termenv","trashhalo","lucasb","nfnt","srwiley","Lanczos"],"version":"0.2"}

internal/tui/components/dialog/filepicker.go 🔗

@@ -1,468 +0,0 @@
-package dialog
-
-// import (
-// 	"fmt"
-// 	"net/http"
-// 	"os"
-// 	"path/filepath"
-// 	"sort"
-// 	"strings"
-// 	"time"
-//
-// 	"github.com/charmbracelet/bubbles/v2/key"
-// 	"github.com/charmbracelet/bubbles/v2/textinput"
-// 	"github.com/charmbracelet/bubbles/v2/viewport"
-// 	tea "github.com/charmbracelet/bubbletea/v2"
-// 	"github.com/charmbracelet/lipgloss/v2"
-// 	"github.com/opencode-ai/opencode/internal/app"
-// 	"github.com/opencode-ai/opencode/internal/logging"
-// 	"github.com/opencode-ai/opencode/internal/message"
-// 	"github.com/opencode-ai/opencode/internal/tui/image"
-// 	"github.com/opencode-ai/opencode/internal/tui/styles"
-// 	"github.com/opencode-ai/opencode/internal/tui/util"
-// )
-//
-// const (
-// 	maxAttachmentSize = int64(5 * 1024 * 1024) // 5MB
-// 	downArrow         = "down"
-// 	upArrow           = "up"
-// )
-//
-// type FilePrickerKeyMap struct {
-// 	Enter          key.Binding
-// 	Down           key.Binding
-// 	Up             key.Binding
-// 	Forward        key.Binding
-// 	Backward       key.Binding
-// 	OpenFilePicker key.Binding
-// 	Esc            key.Binding
-// 	InsertCWD      key.Binding
-// }
-//
-// var filePickerKeyMap = FilePrickerKeyMap{
-// 	Enter: key.NewBinding(
-// 		key.WithKeys("enter"),
-// 		key.WithHelp("enter", "select file/enter directory"),
-// 	),
-// 	Down: key.NewBinding(
-// 		key.WithKeys("j", downArrow),
-// 		key.WithHelp("↓/j", "down"),
-// 	),
-// 	Up: key.NewBinding(
-// 		key.WithKeys("k", upArrow),
-// 		key.WithHelp("↑/k", "up"),
-// 	),
-// 	Forward: key.NewBinding(
-// 		key.WithKeys("l"),
-// 		key.WithHelp("l", "enter directory"),
-// 	),
-// 	Backward: key.NewBinding(
-// 		key.WithKeys("h", "backspace"),
-// 		key.WithHelp("h/backspace", "go back"),
-// 	),
-// 	OpenFilePicker: key.NewBinding(
-// 		key.WithKeys("ctrl+f"),
-// 		key.WithHelp("ctrl+f", "open file picker"),
-// 	),
-// 	Esc: key.NewBinding(
-// 		key.WithKeys("esc"),
-// 		key.WithHelp("esc", "close/exit"),
-// 	),
-// 	InsertCWD: key.NewBinding(
-// 		key.WithKeys("i"),
-// 		key.WithHelp("i", "manual path input"),
-// 	),
-// }
-//
-// type filepickerCmp struct {
-// 	basePath       string
-// 	width          int
-// 	height         int
-// 	cursor         int
-// 	err            error
-// 	cursorChain    stack
-// 	viewport       viewport.Model
-// 	dirs           []os.DirEntry
-// 	cwdDetails     *DirNode
-// 	selectedFile   string
-// 	cwd            textinput.Model
-// 	ShowFilePicker bool
-// 	app            *app.App
-// }
-//
-// type DirNode struct {
-// 	parent    *DirNode
-// 	child     *DirNode
-// 	directory string
-// }
-// type stack []int
-//
-// func (s stack) Push(v int) stack {
-// 	return append(s, v)
-// }
-//
-// func (s stack) Pop() (stack, int) {
-// 	l := len(s)
-// 	return s[:l-1], s[l-1]
-// }
-//
-// type AttachmentAddedMsg struct {
-// 	Attachment message.Attachment
-// }
-//
-// func (f *filepickerCmp) Init() tea.Cmd {
-// 	return nil
-// }
-//
-// func (f *filepickerCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
-// 	var cmd tea.Cmd
-// 	switch msg := msg.(type) {
-// 	case tea.WindowSizeMsg:
-// 		f.width = 60
-// 		f.height = 20
-// 		f.viewport.SetWidth(80)
-// 		f.viewport.SetHeight(22)
-// 		f.cursor = 0
-// 		f.getCurrentFileBelowCursor()
-// 	case tea.KeyPressMsg:
-// 		if f.cwd.Focused() {
-// 			f.cwd, cmd = f.cwd.Update(msg)
-// 		}
-// 		switch {
-// 		case key.Matches(msg, filePickerKeyMap.InsertCWD):
-// 			f.cwd.Focus()
-// 			return f, cmd
-// 		case key.Matches(msg, filePickerKeyMap.Esc):
-// 			if f.cwd.Focused() {
-// 				f.cwd.Blur()
-// 			}
-// 		case key.Matches(msg, filePickerKeyMap.Down):
-// 			if !f.cwd.Focused() || msg.String() == downArrow {
-// 				if f.cursor < len(f.dirs)-1 {
-// 					f.cursor++
-// 					f.getCurrentFileBelowCursor()
-// 				}
-// 			}
-// 		case key.Matches(msg, filePickerKeyMap.Up):
-// 			if !f.cwd.Focused() || msg.String() == upArrow {
-// 				if f.cursor > 0 {
-// 					f.cursor--
-// 					f.getCurrentFileBelowCursor()
-// 				}
-// 			}
-// 		case key.Matches(msg, filePickerKeyMap.Enter):
-// 			var path string
-// 			var isPathDir bool
-// 			if f.cwd.Focused() {
-// 				path = f.cwd.Value()
-// 				fileInfo, err := os.Stat(path)
-// 				if err != nil {
-// 					logging.ErrorPersist("Invalid path")
-// 					return f, cmd
-// 				}
-// 				isPathDir = fileInfo.IsDir()
-// 			} else {
-// 				path = filepath.Join(f.cwdDetails.directory, "/", f.dirs[f.cursor].Name())
-// 				isPathDir = f.dirs[f.cursor].IsDir()
-// 			}
-// 			if isPathDir {
-// 				newWorkingDir := DirNode{parent: f.cwdDetails, directory: path}
-// 				f.cwdDetails.child = &newWorkingDir
-// 				f.cwdDetails = f.cwdDetails.child
-// 				f.cursorChain = f.cursorChain.Push(f.cursor)
-// 				f.dirs = readDir(f.cwdDetails.directory, false)
-// 				f.cursor = 0
-// 				f.cwd.SetValue(f.cwdDetails.directory)
-// 				f.getCurrentFileBelowCursor()
-// 			} else {
-// 				f.selectedFile = path
-// 				return f.addAttachmentToMessage()
-// 			}
-// 		case key.Matches(msg, filePickerKeyMap.Esc):
-// 			if !f.cwd.Focused() {
-// 				f.cursorChain = make(stack, 0)
-// 				f.cursor = 0
-// 			} else {
-// 				f.cwd.Blur()
-// 			}
-// 		case key.Matches(msg, filePickerKeyMap.Forward):
-// 			if !f.cwd.Focused() {
-// 				if f.dirs[f.cursor].IsDir() {
-// 					path := filepath.Join(f.cwdDetails.directory, "/", f.dirs[f.cursor].Name())
-// 					newWorkingDir := DirNode{parent: f.cwdDetails, directory: path}
-// 					f.cwdDetails.child = &newWorkingDir
-// 					f.cwdDetails = f.cwdDetails.child
-// 					f.cursorChain = f.cursorChain.Push(f.cursor)
-// 					f.dirs = readDir(f.cwdDetails.directory, false)
-// 					f.cursor = 0
-// 					f.cwd.SetValue(f.cwdDetails.directory)
-// 					f.getCurrentFileBelowCursor()
-// 				}
-// 			}
-// 		case key.Matches(msg, filePickerKeyMap.Backward):
-// 			if !f.cwd.Focused() {
-// 				if len(f.cursorChain) != 0 && f.cwdDetails.parent != nil {
-// 					f.cursorChain, f.cursor = f.cursorChain.Pop()
-// 					f.cwdDetails = f.cwdDetails.parent
-// 					f.cwdDetails.child = nil
-// 					f.dirs = readDir(f.cwdDetails.directory, false)
-// 					f.cwd.SetValue(f.cwdDetails.directory)
-// 					f.getCurrentFileBelowCursor()
-// 				}
-// 			}
-// 		case key.Matches(msg, filePickerKeyMap.OpenFilePicker):
-// 			f.dirs = readDir(f.cwdDetails.directory, false)
-// 			f.cursor = 0
-// 			f.getCurrentFileBelowCursor()
-// 		}
-// 	}
-// 	return f, cmd
-// }
-//
-// func (f *filepickerCmp) addAttachmentToMessage() (tea.Model, tea.Cmd) {
-// 	// modeInfo := GetSelectedModel(config.Get())
-// 	// if !modeInfo.SupportsAttachments {
-// 	// 	logging.ErrorPersist(fmt.Sprintf("Model %s doesn't support attachments", modeInfo.Name))
-// 	// 	return f, nil
-// 	// }
-//
-// 	selectedFilePath := f.selectedFile
-// 	if !isExtSupported(selectedFilePath) {
-// 		logging.ErrorPersist("Unsupported file")
-// 		return f, nil
-// 	}
-//
-// 	isFileLarge, err := image.ValidateFileSize(selectedFilePath, maxAttachmentSize)
-// 	if err != nil {
-// 		logging.ErrorPersist("unable to read the image")
-// 		return f, nil
-// 	}
-// 	if isFileLarge {
-// 		logging.ErrorPersist("file too large, max 5MB")
-// 		return f, nil
-// 	}
-//
-// 	content, err := os.ReadFile(selectedFilePath)
-// 	if err != nil {
-// 		logging.ErrorPersist("Unable read selected file")
-// 		return f, nil
-// 	}
-//
-// 	mimeBufferSize := min(512, len(content))
-// 	mimeType := http.DetectContentType(content[:mimeBufferSize])
-// 	fileName := filepath.Base(selectedFilePath)
-// 	attachment := message.Attachment{FilePath: selectedFilePath, FileName: fileName, MimeType: mimeType, Content: content}
-// 	f.selectedFile = ""
-// 	return f, util.CmdHandler(AttachmentAddedMsg{attachment})
-// }
-//
-// func (f *filepickerCmp) View() tea.View {
-// 	t := styles.CurrentTheme()
-// 	baseStyle := t.S().Base
-// 	const maxVisibleDirs = 20
-// 	const maxWidth = 80
-//
-// 	adjustedWidth := maxWidth
-// 	for _, file := range f.dirs {
-// 		if len(file.Name()) > adjustedWidth-4 { // Account for padding
-// 			adjustedWidth = len(file.Name()) + 4
-// 		}
-// 	}
-// 	adjustedWidth = max(30, min(adjustedWidth, f.width-15)) + 1
-//
-// 	files := make([]string, 0, maxVisibleDirs)
-// 	startIdx := 0
-//
-// 	if len(f.dirs) > maxVisibleDirs {
-// 		halfVisible := maxVisibleDirs / 2
-// 		if f.cursor >= halfVisible && f.cursor < len(f.dirs)-halfVisible {
-// 			startIdx = f.cursor - halfVisible
-// 		} else if f.cursor >= len(f.dirs)-halfVisible {
-// 			startIdx = len(f.dirs) - maxVisibleDirs
-// 		}
-// 	}
-//
-// 	endIdx := min(startIdx+maxVisibleDirs, len(f.dirs))
-//
-// 	for i := startIdx; i < endIdx; i++ {
-// 		file := f.dirs[i]
-// 		itemStyle := t.S().Text.Width(adjustedWidth)
-//
-// 		if i == f.cursor {
-// 			itemStyle = itemStyle.
-// 				Background(t.Primary).
-// 				Bold(true)
-// 		}
-// 		filename := file.Name()
-//
-// 		if len(filename) > adjustedWidth-4 {
-// 			filename = filename[:adjustedWidth-7] + "..."
-// 		}
-// 		if file.IsDir() {
-// 			filename = filename + "/"
-// 		}
-// 		// No need to reassign filename if it's not changing
-//
-// 		files = append(files, itemStyle.Padding(0, 1).Render(filename))
-// 	}
-//
-// 	// Pad to always show exactly 21 lines
-// 	for len(files) < maxVisibleDirs {
-// 		files = append(files, baseStyle.Width(adjustedWidth).Render(""))
-// 	}
-//
-// 	currentPath := baseStyle.
-// 		Height(1).
-// 		Width(adjustedWidth).
-// 		Render(f.cwd.View())
-//
-// 	viewportstyle := baseStyle.
-// 		Width(f.viewport.Width()).
-// 		Border(lipgloss.RoundedBorder()).
-// 		BorderForeground(t.BorderFocus).
-// 		Padding(2).
-// 		Render(f.viewport.View())
-// 	var insertExitText string
-// 	if f.IsCWDFocused() {
-// 		insertExitText = "Press esc to exit typing path"
-// 	} else {
-// 		insertExitText = "Press i to start typing path"
-// 	}
-//
-// 	content := lipgloss.JoinVertical(
-// 		lipgloss.Left,
-// 		currentPath,
-// 		baseStyle.Width(adjustedWidth).Render(""),
-// 		baseStyle.Width(adjustedWidth).Render(lipgloss.JoinVertical(lipgloss.Left, files...)),
-// 		baseStyle.Width(adjustedWidth).Render(""),
-// 		t.S().Muted.Width(adjustedWidth).Render(insertExitText),
-// 	)
-//
-// 	f.cwd.SetValue(f.cwd.Value())
-// 	contentStyle := baseStyle.Padding(1, 2).
-// 		Border(lipgloss.RoundedBorder()).
-// 		BorderForeground(t.BorderFocus).
-// 		Width(lipgloss.Width(content) + 4)
-//
-// 	return tea.NewView(
-// 		lipgloss.JoinHorizontal(lipgloss.Center, contentStyle.Render(content), viewportstyle),
-// 	)
-// }
-//
-// type FilepickerCmp interface {
-// 	util.Model
-// 	ToggleFilepicker(showFilepicker bool)
-// 	IsCWDFocused() bool
-// }
-//
-// func (f *filepickerCmp) ToggleFilepicker(showFilepicker bool) {
-// 	f.ShowFilePicker = showFilepicker
-// }
-//
-// func (f *filepickerCmp) IsCWDFocused() bool {
-// 	return f.cwd.Focused()
-// }
-//
-// func NewFilepickerCmp(app *app.App) FilepickerCmp {
-// 	homepath, err := os.UserHomeDir()
-// 	if err != nil {
-// 		logging.Error("error loading user files")
-// 		return nil
-// 	}
-// 	baseDir := DirNode{parent: nil, directory: homepath}
-// 	dirs := readDir(homepath, false)
-// 	viewport := viewport.New()
-// 	currentDirectory := textinput.New()
-// 	currentDirectory.CharLimit = 200
-// 	currentDirectory.SetWidth(44)
-// 	currentDirectory.Cursor().Blink = true
-// 	currentDirectory.SetValue(baseDir.directory)
-// 	return &filepickerCmp{cwdDetails: &baseDir, dirs: dirs, cursorChain: make(stack, 0), viewport: viewport, cwd: currentDirectory, app: app}
-// }
-//
-// func (f *filepickerCmp) getCurrentFileBelowCursor() {
-// 	if len(f.dirs) == 0 || f.cursor < 0 || f.cursor >= len(f.dirs) {
-// 		logging.Error(fmt.Sprintf("Invalid cursor position. Dirs length: %d, Cursor: %d", len(f.dirs), f.cursor))
-// 		f.viewport.SetContent("Preview unavailable")
-// 		return
-// 	}
-//
-// 	dir := f.dirs[f.cursor]
-// 	filename := dir.Name()
-// 	if !dir.IsDir() && isExtSupported(filename) {
-// 		fullPath := f.cwdDetails.directory + "/" + dir.Name()
-//
-// 		go func() {
-// 			imageString, err := image.ImagePreview(f.viewport.Width()-4, fullPath)
-// 			if err != nil {
-// 				logging.Error(err.Error())
-// 				f.viewport.SetContent("Preview unavailable")
-// 				return
-// 			}
-//
-// 			f.viewport.SetContent(imageString)
-// 		}()
-// 	} else {
-// 		f.viewport.SetContent("Preview unavailable")
-// 	}
-// }
-//
-// func readDir(path string, showHidden bool) []os.DirEntry {
-// 	logging.Info(fmt.Sprintf("Reading directory: %s", path))
-//
-// 	entriesChan := make(chan []os.DirEntry, 1)
-// 	errChan := make(chan error, 1)
-//
-// 	go func() {
-// 		dirEntries, err := os.ReadDir(path)
-// 		if err != nil {
-// 			logging.ErrorPersist(err.Error())
-// 			errChan <- err
-// 			return
-// 		}
-// 		entriesChan <- dirEntries
-// 	}()
-//
-// 	select {
-// 	case dirEntries := <-entriesChan:
-// 		sort.Slice(dirEntries, func(i, j int) bool {
-// 			if dirEntries[i].IsDir() == dirEntries[j].IsDir() {
-// 				return dirEntries[i].Name() < dirEntries[j].Name()
-// 			}
-// 			return dirEntries[i].IsDir()
-// 		})
-//
-// 		if showHidden {
-// 			return dirEntries
-// 		}
-//
-// 		var sanitizedDirEntries []os.DirEntry
-// 		for _, dirEntry := range dirEntries {
-// 			isHidden, _ := IsHidden(dirEntry.Name())
-// 			if !isHidden {
-// 				if dirEntry.IsDir() || isExtSupported(dirEntry.Name()) {
-// 					sanitizedDirEntries = append(sanitizedDirEntries, dirEntry)
-// 				}
-// 			}
-// 		}
-//
-// 		return sanitizedDirEntries
-//
-// 	case err := <-errChan:
-// 		logging.ErrorPersist(fmt.Sprintf("Error reading directory %s", path), err)
-// 		return []os.DirEntry{}
-//
-// 	case <-time.After(5 * time.Second):
-// 		logging.ErrorPersist(fmt.Sprintf("Timeout reading directory %s", path), nil)
-// 		return []os.DirEntry{}
-// 	}
-// }
-//
-// func IsHidden(file string) (bool, error) {
-// 	return strings.HasPrefix(file, "."), nil
-// }
-//
-// func isExtSupported(path string) bool {
-// 	ext := strings.ToLower(filepath.Ext(path))
-// 	return (ext == ".jpg" || ext == ".jpeg" || ext == ".webp" || ext == ".png")
-// }

internal/tui/components/dialogs/filepicker/filepicker.go 🔗

@@ -5,12 +5,14 @@ import (
 	"strings"
 
 	"github.com/charmbracelet/bubbles/v2/filepicker"
+	"github.com/charmbracelet/bubbles/v2/key"
 	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/opencode-ai/opencode/internal/tui/components/core"
 	"github.com/opencode-ai/opencode/internal/tui/components/dialogs"
 	"github.com/opencode-ai/opencode/internal/tui/components/image"
 	"github.com/opencode-ai/opencode/internal/tui/styles"
+	"github.com/opencode-ai/opencode/internal/tui/util"
 )
 
 const (
@@ -19,16 +21,19 @@ const (
 	fileSelectionHight = 10
 )
 
+type FilePickedMsg struct {
+	FilePath string
+}
+
 type FilePicker interface {
 	dialogs.DialogModel
 }
 
-type filePicker struct {
+type model struct {
 	wWidth          int
 	wHeight         int
 	width           int
-	filepicker      filepicker.Model
-	selectedFile    string
+	filePicker      filepicker.Model
 	highlightedFile string
 	image           image.Model
 }
@@ -46,31 +51,40 @@ func NewFilePickerCmp() FilePicker {
 	fp.SetHeight(fileSelectionHight)
 
 	image := image.New(1, 1, "")
-	return &filePicker{filepicker: fp, image: image}
+	return &model{filePicker: fp, image: image}
 }
 
-func (m *filePicker) Init() tea.Cmd {
-	return m.filepicker.Init()
+func (m *model) Init() tea.Cmd {
+	return m.filePicker.Init()
 }
 
-func (m *filePicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
 	case tea.WindowSizeMsg:
 		m.wWidth = msg.Width
 		m.wHeight = msg.Height
 		m.width = min(70, m.wWidth)
-		styles := m.filepicker.Styles
+		styles := m.filePicker.Styles
 		styles.Directory = styles.Directory.Width(m.width - 4)
 		styles.Selected = styles.Selected.PaddingLeft(1).Width(m.width - 4)
 		styles.DisabledSelected = styles.DisabledSelected.PaddingLeft(1).Width(m.width - 4)
 		styles.File = styles.File.Width(m.width)
-		m.filepicker.Styles = styles
+		m.filePicker.Styles = styles
 		return m, nil
+
+	case tea.KeyPressMsg:
+		if key.Matches(msg, m.filePicker.KeyMap.Back) {
+			// make sure we don't go back if we are at the home directory
+			homeDir, _ := os.UserHomeDir()
+			if m.filePicker.CurrentDirectory == homeDir {
+				return m, nil
+			}
+		}
 	}
 
 	var cmd tea.Cmd
 	var cmds []tea.Cmd
-	m.filepicker, cmd = m.filepicker.Update(msg)
+	m.filePicker, cmd = m.filePicker.Update(msg)
 	cmds = append(cmds, cmd)
 	if m.highlightedFile != m.currentImage() && m.currentImage() != "" {
 		w, h := m.imagePreviewSize()
@@ -80,37 +94,40 @@ func (m *filePicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	m.highlightedFile = m.currentImage()
 
 	// Did the user select a file?
-	if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect {
+	if didSelect, path := m.filePicker.DidSelectFile(msg); didSelect {
 		// Get the path of the selected file.
-		m.selectedFile = path
+		return m, tea.Sequence(
+			util.CmdHandler(dialogs.CloseDialogMsg{}),
+			util.CmdHandler(FilePickedMsg{FilePath: path}),
+		)
 	}
 	m.image, cmd = m.image.Update(msg)
 	cmds = append(cmds, cmd)
 	return m, tea.Batch(cmds...)
 }
 
-func (m *filePicker) View() tea.View {
+func (m *model) View() tea.View {
 	t := styles.CurrentTheme()
 
 	content := lipgloss.JoinVertical(
 		lipgloss.Left,
 		t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Add Image", m.width-4)),
 		m.imagePreview(),
-		m.filepicker.View(),
+		m.filePicker.View(),
 	)
 	return tea.NewView(m.style().Render(content))
 }
 
-func (m *filePicker) currentImage() string {
-	for _, ext := range m.filepicker.AllowedTypes {
-		if strings.HasSuffix(m.filepicker.HighlightedPath(), ext) {
-			return m.filepicker.HighlightedPath()
+func (m *model) currentImage() string {
+	for _, ext := range m.filePicker.AllowedTypes {
+		if strings.HasSuffix(m.filePicker.HighlightedPath(), ext) {
+			return m.filePicker.HighlightedPath()
 		}
 	}
 	return ""
 }
 
-func (m *filePicker) imagePreview() string {
+func (m *model) imagePreview() string {
 	t := styles.CurrentTheme()
 	w, h := m.imagePreviewSize()
 	if m.currentImage() == "" {
@@ -125,16 +142,16 @@ func (m *filePicker) imagePreview() string {
 	return m.imagePreviewStyle().Width(w).Height(h).Render(m.image.View())
 }
 
-func (m *filePicker) imagePreviewStyle() lipgloss.Style {
+func (m *model) imagePreviewStyle() lipgloss.Style {
 	t := styles.CurrentTheme()
 	return t.S().Base.Padding(1, 1, 1, 1)
 }
 
-func (m *filePicker) imagePreviewSize() (int, int) {
+func (m *model) imagePreviewSize() (int, int) {
 	return m.width - 4, min(20, m.wHeight/2)
 }
 
-func (m *filePicker) style() lipgloss.Style {
+func (m *model) style() lipgloss.Style {
 	t := styles.CurrentTheme()
 	return t.S().Base.
 		Width(m.width).
@@ -143,12 +160,12 @@ func (m *filePicker) style() lipgloss.Style {
 }
 
 // ID implements FilePicker.
-func (m *filePicker) ID() dialogs.DialogID {
+func (m *model) ID() dialogs.DialogID {
 	return FilePickerID
 }
 
 // Position implements FilePicker.
-func (m *filePicker) Position() (int, int) {
+func (m *model) Position() (int, int) {
 	row := m.wHeight/4 - 2 // just a bit above the center
 	col := m.wWidth / 2
 	col -= m.width / 2

internal/tui/components/image/image.go 🔗

@@ -3,7 +3,6 @@
 package image
 
 import (
-	"context"
 	"fmt"
 	_ "image/jpeg"
 	_ "image/png"
@@ -17,8 +16,6 @@ type Model struct {
 	width  uint
 	height uint
 	err    error
-
-	cancelAnimation context.CancelFunc
 }
 
 func New(width, height uint, url string) Model {
@@ -38,11 +35,11 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
 	case errMsg:
 		m.err = msg
 		return m, nil
-	case rewdrawMsg:
+	case redrawMsg:
 		m.width = msg.width
 		m.height = msg.height
 		m.url = msg.url
-		return m, loadUrl(m.url)
+		return m, loadURL(m.url)
 	case loadMsg:
 		return handleLoadMsg(m, msg)
 	}
@@ -60,7 +57,7 @@ type errMsg struct{ error }
 
 func (m Model) Redraw(width uint, height uint, url string) tea.Cmd {
 	return func() tea.Msg {
-		return rewdrawMsg{
+		return redrawMsg{
 			width:  width,
 			height: height,
 			url:    url,
@@ -68,9 +65,9 @@ func (m Model) Redraw(width uint, height uint, url string) tea.Cmd {
 	}
 }
 
-func (m Model) UpdateUrl(url string) tea.Cmd {
+func (m Model) UpdateURL(url string) tea.Cmd {
 	return func() tea.Msg {
-		return rewdrawMsg{
+		return redrawMsg{
 			width:  m.width,
 			height: m.height,
 			url:    url,
@@ -78,7 +75,7 @@ func (m Model) UpdateUrl(url string) tea.Cmd {
 	}
 }
 
-type rewdrawMsg struct {
+type redrawMsg struct {
 	width  uint
 	height uint
 	url    string

internal/tui/components/image/load.go 🔗

@@ -6,7 +6,6 @@ import (
 	"image"
 	"image/png"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"os"
 	"strings"
@@ -24,7 +23,7 @@ type loadMsg struct {
 	io.ReadCloser
 }
 
-func loadUrl(url string) tea.Cmd {
+func loadURL(url string) tea.Cmd {
 	var r io.ReadCloser
 	var err error
 
@@ -52,20 +51,9 @@ func load(r io.ReadCloser) tea.Cmd {
 }
 
 func handleLoadMsg(m Model, msg loadMsg) (Model, tea.Cmd) {
-	if m.cancelAnimation != nil {
-		m.cancelAnimation()
-	}
-
-	// blank out image so it says "loading..."
-	m.image = ""
-
-	return handleLoadMsgStatic(m, msg)
-}
-
-func handleLoadMsgStatic(m Model, msg loadMsg) (Model, tea.Cmd) {
 	defer msg.Close()
 
-	img, err := readerToimage(m.width, m.height, m.url, msg)
+	img, err := readerToImage(m.width, m.height, m.url, msg)
 	if err != nil {
 		return m, func() tea.Msg { return errMsg{err} }
 	}
@@ -73,7 +61,7 @@ func handleLoadMsgStatic(m Model, msg loadMsg) (Model, tea.Cmd) {
 	return m, nil
 }
 
-func imageToString(width, height uint, url string, img image.Image) (string, error) {
+func imageToString(width, height uint, img image.Image) (string, error) {
 	img = resize.Thumbnail(width, height*2-4, img, resize.Lanczos3)
 	b := img.Bounds()
 	w := b.Max.X
@@ -84,7 +72,7 @@ func imageToString(width, height uint, url string, img image.Image) (string, err
 		for x := w; x < int(width); x = x + 2 {
 			str.WriteString(" ")
 		}
-		for x := 0; x < w; x++ {
+		for x := range w {
 			c1, _ := colorful.MakeColor(img.At(x, y))
 			color1 := p.Color(c1.Hex())
 			c2, _ := colorful.MakeColor(img.At(x, y+1))
@@ -99,9 +87,9 @@ func imageToString(width, height uint, url string, img image.Image) (string, err
 	return str.String(), nil
 }
 
-func readerToimage(width uint, height uint, url string, r io.Reader) (string, error) {
+func readerToImage(width uint, height uint, url string, r io.Reader) (string, error) {
 	if strings.HasSuffix(strings.ToLower(url), ".svg") {
-		return svgToimage(width, height, url, r)
+		return svgToImage(width, height, r)
 	}
 
 	img, _, err := imageorient.Decode(r)
@@ -109,15 +97,15 @@ func readerToimage(width uint, height uint, url string, r io.Reader) (string, er
 		return "", err
 	}
 
-	return imageToString(width, height, url, img)
+	return imageToString(width, height, img)
 }
 
-func svgToimage(width uint, height uint, url string, r io.Reader) (string, error) {
+func svgToImage(width uint, height uint, r io.Reader) (string, error) {
 	// Original author: https://stackoverflow.com/users/10826783/usual-human
 	// https://stackoverflow.com/questions/42993407/how-to-create-and-export-svg-to-png-jpeg-in-golang
 	// Adapted to use size from SVG, and to use temp file.
 
-	tmpPngFile, err := ioutil.TempFile("", "imgcat.*.png")
+	tmpPngFile, err := os.CreateTemp("", "img.*.png")
 	if err != nil {
 		return "", err
 	}
@@ -153,5 +141,5 @@ func svgToimage(width uint, height uint, url string, r io.Reader) (string, error
 	if err != nil {
 		return "", err
 	}
-	return imageToString(width, height, url, img)
+	return imageToString(width, height, img)
 }