#
tokens: 49390/50000 86/88 files (page 1/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 2. Use http://codebase.md/Zebbeni/ansizalizer?page={x} to view the full context.

# Directory Structure

```
├── .gitignore
├── ansizalizer
├── app
│   ├── adapt
│   │   └── generate.go
│   ├── export.go
│   ├── item.go
│   ├── model.go
│   ├── process
│   │   ├── ascii.go
│   │   ├── custom.go
│   │   ├── image.go
│   │   ├── renderer.go
│   │   └── unicode.go
│   ├── resize.go
│   ├── update.go
│   └── view.go
├── assets
│   └── palettes
│       ├── android-screenshot-editor.hex
│       ├── cascade-gb.hex
│       ├── dull-aquatic.hex
│       ├── florescence.hex
│       ├── gb-blue-steel.hex
│       ├── hama-beads-tub.hex
│       ├── kiwami64-v1.hex
│       └── yes.hex
├── controls
│   ├── browser
│   │   ├── item.go
│   │   ├── model.go
│   │   └── update.go
│   ├── export
│   │   ├── destination
│   │   │   ├── model.go
│   │   │   ├── update.go
│   │   │   └── view.go
│   │   ├── model.go
│   │   ├── source
│   │   │   ├── model.go
│   │   │   ├── update.go
│   │   │   └── view.go
│   │   ├── update.go
│   │   └── view.go
│   ├── menu
│   │   └── model.go
│   ├── model.go
│   ├── settings
│   │   ├── advanced
│   │   │   ├── dithering
│   │   │   │   ├── list.go
│   │   │   │   ├── model.go
│   │   │   │   ├── update.go
│   │   │   │   └── view.go
│   │   │   ├── model.go
│   │   │   ├── sampling
│   │   │   │   ├── const.go
│   │   │   │   ├── item.go
│   │   │   │   ├── model.go
│   │   │   │   └── update.go
│   │   │   ├── update.go
│   │   │   └── view.go
│   │   ├── characters
│   │   │   ├── init.go
│   │   │   ├── model.go
│   │   │   ├── tabs.go
│   │   │   ├── update.go
│   │   │   └── view.go
│   │   ├── colors
│   │   │   ├── model.go
│   │   │   ├── update.go
│   │   │   └── view.go
│   │   ├── item.go
│   │   ├── model.go
│   │   ├── palettes
│   │   │   ├── adaptive
│   │   │   │   ├── init.go
│   │   │   │   ├── model.go
│   │   │   │   ├── update.go
│   │   │   │   └── view.go
│   │   │   ├── loader
│   │   │   │   ├── item.go
│   │   │   │   ├── model.go
│   │   │   │   ├── values.go
│   │   │   │   └── view.go
│   │   │   ├── lospec
│   │   │   │   ├── init.go
│   │   │   │   ├── list.go
│   │   │   │   ├── model.go
│   │   │   │   ├── update.go
│   │   │   │   └── view.go
│   │   │   ├── matrix.go
│   │   │   ├── model.go
│   │   │   ├── update.go
│   │   │   └── view.go
│   │   ├── size
│   │   │   ├── init.go
│   │   │   ├── model.go
│   │   │   ├── update.go
│   │   │   └── view.go
│   │   ├── state.go
│   │   ├── update.go
│   │   └── view.go
│   ├── update.go
│   └── view.go
├── display
│   └── model.go
├── env
│   ├── os_darwin.go
│   ├── os_linux.go
│   └── os_windows.go
├── event
│   ├── command.go
│   └── keymap.go
├── global
│   └── file.go
├── go.mod
├── go.sum
├── images
│   └── characters
│       ├── char_001.png
│       ├── char_002.png
│       ├── char_003.png
│       ├── char_004.png
│       ├── char_005.png
│       ├── char_006.png
│       ├── char_007.png
│       ├── char_008.png
│       ├── char_009.png
│       ├── char_010.png
│       ├── char_011.png
│       ├── char_012.png
│       ├── char_013.png
│       ├── char_014.png
│       ├── char_015.png
│       ├── char_016.png
│       ├── char_017.png
│       ├── char_018.png
│       ├── char_019.png
│       ├── char_020.png
│       ├── char_021.png
│       ├── char_022.png
│       ├── char_023.png
│       ├── char_024.png
│       ├── char_025.png
│       ├── char_026.png
│       ├── char_027.png
│       └── char_028.png
├── LICENSE.md
├── main.go
├── palette
│   ├── model.go
│   └── view.go
├── README.md
├── style
│   ├── box.go
│   └── color.go
├── test_images
│   ├── dock.png
│   ├── mermaid.png
│   ├── mona_lisa.jpg
│   ├── planet.png
│   ├── robots.png
│   ├── sewer.png
│   └── throne.png
└── viewer
    ├── model.go
    └── update.go
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Images test directory
images/*

# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
*.idea/
*.hex
*.ansi

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# ANSIZALIZER
A TUI to convert Images to ANSI strings using bubbletea

![Screenshot 2024-04-02 150412](https://github.com/Zebbeni/ansizalizer/assets/3377325/141c3662-7e70-4e82-ac0c-5db77adbf1c7)

## Features
- A keyboard-navigable Text-based UI
- File browser: Search .png and .jpeg image files and preview in real-time
- Export ANSI image strings to '.ansi' text files or copy directly to your Clipboard
- Save files individually or Batch Process All Images in a chosen directory
- Browse Lospec.com for cool color palettes

## Render Options
- Set output Width and Height of rendered text images (in characters)
- Choose character sets to use in output (ASCII, Unicode, or Custom)
- Render images with "true" colors or convert using Limited Color Palettes
- Generate new color palettes by sampling previewed image files
- Use Advanced settings to tweak pixel Sampling mode and Dithering options

![Screenshot 2024-04-02 155820](https://github.com/Zebbeni/ansizalizer/assets/3377325/24095f45-5c73-4654-a5e1-b491cda9dc66)

## To Run

**On Windows:**
```bash
go install
go build
start ansizalizer.exe
```

**On Mac/Linux:**
```bash
go install
go build
./ansizalizer
```

![Screenshot 2024-04-02 155006](https://github.com/Zebbeni/ansizalizer/assets/3377325/d41df628-6c84-44e0-aa34-f7fcb72ed827)

## FAQ / Troubleshooting
**Q: The UI isn't rendering correctly**

Check your default console appearance settings. Make sure your chosen font, font size, and line height aren't the cause of the problem. 'DejaVu Sans Mono' works well for me on Windows.

**Q: My images look squashed / stretched**

Try adjusting the value of Char Size Ratio under Settings > Size. Depending on what font your console uses, your characters may have a width-to-height ratio different than 0.5.

**Q: My exported .ansi files take up more space than the original image**

The ANSI code that produces the text-rendered images isn't (currently) optimized for file size. If using this tool to batch process lots of text art for use in a game or application, I'd consider compressing the resulting text files and decompressing them as needed.

```

--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------

```markdown
MIT License

Copyright (c) 2024 Andrew Albers

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

```

--------------------------------------------------------------------------------
/env/os_darwin.go:
--------------------------------------------------------------------------------

```go
//go:build darwin

package env

const PollForSizeChange = false
```

--------------------------------------------------------------------------------
/env/os_linux.go:
--------------------------------------------------------------------------------

```go
//go:build linux

package env

const PollForSizeChange = false

```

--------------------------------------------------------------------------------
/env/os_windows.go:
--------------------------------------------------------------------------------

```go
//go:build windows

package env

const PollForSizeChange = true

```

--------------------------------------------------------------------------------
/global/file.go:
--------------------------------------------------------------------------------

```go
package global

var (
	ImgExtensions = map[string]bool{".png": true, ".jpg": true, ".jpeg": true}
)

```

--------------------------------------------------------------------------------
/controls/settings/palettes/loader/item.go:
--------------------------------------------------------------------------------

```go
package loader

import (
	"github.com/Zebbeni/ansizalizer/palette"
)

type item struct {
	palette palette.Model
}

func (i item) FilterValue() string {
	return i.palette.Name()
}

func (i item) Title() string {
	return i.palette.Name()
}

func (i item) Description() string {
	return i.palette.View()
}

```

--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------

```go
package main

import (
	"fmt"
	"os"

	tea "github.com/charmbracelet/bubbletea"

	"github.com/Zebbeni/ansizalizer/app"
	"github.com/Zebbeni/ansizalizer/event"
)

func init() {
	event.InitKeyMap()
}

func main() {
	m := app.New()
	p := tea.NewProgram(m)
	if _, err := p.Run(); err != nil {
		fmt.Println("Error running program:", err)
		os.Exit(1)
	}
}

```

--------------------------------------------------------------------------------
/controls/settings/state.go:
--------------------------------------------------------------------------------

```go
package settings

type State int

const (
	None State = iota
	Colors
	Characters
	Size
	Advanced
)

var States = []State{
	Colors,
	Characters,
	Size,
	Advanced,
}

var stateOrder = []State{Colors, Characters, Size, Advanced}

var stateTitles = map[State]string{
	Colors:     "Colors",
	Characters: "Characters",
	Size:       "Size",
	Advanced:   "Advanced",
}

```

--------------------------------------------------------------------------------
/viewer/update.go:
--------------------------------------------------------------------------------

```go
package viewer

import (
	"fmt"
	"path/filepath"

	tea "github.com/charmbracelet/bubbletea"

	"github.com/Zebbeni/ansizalizer/event"
)

func (m Model) handleFinishRenderMsg(msg event.FinishRenderToViewMsg) (Model, tea.Cmd) {
	m.WaitingOnRender = false
	m.imgString = msg.ImgString

	displayMsg := fmt.Sprintf("viewing %s/%s with %s palette", filepath.Base(filepath.Dir(msg.FilePath)), filepath.Base(msg.FilePath), msg.ColorsString)
	return m, event.BuildDisplayCmd(displayMsg)
}

```

--------------------------------------------------------------------------------
/controls/settings/item.go:
--------------------------------------------------------------------------------

```go
package settings

//type item struct {
//	name  string
//	state State
//}
//
//func (i item) FilterValue() string {
//	return i.name
//}
//
//func (i item) Title() string {
//	return i.name
//}
//
//func (i item) Description() string {
//	return ""
//}

//func newMenu() list.Model {
//	items := []list.Item{
//		item{name: "Loader", state: Loader},
//		item{name: "Advanced", state: Advanced},
//		//item{name: "Limited", state: Limited},
//		//item{name: "Characters", state: Characters},
//	}
//	return menu.New(items)
//}

```

--------------------------------------------------------------------------------
/controls/settings/palettes/adaptive/init.go:
--------------------------------------------------------------------------------

```go
package adaptive

import (
	"fmt"

	"github.com/charmbracelet/bubbles/textinput"
	"github.com/charmbracelet/lipgloss"
)

var (
	promptStyle      = lipgloss.NewStyle().Width(8).PaddingLeft(1)
	placeholderStyle = lipgloss.NewStyle()
)

func newInput(state State) textinput.Model {
	textinput.New()
	input := textinput.New()
	input.Prompt = stateNames[state]
	input.PromptStyle = promptStyle
	input.PlaceholderStyle = placeholderStyle
	input.Cursor.Blink = true
	input.CharLimit = 3
	input.SetValue(fmt.Sprintf("16"))
	return input
}

```

--------------------------------------------------------------------------------
/controls/settings/advanced/sampling/const.go:
--------------------------------------------------------------------------------

```go
package sampling

import "github.com/nfnt/resize"

var Functions = []resize.InterpolationFunction{
	resize.NearestNeighbor,
	resize.Bicubic,
	resize.Bilinear,
	resize.Lanczos2,
	resize.Lanczos3,
	resize.MitchellNetravali,
}

var nameMap = map[resize.InterpolationFunction]string{
	resize.NearestNeighbor:   "Nearest Neighbor",
	resize.Bicubic:           "Bicubic",
	resize.Bilinear:          "Bilinear",
	resize.Lanczos2:          "Lanczos2",
	resize.Lanczos3:          "Lanczos3",
	resize.MitchellNetravali: "MitchellNetravali",
}

```

--------------------------------------------------------------------------------
/viewer/model.go:
--------------------------------------------------------------------------------

```go
package viewer

import (
	tea "github.com/charmbracelet/bubbletea"

	"github.com/Zebbeni/ansizalizer/controls/settings"
	"github.com/Zebbeni/ansizalizer/event"
)

type Model struct {
	imgString string
	settings  settings.Model

	WaitingOnRender bool
}

func New() Model {
	return Model{}
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch msg := msg.(type) {
	case event.FinishRenderToViewMsg:
		return m.handleFinishRenderMsg(msg)
	}
	return m, nil
}

func (m Model) View() string {
	if m.WaitingOnRender {
		return ""
	}
	return m.imgString
}

```

--------------------------------------------------------------------------------
/controls/settings/characters/init.go:
--------------------------------------------------------------------------------

```go
package characters

import (
	"github.com/charmbracelet/bubbles/textinput"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/style"
)

var (
	promptStyle      = lipgloss.NewStyle().Padding(0, 1, 0, 1)
	placeholderStyle = lipgloss.NewStyle()
)

// TODO: This is basically the same as we have in adaptive. Maybe generalize?
func newInput(prompt string, value string) textinput.Model {
	textinput.New()
	input := textinput.New()
	input.Prompt = prompt
	input.PromptStyle = style.NormalButtonNode.Copy().Padding(0, 1, 0, 0)
	input.PlaceholderStyle = placeholderStyle
	input.Cursor.Blink = true
	input.SetValue(value)
	return input
}

```

--------------------------------------------------------------------------------
/controls/settings/palettes/matrix.go:
--------------------------------------------------------------------------------

```go
package palettes

import (
	"github.com/makeworld-the-better-one/dither/v2"
)

type Matrix struct {
	Name   string
	Method dither.ErrorDiffusionMatrix
}

func getMatrixMenuItems() []Matrix {
	return []Matrix{
		Matrix{Name: "Simple2D", Method: dither.Simple2D},
		Matrix{Name: "FloydSteinberg", Method: dither.FloydSteinberg},
		Matrix{Name: "JarvisJudiceNinke", Method: dither.JarvisJudiceNinke},
		Matrix{Name: "Atkinson", Method: dither.Atkinson},
		Matrix{Name: "Stucki", Method: dither.Stucki},
		Matrix{Name: "Burkes", Method: dither.Burkes},
		Matrix{Name: "Sierra", Method: dither.Sierra},
		Matrix{Name: "StevenPigeon", Method: dither.StevenPigeon},
	}
}

```

--------------------------------------------------------------------------------
/app/adapt/generate.go:
--------------------------------------------------------------------------------

```go
package adapt

import (
	"bufio"
	"image"
	"image/color"
	"os"
	"path/filepath"
	"strings"

	"github.com/mccutchen/palettor"

	"github.com/Zebbeni/ansizalizer/controls/settings/palettes/adaptive"
)

func GeneratePalette(m adaptive.Model, imgFilePath string) (color.Palette, string) {
	if imgFilePath == "" {
		return nil, ""
	}

	var img image.Image
	imgFile, err := os.Open(imgFilePath)
	if err != nil {
		return nil, ""
	}
	defer imgFile.Close()
	imageReader := bufio.NewReader(imgFile)
	img, _, err = image.Decode(imageReader)
	if err != nil {
		return nil, ""
	}

	count, iterations := m.Info()
	palette, err := palettor.Extract(count, iterations, img)

	name := strings.Split(filepath.Base(imgFilePath), ".")[0]

	return palette.Colors(), name
}

```

--------------------------------------------------------------------------------
/app/item.go:
--------------------------------------------------------------------------------

```go
package app

import "github.com/charmbracelet/bubbles/list"

type item struct {
	name  string
	state State
}

func (i item) FilterValue() string {
	return i.name
}

func (i item) Title() string {
	return i.name
}

func (i item) Description() string {
	return ""
}

func newMenu() list.Model {
	items := []list.Item{
		item{name: "File", state: Browser},
		item{name: "Settings", state: Settings},
	}
	menu := list.New(items, NewDelegate(), 20, 20)
	menu.SetShowHelp(false)
	menu.SetShowFilter(false)
	menu.SetShowTitle(false)
	menu.SetShowStatusBar(false)

	menu.KeyMap.ForceQuit.Unbind()
	menu.KeyMap.Quit.Unbind()
	return menu
}

func NewDelegate() list.DefaultDelegate {
	delegate := list.NewDefaultDelegate()
	delegate.SetSpacing(0)
	delegate.ShowDescription = false
	return delegate
}

```

--------------------------------------------------------------------------------
/controls/settings/palettes/lospec/init.go:
--------------------------------------------------------------------------------

```go
package lospec

import (
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"github.com/charmbracelet/bubbles/textinput"
)

var (
	promptStyle      = lipgloss.NewStyle().Padding(0, 1, 0, 1)
	placeholderStyle = lipgloss.NewStyle()
)

// TODO: This is basically the same as we have in adaptive. Maybe generalize?
func newInput(state State, value string) textinput.Model {
	textinput.New()
	input := textinput.New()
	input.Prompt = stateNames[state]
	input.PromptStyle = promptStyle
	input.PlaceholderStyle = placeholderStyle
	input.Cursor.Blink = true
	input.SetValue(value)
	return input
}

func (m Model) InitializeList() (Model, tea.Cmd) {
	m.didInitializeList = true
	return m.searchLospec(0)
}

func (m Model) DidInitializeList() bool {
	return m.didInitializeList
}

```

--------------------------------------------------------------------------------
/display/model.go:
--------------------------------------------------------------------------------

```go
package display

import (
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/event"
	"github.com/Zebbeni/ansizalizer/style"
)

type Model struct {
	msg   string
	width int
}

func New() Model {
	return Model{}
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch msg := msg.(type) {
	case event.DisplayMsg:
		m.msg = string(msg)
	}
	return m, nil
}

func (m Model) View() string {
	// TODO: Switch style based on event type (warning, info, etc.)
	displayStyle := style.ExtraDimTitle.Copy().Width(m.width - 2)
	return displayStyle.Border(lipgloss.RoundedBorder()).BorderForeground(style.ExtraDimColor).Render(m.msg)
}

func (m Model) SetWidth(w int) Model {
	m.width = w
	return m
}

```

--------------------------------------------------------------------------------
/controls/settings/view.go:
--------------------------------------------------------------------------------

```go
package settings

import (
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/style"
)

var (
	activeColor = lipgloss.Color("#aaaaaa")
	focusColor  = lipgloss.Color("#ffffff")
	normalColor = lipgloss.Color("#555555")
)

func (m Model) renderWithBorder(content string, state State) string {
	renderColor := normalColor
	if m.active == state {
		renderColor = activeColor
	} else if m.focus == state {
		renderColor = focusColor
	}

	textStyle := lipgloss.NewStyle().
		AlignHorizontal(lipgloss.Center).
		Padding(0, 1, 0, 1).
		Foreground(renderColor)
	borderStyle := lipgloss.NewStyle().
		Border(lipgloss.RoundedBorder()).
		BorderForeground(renderColor)

	renderer := style.BoxWithLabel{
		BoxStyle:   borderStyle,
		LabelStyle: textStyle,
	}

	return renderer.Render(stateTitles[state], content, m.width-2)
}

```

--------------------------------------------------------------------------------
/controls/settings/advanced/sampling/update.go:
--------------------------------------------------------------------------------

```go
package sampling

import (
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"

	"github.com/Zebbeni/ansizalizer/event"
)

func (m Model) handleEsc() (Model, tea.Cmd) {
	m.ShouldClose = true
	m.list.SetDelegate(NewDelegate(false))
	return m, nil
}

func (m Model) handleEnter() (Model, tea.Cmd) {
	m.ShouldClose = true
	m.list.SetDelegate(NewDelegate(false))
	return m, nil
}

func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
	if key.Matches(msg, event.KeyMap.Up) && m.list.Index() == 0 {
		m.list.SetDelegate(NewDelegate(false))
		m.ShouldClose = true
		return m, nil
	}

	var cmd tea.Cmd
	m.list, cmd = m.list.Update(msg)
	m.list.SetDelegate(NewDelegate(true))
	selectedItem := m.list.SelectedItem().(item)

	if selectedItem.Function == m.Function {
		return m, cmd
	}

	m.Function = selectedItem.Function

	return m, tea.Batch(cmd, event.StartRenderToViewCmd)
}

```

--------------------------------------------------------------------------------
/controls/settings/size/init.go:
--------------------------------------------------------------------------------

```go
package size

import (
	"fmt"
	"strconv"

	"github.com/charmbracelet/bubbles/textinput"
	"github.com/charmbracelet/lipgloss"
)

var (
	promptStyle      = lipgloss.NewStyle().Width(8).Padding(0, 0, 0, 1)
	placeholderStyle = lipgloss.NewStyle()

	floatPromptStyle      = lipgloss.NewStyle().Padding(0, 1)
	floatPlaceholderStyle = lipgloss.NewStyle()
)

func newInput(state State, value int) textinput.Model {
	textinput.New()
	input := textinput.New()
	input.Prompt = stateNames[state]
	input.PromptStyle = promptStyle
	input.PlaceholderStyle = placeholderStyle
	input.CharLimit = 3
	input.SetValue(strconv.Itoa(value))
	return input
}

func newFloatInput(state State, value float64) textinput.Model {
	textinput.New()
	input := textinput.New()
	input.Prompt = stateNames[state]
	input.PromptStyle = floatPromptStyle
	input.PlaceholderStyle = floatPlaceholderStyle
	input.CharLimit = 5
	input.SetValue(fmt.Sprintf("%1.2f", value))
	return input
}

```

--------------------------------------------------------------------------------
/controls/settings/colors/view.go:
--------------------------------------------------------------------------------

```go
package colors

import (
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/style"
)

func (m Model) drawPaletteToggles() string {
	title := style.DimmedTitle.Copy().PaddingLeft(1).Render("Mode:")

	trueColorStyle := style.NormalButtonNode
	if m.IsActive && m.focus == UseTrueColor {
		trueColorStyle = style.FocusButtonNode
	} else if m.mode == UseTrueColor {
		trueColorStyle = style.ActiveButtonNode
	}
	trueColorNode := trueColorStyle.Render("True Color")
	trueColorNode = lipgloss.NewStyle().PaddingLeft(1).Render(trueColorNode)

	palettedStyle := style.NormalButtonNode
	if m.IsActive && m.focus == UsePalette {
		palettedStyle = style.FocusButtonNode
	} else if m.mode == UsePalette {
		palettedStyle = style.ActiveButtonNode
	}
	palettedNode := palettedStyle.Render("Palette")
	palettedNode = lipgloss.NewStyle().PaddingLeft(1).Render(palettedNode)

	return lipgloss.JoinHorizontal(lipgloss.Left, title, trueColorNode, palettedNode)
}

```

--------------------------------------------------------------------------------
/palette/view.go:
--------------------------------------------------------------------------------

```go
package palette

import (
	"image/color"
	"math"

	"github.com/charmbracelet/lipgloss"
	"github.com/lucasb-eyer/go-colorful"
)

func Palette(palette color.Palette, w, h int) string {
	runes := make([]string, len(palette)/2+1)
	rows := make([]string, 0, h)
	for idx := 0; idx < len(palette); idx += 2 {
		var fg, bg colorful.Color
		var lipFg, lipBg lipgloss.Color

		fg, _ = colorful.MakeColor(palette[idx])
		lipFg = lipgloss.Color(fg.Hex())
		style := lipgloss.NewStyle().Foreground(lipFg)

		if idx+1 < len(palette) {
			bg, _ = colorful.MakeColor(palette[idx+1])
			lipBg = lipgloss.Color(bg.Hex())
			style = style.Copy().Background(lipBg)
		}
		runes[idx/2] = style.Render(string('▀'))
	}
	for i := 0; i < h; i++ {
		start := w * i
		if start >= len(runes) {
			break
		}
		stop := int(math.Min(float64(w*(i+1)), float64(len(runes))))
		rows = append(rows, "")
		rows[i] = lipgloss.JoinHorizontal(lipgloss.Left, runes[start:stop]...)
	}
	return lipgloss.JoinVertical(lipgloss.Left, rows...)
}

```

--------------------------------------------------------------------------------
/controls/export/destination/view.go:
--------------------------------------------------------------------------------

```go
package destination

import (
	"fmt"
	"path/filepath"

	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/style"
)

func (m Model) drawSelected() string {
	title := style.DimmedTitle.Copy().Render("Selected")

	valueStyle := style.DimmedTitle.Copy()

	if Input == m.focus {
		if m.IsActive {
			valueStyle = style.SelectedTitle.Copy()
		} else {
			valueStyle = style.NormalTitle.Copy()
		}
	}
	valueStyle.Padding(0, 0, 1, 0)

	path := m.Browser.SelectedDir

	parent := filepath.Base(filepath.Dir(path))
	selected := filepath.Base(path)
	value := fmt.Sprintf("%s/%s", parent, selected)

	valueRunes := []rune(value)
	if len(valueRunes) > m.width {
		value = string(valueRunes[len(valueRunes)-m.width:])
	}

	valueContent := valueStyle.Render(value)

	valueWidth := m.width
	widthStyle := lipgloss.NewStyle().Width(valueWidth).AlignHorizontal(lipgloss.Center)
	content := lipgloss.JoinVertical(lipgloss.Center, title, valueContent)

	return widthStyle.Render(content)
}

func drawBrowserTitle() string {
	return style.DimmedTitle.Copy().Padding(0, 2, 1, 2).Render("Select a directory")
}

```

--------------------------------------------------------------------------------
/controls/export/destination/update.go:
--------------------------------------------------------------------------------

```go
package destination

import (
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"

	"github.com/Zebbeni/ansizalizer/event"
)

type Direction int

const (
	Up Direction = iota
	Down
)

var (
	navMap = map[Direction]map[State]State{
		Down: {Input: Browser},
		Up:   {Browser: Input},
	}
)

func (m Model) handleEsc() (Model, tea.Cmd) {
	m.ShouldClose = true
	m.IsActive = false
	return m, nil
}

func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
	switch {
	case key.Matches(msg, event.KeyMap.Down):
		if next, hasNext := navMap[Down][m.focus]; hasNext {
			m.focus = next
		}
	case key.Matches(msg, event.KeyMap.Up):
		if next, hasNext := navMap[Up][m.focus]; hasNext {
			m.focus = next
		} else {
			m.ShouldClose = true
		}
	}
	return m, nil
}

func (m Model) handleEnter() (Model, tea.Cmd) {
	switch m.focus {
	case Input:
		m.focus = Browser
	}
	return m, nil
}

func (m Model) handleDstBrowserUpdate(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	m.Browser, cmd = m.Browser.Update(msg)
	m.selectedDir = m.Browser.SelectedDir

	if m.Browser.ShouldClose {
		m.focus = Input
		m.Browser.ShouldClose = false
	}
	return m, cmd
}

```

--------------------------------------------------------------------------------
/app/process/image.go:
--------------------------------------------------------------------------------

```go
package process

import (
	"bufio"
	"image"
	"os"

	"github.com/lucasb-eyer/go-colorful"

	"github.com/Zebbeni/ansizalizer/controls/settings"
	"github.com/Zebbeni/ansizalizer/controls/settings/characters"
)

var (
	black = colorful.Color{}
)

func RenderImageFile(s settings.Model, imgFilePath string) string {
	if imgFilePath == "" {
		return "Browse an image to render"
	}

	var img image.Image
	imgFile, err := os.Open(imgFilePath)
	if err != nil {
		return "Could not open image " + imgFilePath
	}
	defer imgFile.Close()
	imageReader := bufio.NewReader(imgFile)
	img, _, err = image.Decode(imageReader)
	if err != nil {
		return "Could not decode image " + imgFilePath
	}

	renderer := New(s)
	imgString := renderer.process(img)
	return imgString
}

func (m Renderer) process(input image.Image) string {
	isTrueColor, _, palette := m.Settings.Colors.GetSelected()
	if !isTrueColor && len(palette.Colors()) == 0 {
		return "Choose a color palette"
	}
	mode, _, _, _ := m.Settings.Characters.Selected()
	switch mode {
	case characters.Ascii:
		return m.processAscii(input)
	case characters.Unicode:
		return m.processUnicode(input)
	case characters.Custom:
		return m.processCustom(input)
	}
	return "Choose a character type"
}

```

--------------------------------------------------------------------------------
/controls/settings/palettes/lospec/list.go:
--------------------------------------------------------------------------------

```go
package lospec

import (
	"github.com/charmbracelet/bubbles/list"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/style"
)

func CreateList(items []list.Item, w int) list.Model {
	newList := list.New(items, NewDelegate(), w, 22)

	newList.KeyMap.ForceQuit.Unbind()
	newList.KeyMap.Quit.Unbind()
	newList.SetShowHelp(false)
	newList.SetShowStatusBar(false)
	newList.SetShowTitle(false)
	newList.SetFilteringEnabled(false)

	return newList
}

func NewDelegate() list.DefaultDelegate {
	delegate := list.NewDefaultDelegate()
	delegate.SetSpacing(0)
	delegate.ShowDescription = true
	delegate.Styles = ItemStyles()
	return delegate
}

func ItemStyles() (s list.DefaultItemStyles) {
	s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2)
	s.NormalDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2)

	s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1).
		Border(lipgloss.NormalBorder(), false, false, false, true).
		BorderForeground(style.SelectedColor1)
	s.SelectedDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2)

	s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0)
	s.DimmedDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2)

	return s
}

```

--------------------------------------------------------------------------------
/event/keymap.go:
--------------------------------------------------------------------------------

```go
package event

import (
	"github.com/charmbracelet/bubbles/key"
)

type Map struct {
	Enter key.Binding
	Nav   key.Binding
	Right key.Binding
	Left  key.Binding
	Up    key.Binding
	Down  key.Binding
	Copy  key.Binding
	Save  key.Binding
	Esc   key.Binding
}

var KeyMap Map

func InitKeyMap() {
	KeyMap = Map{
		Enter: key.NewBinding(
			key.WithKeys("return", "enter"),
			key.WithHelp("↲/enter", "select/focus menu"),
		),
		Nav: key.NewBinding(
			key.WithKeys("up", "down", "right", "left"),
			key.WithHelp("↕/↔", "navigate"),
		),
		Right: key.NewBinding(
			key.WithKeys("right"),
		),
		Left: key.NewBinding(
			key.WithKeys("left"),
		),
		Up: key.NewBinding(
			key.WithKeys("up"),
		),
		Down: key.NewBinding(
			key.WithKeys("down"),
		),
		Copy: key.NewBinding(
			key.WithKeys("ctrl+c"),
			key.WithHelp("ctrl+c", "copy to clipboard")),
		Save: key.NewBinding(
			key.WithKeys("ctrl+s"),
			key.WithHelp("ctrl+s", "save to file")),
		Esc: key.NewBinding(
			key.WithKeys("esc"),
			key.WithHelp("esc", "back/exit menu"),
		),
	}
}

func (k Map) ShortHelp() []key.Binding {
	return []key.Binding{k.Nav, k.Enter, k.Esc, k.Copy, k.Save}
}

func (k Map) FullHelp() [][]key.Binding {
	return [][]key.Binding{{k.Nav, k.Enter, k.Esc, k.Copy, k.Save}}
}

```

--------------------------------------------------------------------------------
/controls/settings/advanced/sampling/model.go:
--------------------------------------------------------------------------------

```go
package sampling

import (
	"github.com/charmbracelet/bubbles/key"
	"github.com/charmbracelet/bubbles/list"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/nfnt/resize"

	"github.com/Zebbeni/ansizalizer/event"
	"github.com/Zebbeni/ansizalizer/style"
)

type Model struct {
	Function resize.InterpolationFunction

	list list.Model

	IsActive    bool
	ShouldClose bool
}

func New(w int) Model {
	items := menuItems()
	selected := items[0].(item)
	menu := newMenu(items, w, len(items))

	return Model{
		Function:    selected.Function,
		list:        menu,
		IsActive:    false,
		ShouldClose: false,
	}
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, event.KeyMap.Esc):
			return m.handleEsc()
		case key.Matches(msg, event.KeyMap.Enter):
			return m.handleEnter()
		case key.Matches(msg, event.KeyMap.Nav):
			return m.handleNav(msg)
		}
	}
	return m, nil
}

func (m Model) View() string {
	prompt := style.DimmedTitle.Copy().Render("Select Method")
	menu := m.list.View()
	content := lipgloss.JoinVertical(lipgloss.Left, prompt, menu)
	return lipgloss.NewStyle().Padding(0, 1).Render(content)
}

```

--------------------------------------------------------------------------------
/app/resize.go:
--------------------------------------------------------------------------------

```go
package app

import (
	"os"
	"time"

	"golang.org/x/term"

	tea "github.com/charmbracelet/bubbletea"
)

// There is (currently) no support on Windows for detecting resize events, so
// we instead poll at regular intervals to check if the terminal size changed.
// If a resize is detected in this way, we send a WindowSizeMsg with the new
// dimensions to bubbletea, and handle it in the Model event handler
type checkSizeMsg int

const (
	resizeCheckDuration = time.Second / 4
)

func (m Model) handleSizeMsg(msg tea.WindowSizeMsg) (Model, tea.Cmd) {
	w, h := msg.Width, msg.Height
	m.w, m.h = w, h
	m.display = m.display.SetWidth(m.rPanelWidth())

	tea.ClearScreen()
	return m, nil
}

func (m Model) handleCheckSizeMsg() (Model, tea.Cmd) {
	w, h, _ := term.GetSize(int(os.Stdout.Fd()))
	if w == m.w && h == m.h {
		return m, pollForSizeChange
	}
	updateSizeCmd := func() tea.Msg {
		return tea.WindowSizeMsg{Width: w, Height: h}
	}
	return m, tea.Batch(pollForSizeChange, updateSizeCmd)
}

func pollForSizeChange() tea.Msg {
	time.Sleep(resizeCheckDuration)
	return checkSizeMsg(1)
}

func (m Model) leftPanelHeight() int {
	return m.h - helpHeight
}

func (m Model) rPanelWidth() int {
	return m.w - controlsWidth
}

func (m Model) rPanelHeight() int {
	return m.h - helpHeight
}

```

--------------------------------------------------------------------------------
/controls/export/view.go:
--------------------------------------------------------------------------------

```go
package export

import (
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/style"
)

var (
	activeColor = lipgloss.Color("#aaaaaa")
	focusColor  = lipgloss.Color("#ffffff")
	normalColor = lipgloss.Color("#555555")
)

func (m Model) renderWithBorder(content string, state State) string {
	renderColor := normalColor
	if m.active == state {
		renderColor = activeColor
	} else if m.focus == state {
		renderColor = focusColor
	}

	textStyle := lipgloss.NewStyle().
		AlignHorizontal(lipgloss.Center).
		Padding(0, 1, 0, 1).
		Foreground(renderColor)
	borderStyle := lipgloss.NewStyle().
		Border(lipgloss.RoundedBorder()).
		BorderForeground(renderColor)

	renderer := style.BoxWithLabel{
		BoxStyle:   borderStyle,
		LabelStyle: textStyle,
	}

	return renderer.Render(stateTitles[state], content, m.width-2)
}

func (m Model) drawProcessButton() string {
	buttonStyle := style.NormalButton
	if m.focus == Process {
		buttonStyle = style.FocusButton
	}

	centerStyle := lipgloss.NewStyle().AlignHorizontal(lipgloss.Center)

	internalStyle := centerStyle.Copy().Width(m.width - 2)
	title := internalStyle.Render(stateTitles[Process])
	button := buttonStyle.Render(title)

	return centerStyle.Copy().Width(m.width).AlignHorizontal(lipgloss.Center).Render(button)
}

```

--------------------------------------------------------------------------------
/controls/settings/palettes/loader/view.go:
--------------------------------------------------------------------------------

```go
package loader

import (
	"github.com/charmbracelet/bubbles/list"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/style"
)

const (
	maxWidth          = 30
	maxNormalHeight   = 1
	maxSelectedHeight = 2
)

// NewItemStyles returns style definitions for a default item.
// DefaultItemView for when these come into play.
func NewItemStyles() (s list.DefaultItemStyles) {

	s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2)
	s.NormalDesc = style.DimmedParagraph.Copy().MaxHeight(maxNormalHeight).Padding(0, 0, 0, 2)

	s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1).
		Border(lipgloss.NormalBorder(), false, false, false, true).
		BorderForeground(style.SelectedColor1)
	s.SelectedDesc = style.SelectedTitle.Copy().MaxHeight(maxSelectedHeight).Padding(0, 0, 0, 1).
		Border(lipgloss.NormalBorder(), false, false, false, true).
		BorderForeground(style.SelectedColor1)

	s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0)
	s.DimmedDesc = style.DimmedParagraph.Copy().MaxHeight(maxNormalHeight).Padding(0, 0, 0, 2)

	return s
}

func (m Model) drawTitle() string {
	title := style.DimmedTitle.Copy().Italic(true).Render("Load from .hex file")
	return lipgloss.NewStyle().Width(m.width).PaddingBottom(1).AlignHorizontal(lipgloss.Center).Render(title)
}

```

--------------------------------------------------------------------------------
/controls/menu/model.go:
--------------------------------------------------------------------------------

```go
package menu

import (
	"github.com/charmbracelet/bubbles/list"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/style"
)

func New(items []list.Item, w int) list.Model {
	newList := list.New(items, NewDelegate(), w, 18)

	newList.KeyMap.ForceQuit.Unbind()
	newList.KeyMap.Quit.Unbind()
	newList.SetShowHelp(false)
	newList.SetShowStatusBar(false)
	newList.SetShowTitle(false)
	newList.SetFilteringEnabled(false)

	return newList
}

func NewDelegate() list.DefaultDelegate {
	delegate := list.NewDefaultDelegate()
	delegate.SetSpacing(0)
	delegate.ShowDescription = false
	delegate.Styles = ItemStyles()
	return delegate
}

func ItemStyles() (s list.DefaultItemStyles) {
	s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2)
	s.NormalDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2)

	s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1).
		Border(lipgloss.NormalBorder(), false, false, false, true).
		BorderForeground(style.SelectedColor1)
	s.NormalDesc = style.SelectedTitle.Copy().MaxHeight(1).Padding(0, 0, 0, 2).
		Border(lipgloss.NormalBorder(), false, false, false, true).
		BorderForeground(style.SelectedColor1)

	s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0)
	s.DimmedDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2)

	return s
}

```

--------------------------------------------------------------------------------
/controls/settings/palettes/view.go:
--------------------------------------------------------------------------------

```go
package palettes

import "github.com/charmbracelet/lipgloss"

var (
	stateOrder = []State{Load, Adapt, Lospec}
	stateNames = map[State]string{
		Load:   "Load",
		Adapt:  "Sample",
		Lospec: "Lospec",
	}

	activeStyle = lipgloss.NewStyle().
			BorderStyle(lipgloss.RoundedBorder()).
			BorderForeground(lipgloss.Color("#aaaaaa")).
			Foreground(lipgloss.Color("#aaaaaa"))
	focusStyle = lipgloss.NewStyle().
			BorderStyle(lipgloss.RoundedBorder()).
			BorderForeground(lipgloss.Color("#ffffff")).
			Foreground(lipgloss.Color("#ffffff"))
	normalStyle = lipgloss.NewStyle().
			BorderStyle(lipgloss.RoundedBorder()).
			BorderForeground(lipgloss.Color("#555555")).
			Foreground(lipgloss.Color("#555555"))
	titleStyle = lipgloss.NewStyle().
			Foreground(lipgloss.Color("#888888"))
)

func (m Model) drawTitle() string {
	return titleStyle.Copy().Italic(true).Width(m.width).Align(lipgloss.Center).Render("Colors")
}

func (m Model) drawButtons() string {
	buttons := make([]string, len(stateOrder))
	for i, state := range stateOrder {
		style := normalStyle
		if m.IsActive && state == m.focus {
			style = focusStyle
		} else if state == m.selected {
			style = activeStyle
		}
		buttons[i] = style.Copy().AlignHorizontal(lipgloss.Center).Padding(0, 1).Render(stateNames[state])
	}
	return lipgloss.JoinHorizontal(lipgloss.Left, buttons...)
}

```

--------------------------------------------------------------------------------
/controls/browser/item.go:
--------------------------------------------------------------------------------

```go
package browser

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/charmbracelet/bubbles/list"
)

type item struct {
	name  string
	path  string
	isDir bool
	isTop bool
}

func (i item) FilterValue() string {
	return i.name
}

func (i item) Title() string {
	if i.isTop {
		return "↑"
	}
	if i.isDir {
		return fmt.Sprintf("%s/", i.name)
	}
	return i.name
}

func (i item) Description() string {
	if i.isDir {
		return "directory"
	}
	return "file"
}

func getItems(extensions map[string]bool, dir string) []list.Item {
	entries, err := os.ReadDir(dir)
	if err != nil {
		fmt.Println("Error reading directory entries:", err)
		os.Exit(1)
	}

	parentPath := filepath.Dir(dir)
	parentName := filepath.Base(parentPath)
	parentItem := item{name: parentName, path: parentPath, isDir: true, isTop: true}

	dirItems := []list.Item{parentItem}
	fileItems := make([]list.Item, 0)

	for _, e := range entries {
		path := fmt.Sprintf("%s/%s", dir, e.Name())

		if e.IsDir() {
			name := e.Name()
			dirItem := item{name: name, path: path, isDir: true, isTop: false}
			dirItems = append(dirItems, dirItem)
			continue
		}

		ext := filepath.Ext(e.Name())
		if _, ok := extensions[ext]; ok {
			fileItem := item{name: e.Name(), path: path, isDir: false, isTop: false}
			fileItems = append(fileItems, fileItem)
		}
	}

	return append(dirItems, fileItems...)
}

```

--------------------------------------------------------------------------------
/controls/export/model.go:
--------------------------------------------------------------------------------

```go
package export

import (
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/controls/export/destination"
	"github.com/Zebbeni/ansizalizer/controls/export/source"
)

type State int

const (
	None State = iota
	Source
	Destination
	Process
)

var (
	stateTitles = map[State]string{
		Source:      "Source",
		Destination: "Destination",
		Process:     "Process",
	}
)

type Model struct {
	active State
	focus  State

	Source      source.Model
	Destination destination.Model

	ShouldClose   bool
	ShouldUnfocus bool

	width int
}

func New(w int) Model {
	return Model{
		focus:         Source,
		active:        None,
		Source:        source.New(w - 2),
		Destination:   destination.New(w - 2),
		ShouldClose:   false,
		ShouldUnfocus: false,
		width:         w,
	}
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch m.active {
	case Source:
		return m.handleSourceUpdate(msg)
	case Destination:
		return m.handleDestinationUpdate(msg)
	}

	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}

	return m.handleKeyMsg(keyMsg)
}

func (m Model) View() string {
	src := m.renderWithBorder(m.Source.View(), Source)
	dst := m.renderWithBorder(m.Destination.View(), Destination)
	process := m.drawProcessButton()
	return lipgloss.JoinVertical(lipgloss.Left, src, dst, process)
}

```

--------------------------------------------------------------------------------
/controls/settings/advanced/dithering/model.go:
--------------------------------------------------------------------------------

```go
package dithering

import (
	"github.com/charmbracelet/bubbles/list"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/makeworld-the-better-one/dither/v2"
)

type State int

const (
	DitherOn State = iota
	DitherOff
	SerpentineOn
	SerpentineOff
	Matrix
)

type Model struct {
	focus State

	doDithering  bool
	doSerpentine bool
	matrix       dither.ErrorDiffusionMatrix

	list list.Model

	IsActive    bool
	ShouldClose bool

	width int
}

func New(w int) Model {
	return Model{
		focus:        DitherOff,
		doDithering:  false,
		doSerpentine: false,
		matrix:       dither.FloydSteinberg,
		list:         newMatrixMenu(w),
		ShouldClose:  false,
		IsActive:     false,
		width:        w,
	}
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	if m.focus == Matrix {
		return m.handleMatrixListUpdate(msg)
	}

	if keyMsg, ok := msg.(tea.KeyMsg); ok {
		return m.handleKeyMsg(keyMsg)
	}
	return m, nil
}

func (m Model) View() string {
	ditheringOpts := m.drawDitheringOptions()
	serpentineOpts := m.drawSerpentineOptions()
	matrixList := m.drawMatrix()
	content := lipgloss.JoinVertical(lipgloss.Left, ditheringOpts, serpentineOpts, matrixList)
	return lipgloss.NewStyle().Padding(0, 1).Render(content)
}

func (m Model) Settings() (bool, bool, dither.ErrorDiffusionMatrix) {
	return m.doDithering, m.doSerpentine, m.matrix
}

```

--------------------------------------------------------------------------------
/controls/browser/model.go:
--------------------------------------------------------------------------------

```go
package browser

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/charmbracelet/bubbles/key"
	"github.com/charmbracelet/bubbles/list"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/event"
)

type Model struct {
	SelectedDir  string
	SelectedFile string
	ActiveDir    string
	ActiveFile   string

	lists          []list.Model
	fileExtensions map[string]bool

	ShouldClose bool

	width int
}

func New(exts map[string]bool, w int) Model {
	dir, err := os.Getwd()
	if err != nil {
		fmt.Println("Error getting starting directory:", err)
		os.Exit(1)
	}

	m := Model{
		width:          w,
		fileExtensions: exts,
	}
	m = m.addListForDirectory(dir)

	return m
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, event.KeyMap.Esc):
			return m.handleEsc()
		case key.Matches(msg, event.KeyMap.Nav):
			return m.handleNav(msg)
		case key.Matches(msg, event.KeyMap.Enter):
			return m.handleEnter()
		}
	}
	return m, nil
}

func (m Model) currentList() list.Model {
	return m.lists[m.listIndex()]
}

func (m Model) listIndex() int {
	return len(m.lists) - 1
}

func (m Model) View() string {
	browser := m.currentList().View()
	return lipgloss.JoinVertical(lipgloss.Left, browser)
}

func (m Model) ActiveFilename() string {
	return filepath.Base(m.ActiveFile)
}

```

--------------------------------------------------------------------------------
/controls/export/destination/model.go:
--------------------------------------------------------------------------------

```go
package destination

import (
	"os"

	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/controls/browser"
	"github.com/Zebbeni/ansizalizer/event"
)

type State int

const (
	Input State = iota
	Browser
)

type Model struct {
	focus State

	Browser browser.Model

	selectedDir string

	ShouldClose   bool
	ShouldUnfocus bool

	IsActive bool

	width int
}

func New(w int) Model {
	filepath, _ := os.Getwd()

	return Model{
		focus: Input,

		Browser: browser.New(nil, w-2),

		selectedDir: filepath,

		width:       w,
		ShouldClose: false,
	}
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	switch m.focus {
	case Browser:
		return m.handleDstBrowserUpdate(msg)
	}

	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, event.KeyMap.Esc):
			return m.handleEsc()
		case key.Matches(msg, event.KeyMap.Nav):
			return m.handleNav(msg)
		case key.Matches(msg, event.KeyMap.Enter):
			return m.handleEnter()
		}
	}
	return m, cmd
}

func (m Model) View() string {
	content := make([]string, 0, 5)

	selected := lipgloss.NewStyle().PaddingTop(1).Render(m.drawSelected())
	content = append(content, selected)

	if m.focus == Browser {
		content = append(content, m.Browser.View())
	}

	return lipgloss.JoinVertical(lipgloss.Left, content...)
}

func (m Model) GetSelected() string {
	return m.selectedDir
}

```

--------------------------------------------------------------------------------
/controls/settings/model.go:
--------------------------------------------------------------------------------

```go
package settings

import (
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/controls/settings/advanced"
	"github.com/Zebbeni/ansizalizer/controls/settings/characters"
	"github.com/Zebbeni/ansizalizer/controls/settings/colors"
	"github.com/Zebbeni/ansizalizer/controls/settings/size"
)

type Model struct {
	active State
	focus  State

	Colors     colors.Model
	Characters characters.Model
	Size       size.Model
	Advanced   advanced.Model

	ShouldUnfocus bool
	ShouldClose   bool

	width int
}

func New(w int) Model {
	return Model{
		active: None,
		focus:  Colors,

		Colors:     colors.New(w),
		Characters: characters.New(w - 2),
		Size:       size.New(),
		Advanced:   advanced.New(w - 2),

		ShouldUnfocus: false,
		ShouldClose:   false,

		width: w,
	}
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch m.active {
	case Colors:
		return m.handleColorsUpdate(msg)
	case Characters:
		return m.handleCharactersUpdate(msg)
	case Size:
		return m.handleSizeUpdate(msg)
	case Advanced:
		return m.handleAdvancedUpdate(msg)
	}

	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}

	return m.handleKeyMsg(keyMsg)
}

func (m Model) View() string {
	colorCtrls := m.Colors.View()
	charCtrls := m.Characters.View()
	sizeCtrls := m.Size.View()
	sampCtrls := m.Advanced.View()

	col := m.renderWithBorder(colorCtrls, Colors)
	char := m.renderWithBorder(charCtrls, Characters)
	siz := m.renderWithBorder(sizeCtrls, Size)
	sam := m.renderWithBorder(sampCtrls, Advanced)

	return lipgloss.JoinVertical(lipgloss.Top, col, char, siz, sam)
}

```

--------------------------------------------------------------------------------
/controls/settings/advanced/model.go:
--------------------------------------------------------------------------------

```go
package advanced

import (
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/makeworld-the-better-one/dither/v2"
	"github.com/nfnt/resize"

	"github.com/Zebbeni/ansizalizer/controls/settings/advanced/dithering"
	"github.com/Zebbeni/ansizalizer/controls/settings/advanced/sampling"
	"github.com/Zebbeni/ansizalizer/event"
)

type State int

const (
	Menu State = iota
	Sampling
	Dithering
	SamplingControls
	DitheringControls
)

type Model struct {
	focus       State
	active      State
	activeTab   State
	sampling    sampling.Model
	dithering   dithering.Model
	ShouldClose bool
	IsActive    bool
	width       int
}

func New(w int) Model {
	return Model{
		focus:       Sampling,
		active:      Menu,
		activeTab:   Sampling,
		sampling:    sampling.New(w - 2),
		dithering:   dithering.New(w - 2),
		ShouldClose: false,
		IsActive:    false,
		width:       w,
	}
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch m.active {
	case SamplingControls:
		return m.handleSamplingUpdate(msg)
	case DitheringControls:
		return m.handleDitheringUpdate(msg)
	}

	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, event.KeyMap.Enter):
			return m.handleEnter()
		case key.Matches(msg, event.KeyMap.Nav):
			return m.handleNav(msg)
		case key.Matches(msg, event.KeyMap.Esc):
			return m.handleEsc()
		}
	}
	return m, nil
}

func (m Model) View() string {
	return m.drawTabs()
}

func (m Model) SamplingFunction() resize.InterpolationFunction {
	return m.sampling.Function
}

func (m Model) Dithering() (bool, bool, dither.ErrorDiffusionMatrix) {
	return m.dithering.Settings()
}

```

--------------------------------------------------------------------------------
/controls/settings/advanced/dithering/view.go:
--------------------------------------------------------------------------------

```go
package dithering

import (
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/style"
)

func (m Model) drawDitheringOptions() string {
	prompt := style.DimmedTitle.Render("Use Dithering:")
	prompt = lipgloss.NewStyle().Width(15).Render(prompt)

	nodeStyle := style.NormalButtonNode
	if m.IsActive && m.focus == DitherOn {
		nodeStyle = style.FocusButtonNode
	} else if m.doDithering {
		nodeStyle = style.ActiveButtonNode
	}
	onNode := lipgloss.NewStyle().Width(4).Render(nodeStyle.Copy().Render("On"))

	nodeStyle = style.NormalButtonNode
	if m.IsActive && m.focus == DitherOff {
		nodeStyle = style.FocusButtonNode
	} else if !m.doDithering {
		nodeStyle = style.ActiveButtonNode
	}
	offNode := nodeStyle.Copy().Render("Off")

	return lipgloss.JoinHorizontal(lipgloss.Left, prompt, onNode, offNode)
}

func (m Model) drawSerpentineOptions() string {
	prompt := style.DimmedTitle.Render("Do Serpentine:")
	prompt = lipgloss.NewStyle().Width(15).Render(prompt)

	nodeStyle := style.NormalButtonNode
	if m.IsActive && m.focus == SerpentineOn {
		nodeStyle = style.FocusButtonNode
	} else if m.doSerpentine {
		nodeStyle = style.ActiveButtonNode
	}
	onNode := lipgloss.NewStyle().Width(4).Render(nodeStyle.Copy().Render("On"))

	nodeStyle = style.NormalButtonNode
	if m.IsActive && m.focus == SerpentineOff {
		nodeStyle = style.FocusButtonNode
	} else if !m.doSerpentine {
		nodeStyle = style.ActiveButtonNode
	}
	offNode := nodeStyle.Copy().Render("Off")

	return lipgloss.JoinHorizontal(lipgloss.Left, prompt, onNode, offNode)
}

func (m Model) drawMatrix() string {
	prompt := style.DimmedTitle.Copy().PaddingTop(1).Render("Select Matrix")
	return lipgloss.JoinVertical(lipgloss.Left, prompt, m.list.View())
}

```

--------------------------------------------------------------------------------
/controls/update.go:
--------------------------------------------------------------------------------

```go
package controls

import (
	"os"

	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"

	"github.com/Zebbeni/ansizalizer/event"
)

type Direction int

const (
	Left Direction = iota
	Right
	Up
	Down
)

var navMap = map[Direction]map[State]State{
	Right: {Browse: Settings, Settings: Export},
	Left:  {Export: Settings, Settings: Browse},
}

func (m Model) handleOpenUpdate(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	m.FileBrowser, cmd = m.FileBrowser.Update(msg)

	if m.FileBrowser.ShouldClose {
		m.FileBrowser.ShouldClose = false
		m.active = Menu
	}
	return m, cmd
}

func (m Model) handleSettingsUpdate(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	m.Settings, cmd = m.Settings.Update(msg)

	if m.Settings.ShouldClose {
		m.Settings.ShouldClose = false
		m.active = Menu
	}

	return m, cmd
}

func (m Model) handleExportUpdate(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	m.Export, cmd = m.Export.Update(msg)

	if m.Export.ShouldClose {
		m.Export.ShouldClose = false
		m.active = Menu
	}

	return m, cmd
}

func (m Model) handleMenuUpdate(msg tea.Msg) (Model, tea.Cmd) {
	m.active = Menu
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, event.KeyMap.Enter):
			m.active = m.focus

		case key.Matches(msg, event.KeyMap.Nav):
			switch {
			case key.Matches(msg, event.KeyMap.Right):
				if next, hasNext := navMap[Right][m.focus]; hasNext {
					m.focus = next
				}
			case key.Matches(msg, event.KeyMap.Left):
				if next, hasNext := navMap[Left][m.focus]; hasNext {
					m.focus = next
				}
			}

		case key.Matches(msg, event.KeyMap.Esc):
			// Quit program if top-level menu is active and escape pressed
			tea.Quit()
			os.Exit(0)
		}
	}
	return m, nil
}

```

--------------------------------------------------------------------------------
/controls/model.go:
--------------------------------------------------------------------------------

```go
package controls

import (
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/controls/browser"
	"github.com/Zebbeni/ansizalizer/controls/export"
	"github.com/Zebbeni/ansizalizer/controls/settings"
	"github.com/Zebbeni/ansizalizer/global"
)

type State int

const (
	Menu State = iota
	Browse
	Settings
	Export

	numButtons = 3
)

var (
	stateOrder = []State{Browse, Settings, Export}
	stateNames = map[State]string{
		Browse:   "Browse",
		Settings: "Settings",
		Export:   "Export",
	}
)

type Model struct {
	active State
	focus  State

	FileBrowser browser.Model
	Settings    settings.Model
	Export      export.Model

	width int
}

func New(w int) Model {
	return Model{
		active: Menu,
		focus:  Browse,

		FileBrowser: browser.New(global.ImgExtensions, w),
		Settings:    settings.New(w),
		Export:      export.New(w),

		width: w,
	}
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch m.active {
	case Browse:
		return m.handleOpenUpdate(msg)
	case Settings:
		return m.handleSettingsUpdate(msg)
	case Export:
		return m.handleExportUpdate(msg)
	}
	return m.handleMenuUpdate(msg)
}

// View displays a row of 3 buttons above 1 of 3 control panels:
// Browse | Settings | Export
func (m Model) View() string {
	title := m.drawTitle()

	// draw the top three buttons
	buttons := m.drawButtons()
	var controls string

	switch m.active {
	case Browse:
		browserTitle := m.drawBrowserTitle()
		controls = lipgloss.JoinVertical(lipgloss.Left, browserTitle, m.FileBrowser.View())
	case Settings:
		controls = m.Settings.View()
	case Export:
		controls = m.Export.View()
	}

	return lipgloss.JoinVertical(lipgloss.Top, title, buttons, controls)
}

```

--------------------------------------------------------------------------------
/controls/settings/advanced/sampling/item.go:
--------------------------------------------------------------------------------

```go
package sampling

import (
	"github.com/charmbracelet/bubbles/list"
	"github.com/charmbracelet/lipgloss"
	"github.com/nfnt/resize"

	"github.com/Zebbeni/ansizalizer/style"
)

type item struct {
	name     string
	Function resize.InterpolationFunction
}

func (i item) FilterValue() string {
	return i.name
}

func (i item) Title() string {
	return i.name
}

func (i item) Description() string {
	return ""
}

func menuItems() []list.Item {
	items := make([]list.Item, len(nameMap))
	for i, f := range Functions {
		items[i] = item{name: nameMap[f], Function: f}
	}
	return items
}

func newMenu(items []list.Item, width, height int) list.Model {
	l := list.New(items, NewDelegate(false), width, height)
	l.SetShowHelp(false)
	l.SetFilteringEnabled(false)
	l.SetShowTitle(false)
	l.SetShowPagination(false)
	l.SetShowStatusBar(false)

	l.KeyMap.ForceQuit.Unbind()
	l.KeyMap.Quit.Unbind()

	return l
}

func NewDelegate(isActive bool) list.DefaultDelegate {
	delegate := list.NewDefaultDelegate()
	delegate.SetSpacing(0)
	delegate.ShowDescription = false
	if isActive {
		delegate.Styles = ItemStylesActive()
	} else {
		delegate.Styles = ItemStylesInactive()
	}
	return delegate
}

func ItemStylesActive() (s list.DefaultItemStyles) {
	s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2)
	s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1).
		Border(lipgloss.NormalBorder(), false, false, false, true).
		BorderForeground(style.SelectedColor1)
	s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0)
	return s
}

func ItemStylesInactive() (s list.DefaultItemStyles) {
	s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2)
	s.SelectedTitle = style.NormalTitle.Copy().Padding(0, 1, 0, 2)
	s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0)
	return s
}

```

--------------------------------------------------------------------------------
/app/view.go:
--------------------------------------------------------------------------------

```go
package app

import (
	"fmt"

	"github.com/charmbracelet/bubbles/help"
	"github.com/charmbracelet/bubbles/viewport"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/event"
	"github.com/Zebbeni/ansizalizer/style"
)

const (
	displayHeight = 3
	helpHeight    = 1

	controlsWidth = 30
)

func (m Model) renderControls() string {
	viewport := viewport.New(controlsWidth, m.leftPanelHeight())

	leftContent := m.controls.View()

	viewport.SetContent(lipgloss.NewStyle().
		Width(controlsWidth).
		Height(m.leftPanelHeight()).
		Render(leftContent))
	return viewport.View()
}

func (m Model) renderViewer() string {
	imgString := m.viewer.View()
	imgWidth, imgHeight := lipgloss.Size(imgString)

	imgViewer := imgString

	// only render box label border around content if big enough.
	if imgHeight > 1 && imgWidth > 4 {
		boxLabelRenderer := style.BoxWithLabel{
			BoxStyle:   lipgloss.NewStyle().BorderForeground(style.ExtraDimColor).Border(lipgloss.RoundedBorder()),
			LabelStyle: lipgloss.NewStyle().Foreground(style.ExtraDimColor).AlignHorizontal(lipgloss.Center).AlignVertical(lipgloss.Bottom),
		}
		imgViewer = boxLabelRenderer.Render(fmt.Sprintf("%dx%d", imgWidth, imgHeight), imgString, imgWidth)
	}

	renderViewport := viewport.New(m.rPanelWidth()-2, m.rPanelHeight()-displayHeight-2)

	vpRightStyle := lipgloss.NewStyle().Align(lipgloss.Center).AlignVertical(lipgloss.Center)
	rightContent := vpRightStyle.Copy().Width(m.rPanelWidth() - 2).Height(m.rPanelHeight() - 4).Render(imgViewer)
	renderViewport.SetContent(rightContent)

	content := renderViewport.View()

	return style.NormalButton.Copy().BorderForeground(style.DimmedColor1).Render(content)
}

func (m Model) renderHelp() string {
	helpBar := help.New()
	helpContent := helpBar.View(event.KeyMap)
	return lipgloss.NewStyle().PaddingLeft(1).Render(helpContent)
}

```

--------------------------------------------------------------------------------
/style/color.go:
--------------------------------------------------------------------------------

```go
package style

import "github.com/charmbracelet/lipgloss"

var (
	NormalColor1   = lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#aaaaaa"}
	NormalColor2   = lipgloss.AdaptiveColor{Light: "#3a3a3a", Dark: "#888888"}
	SelectedColor1 = lipgloss.AdaptiveColor{Light: "#444444", Dark: "#ffffff"}
	SelectedColor2 = lipgloss.AdaptiveColor{Light: "#666666", Dark: "#dddddd"}
	ExtraDimColor  = lipgloss.AdaptiveColor{Light: "#bbbbbb", Dark: "#444444"}
	DimmedColor1   = lipgloss.AdaptiveColor{Light: "#999999", Dark: "#777777"}
	DimmedColor2   = lipgloss.AdaptiveColor{Light: "#aaaaaa", Dark: "#666666"}

	NormalTitle     = lipgloss.NewStyle().Foreground(NormalColor1)
	NormalParagraph = lipgloss.NewStyle().Foreground(NormalColor2)

	SelectedTitle     = lipgloss.NewStyle().Foreground(SelectedColor1)
	SelectedParagraph = lipgloss.NewStyle().Foreground(SelectedColor2)

	DimmedTitle     = lipgloss.NewStyle().Foreground(DimmedColor1)
	ExtraDimTitle   = lipgloss.NewStyle().Foreground(ExtraDimColor)
	DimmedParagraph = lipgloss.NewStyle().Foreground(DimmedColor2)

	ActiveButton = lipgloss.NewStyle().
			BorderStyle(lipgloss.RoundedBorder()).
			BorderForeground(NormalColor1).
			Foreground(NormalColor1)
	FocusButton = lipgloss.NewStyle().
			BorderStyle(lipgloss.RoundedBorder()).
			BorderForeground(SelectedColor1).
			Foreground(SelectedColor1)
	NormalButton = lipgloss.NewStyle().
			BorderStyle(lipgloss.RoundedBorder()).
			BorderForeground(DimmedColor1).
			Foreground(DimmedColor1)

	ActiveButtonNode = lipgloss.NewStyle().
				PaddingLeft(1).
				Foreground(NormalColor1)
	FocusButtonNode = lipgloss.NewStyle().
			Border(lipgloss.RoundedBorder(), false, false, false, true).
			BorderForeground(SelectedColor1).
			Foreground(SelectedColor1).
			Padding(0)
	NormalButtonNode = lipgloss.NewStyle().
				PaddingLeft(1).
				Foreground(DimmedColor1)
)

```

--------------------------------------------------------------------------------
/palette/model.go:
--------------------------------------------------------------------------------

```go
package palette

import (
	"image/color"
	"math"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/lucasb-eyer/go-colorful"

	"github.com/Zebbeni/ansizalizer/style"
)

type Model struct {
	name   string
	colors color.Palette
	width  int
	height int
}

func New(name string, colors color.Palette, w, h int) Model {
	return Model{
		name:   name,
		colors: colors,
		width:  w,
		height: h,
	}
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	return m, nil
}

func (m Model) View() string {
	title := style.SelectedTitle.Render(m.name)
	description := m.Description()

	return lipgloss.JoinVertical(lipgloss.Top, title, description)
}

func (m Model) FilterValue() string {
	return m.name
}

func (m Model) Title() string {
	return m.name
}

func (m Model) Description() string {
	runes := make([]string, len(m.colors)/2+1)
	rows := make([]string, 0, m.height)
	for idx := 0; idx < len(m.colors); idx += 2 {
		var fg, bg colorful.Color
		var lipFg, lipBg lipgloss.Color

		fg, _ = colorful.MakeColor(m.colors[idx])
		lipFg = lipgloss.Color(fg.Hex())
		blockStyle := lipgloss.NewStyle().Foreground(lipFg)

		if idx+1 < len(m.colors) {
			bg, _ = colorful.MakeColor(m.colors[idx+1])
			lipBg = lipgloss.Color(bg.Hex())
			blockStyle = blockStyle.Copy().Background(lipBg)
		}
		runes[idx/2] = blockStyle.Render(string('▀'))
	}
	for i := 0; i < m.height; i++ {
		start := m.width * i
		if start >= len(runes) {
			break
		}
		stop := int(math.Min(float64(m.width*(i+1)), float64(len(runes))))
		rows = append(rows, "")
		rows[i] = lipgloss.JoinHorizontal(lipgloss.Left, runes[start:stop]...)
	}

	return lipgloss.JoinVertical(lipgloss.Left, rows...)
}

func (m Model) Name() string {
	return m.name
}

func (m Model) Colors() color.Palette {
	colorsCopy := make([]color.Color, len(m.colors))
	copy(colorsCopy, m.colors)
	return colorsCopy
}

```

--------------------------------------------------------------------------------
/controls/settings/colors/model.go:
--------------------------------------------------------------------------------

```go
package colors

import (
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/controls/settings/palettes"
	"github.com/Zebbeni/ansizalizer/event"
	"github.com/Zebbeni/ansizalizer/palette"
)

type State int

const (
	UsePalette State = iota
	UseTrueColor
	Palette
)

type Model struct {
	focus           State
	mode            State
	width           int
	PaletteControls palettes.Model

	IsActive    bool
	ShouldClose bool
}

func New(w int) Model {
	return Model{
		focus:           UseTrueColor,
		mode:            UseTrueColor,
		width:           w,
		PaletteControls: palettes.New(w),
		IsActive:        false,
		ShouldClose:     false,
	}
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch m.focus {
	case Palette:
		return m.handlePaletteUpdate(msg)
	}

	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, event.KeyMap.Enter):
			return m.handleEnter()
		case key.Matches(msg, event.KeyMap.Nav):
			return m.handleNav(msg)
		case key.Matches(msg, event.KeyMap.Esc):
			return m.handleEsc()
		}
	}
	return m, nil
}

func (m Model) View() string {
	paletteToggles := m.drawPaletteToggles()
	if m.mode == UseTrueColor {
		return paletteToggles
	}

	paletteTabs := m.PaletteControls.View()
	return lipgloss.JoinVertical(lipgloss.Left, paletteToggles, paletteTabs)
}

// GetSelected returns isPaletted, isAdaptive, and the palette (if applicable)
func (m Model) GetSelected() (bool, bool, palette.Model) {
	colorPalette := m.PaletteControls.GetCurrentPalette()

	if m.mode == UseTrueColor {
		return true, false, colorPalette
	}

	return false, m.PaletteControls.IsAdaptive(), colorPalette
}

func (m Model) GetCurrentPalette() palette.Model {
	return m.PaletteControls.GetCurrentPalette()
}

func (m Model) IsLimited() bool {
	return m.mode == UsePalette
}

```

--------------------------------------------------------------------------------
/controls/settings/colors/update.go:
--------------------------------------------------------------------------------

```go
package colors

import (
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"

	"github.com/Zebbeni/ansizalizer/event"
)

type Direction int

const (
	Left Direction = iota
	Right
	Up
	Down
)

var navMap = map[Direction]map[State]State{
	Right: {
		UseTrueColor: UsePalette,
	},
	Left: {
		UsePalette: UseTrueColor,
	},
	Up: {
		Palette: UsePalette,
	},
	Down: {
		UseTrueColor: Palette,
		UsePalette:   Palette,
	},
}

func (m Model) handlePaletteUpdate(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	m.PaletteControls, cmd = m.PaletteControls.Update(msg)

	if m.PaletteControls.ShouldClose {
		m.PaletteControls.IsActive = false
		m.PaletteControls.ShouldClose = false
		m.focus = UsePalette
	}
	return m, cmd
}

func (m Model) handleEnter() (Model, tea.Cmd) {
	switch m.focus {
	case UsePalette:
		m.mode = UsePalette
	case UseTrueColor:
		m.mode = UseTrueColor
	}
	return m, nil
}

func (m Model) handleEsc() (Model, tea.Cmd) {
	return m, nil
}

func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	switch {
	case key.Matches(msg, event.KeyMap.Right):
		if next, hasNext := navMap[Right][m.focus]; hasNext {
			return m.setFocus(next)
		}
	case key.Matches(msg, event.KeyMap.Left):
		if next, hasNext := navMap[Left][m.focus]; hasNext {
			return m.setFocus(next)
		}
	case key.Matches(msg, event.KeyMap.Up):
		if next, hasNext := navMap[Up][m.focus]; hasNext {
			return m.setFocus(next)
		} else {
			m.IsActive = false
			m.ShouldClose = true
		}
	case key.Matches(msg, event.KeyMap.Down):
		if next, hasNext := navMap[Down][m.focus]; hasNext {
			return m.setFocus(next)
		} else {
			m.IsActive = false
			m.ShouldClose = true
		}
	}
	return m, cmd
}

func (m Model) setFocus(focus State) (Model, tea.Cmd) {
	if m.mode == UseTrueColor && focus == Palette {
		return m, nil
	}

	m.focus = focus
	switch m.focus {
	case Palette:
		m.PaletteControls.IsActive = true
	}

	return m, nil
}

```

--------------------------------------------------------------------------------
/controls/export/source/model.go:
--------------------------------------------------------------------------------

```go
package source

import (
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/controls/browser"
	"github.com/Zebbeni/ansizalizer/event"
)

type State int

const (
	ExpFile State = iota
	ExpDirectory
	Input
	Browser
	SubDirsYes
	SubDirsNo
)

type Model struct {
	focus State

	doExportDirectory     bool
	includeSubdirectories bool

	Browser      browser.Model
	selectedDir  string
	selectedFile string

	ShouldClose   bool
	ShouldUnfocus bool

	IsActive bool

	width int
}

func New(w int) Model {
	browserModel := browser.New(nil, w-2)

	return Model{
		focus: ExpDirectory,

		Browser: browserModel,

		doExportDirectory:     true,
		includeSubdirectories: false,

		selectedDir:  "",
		selectedFile: "",

		width:       w,
		ShouldClose: false,
	}
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	switch m.focus {
	case Browser:
		return m.handleSrcBrowserUpdate(msg)
	}

	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, event.KeyMap.Esc):
			return m.handleEsc()
		case key.Matches(msg, event.KeyMap.Nav):
			return m.handleNav(msg)
		case key.Matches(msg, event.KeyMap.Enter):
			return m.handleEnter()
		}
	}
	return m, cmd
}

func (m Model) View() string {
	content := make([]string, 0, 5)
	content = append(content, m.drawExportTypeOptions())

	selected := lipgloss.NewStyle().PaddingTop(1).Render(m.drawSelected())
	content = append(content, selected)

	if m.focus == Browser {
		content = append(content, m.Browser.View())
	}

	if m.doExportDirectory {
		content = append(content, m.drawSubDirOptions())
	}

	return lipgloss.JoinVertical(lipgloss.Left, content...)
}

func (m Model) GetSelected() (path string, isDir, useSubDirs bool) {
	if m.doExportDirectory {
		isDir = true
		path = m.selectedDir
		useSubDirs = m.includeSubdirectories
	} else {
		path = m.selectedFile
		isDir = false
		useSubDirs = false
	}
	return
}

```

--------------------------------------------------------------------------------
/event/command.go:
--------------------------------------------------------------------------------

```go
package event

import (
	"fmt"
	"image/color"

	tea "github.com/charmbracelet/bubbletea"
)

type StartRenderToViewMsg bool

func StartRenderToViewCmd() tea.Msg {
	return StartRenderToViewMsg(true)
}

type FinishRenderToViewMsg struct {
	FilePath     string
	ImgString    string
	ColorsString string
}

type StartRenderToExportMsg bool

func StartRenderToExportCmd() tea.Msg {
	return StartRenderToExportMsg(true)
}

type FinishRenderToExportMsg struct {
	FilePath     string
	ImgString    string
	ColorsString string
}

func BuildFinishRenderToExportCmd(msg FinishRenderToExportMsg) tea.Cmd {
	return func() tea.Msg { return msg }
}

type StartAdaptingMsg bool

func StartAdaptingCmd() tea.Msg {
	return StartAdaptingMsg(true)
}

type FinishAdaptingMsg struct {
	Name   string
	Colors color.Palette
}

type StartExportMsg struct {
	SourcePath      string
	DestinationPath string
	IsDir           bool
	UseSubDirs      bool
}

func BuildStartExportCmd(msg StartExportMsg) tea.Cmd {
	return func() tea.Msg { return msg }
}

type FinishExportMsg bool

func FinishExportingCmd() tea.Msg {
	return FinishExportMsg(true)
}

// DisplayMsg could eventually contain a type
// that indicates what style to use (warning, error, etc.)
type DisplayMsg string

func BuildDisplayCmd(msg string) tea.Cmd {
	return func() tea.Msg { return DisplayMsg(msg) }
}

func ClearDisplayCmd() tea.Msg {
	return DisplayMsg("")
}

// LospecRequestMsg is a url request used to get a list of
type LospecRequestMsg struct {
	ID   int
	Page int
	URL  string
}

func BuildLospecRequestCmd(msg LospecRequestMsg) tea.Cmd {
	display := fmt.Sprintf("loading palettes")
	return tea.Batch(func() tea.Msg { return msg }, BuildDisplayCmd(display))
}

type LospecData struct {
	Palettes []struct {
		Colors []string `json:"colors"`
		Title  string   `json:"title"`
	} `json:"palettes"`
	TotalCount int `json:"totalCount"`
}

type LospecResponseMsg struct {
	ID   int
	Page int
	Data LospecData
}

func BuildLospecResponseCmd(msg LospecResponseMsg) tea.Cmd {
	return tea.Batch(func() tea.Msg { return msg }, ClearDisplayCmd)
}

```

--------------------------------------------------------------------------------
/controls/settings/characters/model.go:
--------------------------------------------------------------------------------

```go
package characters

import (
	"github.com/charmbracelet/bubbles/key"
	"github.com/charmbracelet/bubbles/textinput"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/event"
)

type State int

const (
	Ascii State = iota
	Unicode
	Custom
	AsciiAz
	AsciiNums
	AsciiSpec
	AsciiAll
	UnicodeFull
	UnicodeHalf
	UnicodeQuart
	UnicodeShadeLight
	UnicodeShadeMed
	UnicodeShadeHeavy
	SymbolsForm
	OneColor
	TwoColor
)

type Model struct {
	focus        State
	active       State
	mode         State
	charControls State
	unicodeMode  State
	asciiMode    State
	useFgBg      State
	customInput  textinput.Model
	ShouldClose  bool
	IsActive     bool
	width        int
}

func New(w int) Model {
	return Model{
		focus:        Unicode,
		active:       Unicode,
		mode:         Unicode,
		charControls: Unicode,
		asciiMode:    AsciiAz,
		unicodeMode:  UnicodeHalf,
		useFgBg:      TwoColor,
		customInput:  newInput("Symbols", "/%A"),
		ShouldClose:  false,
		IsActive:     false,
		width:        w,
	}
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch m.active {
	case SymbolsForm:
		if m.customInput.Focused() {
			return m.handleSymbolsFormUpdate(msg)
		}
	}

	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, event.KeyMap.Enter):
			return m.handleEnter()
		case key.Matches(msg, event.KeyMap.Nav):
			return m.handleNav(msg)
		case key.Matches(msg, event.KeyMap.Esc):
			return m.handleEsc()
		}
	}
	return m, nil
}

func (m Model) View() string {
	colorsButtons := m.drawColorsButtons()
	charTabs := m.drawCharTabs()
	return lipgloss.JoinVertical(lipgloss.Top, colorsButtons, charTabs)
}

// Selected returns the mode, charMode, whether to use two colors, and the
// current set of custom-defined characters
func (m Model) Selected() (State, State, State, []rune) {
	var charMode State

	switch m.mode {
	case Unicode:
		charMode = m.unicodeMode
	case Ascii:
		charMode = m.asciiMode
	case Custom:
		charMode = Custom
	}

	return m.mode, charMode, m.useFgBg, []rune(m.customInput.Value())
}

```

--------------------------------------------------------------------------------
/controls/export/source/update.go:
--------------------------------------------------------------------------------

```go
package source

import (
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"

	"github.com/Zebbeni/ansizalizer/controls/browser"
	"github.com/Zebbeni/ansizalizer/event"
	"github.com/Zebbeni/ansizalizer/global"
)

type Direction int

const (
	Left Direction = iota
	Right
	Up
	Down
)

var (
	navMap = map[Direction]map[State]State{
		Right: {ExpFile: ExpDirectory, SubDirsYes: SubDirsNo},
		Left:  {ExpDirectory: ExpFile, SubDirsNo: SubDirsYes},
		Down:  {ExpFile: Input, ExpDirectory: Input, Input: SubDirsYes},
		Up:    {Input: ExpFile, SubDirsYes: Input, SubDirsNo: Input},
	}
)

func (m Model) handleEsc() (Model, tea.Cmd) {
	m.ShouldClose = true
	m.IsActive = false
	return m, nil
}

func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
	switch {
	case key.Matches(msg, event.KeyMap.Right):
		if next, hasNext := navMap[Right][m.focus]; hasNext {
			m.focus = next
		}
	case key.Matches(msg, event.KeyMap.Left):
		if next, hasNext := navMap[Left][m.focus]; hasNext {
			m.focus = next
		}
	case key.Matches(msg, event.KeyMap.Down):
		if next, hasNext := navMap[Down][m.focus]; hasNext {
			m.focus = next
		} else {
			m.ShouldClose = true
		}
	case key.Matches(msg, event.KeyMap.Up):
		if next, hasNext := navMap[Up][m.focus]; hasNext {
			m.focus = next
		} else {
			m.ShouldClose = true
		}
	}
	return m, nil
}

func (m Model) handleEnter() (Model, tea.Cmd) {
	switch m.focus {
	case ExpFile:
		m.focus = Browser
		m.doExportDirectory = false
		m.Browser = browser.New(global.ImgExtensions, m.width)
	case ExpDirectory:
		m.focus = Browser
		m.doExportDirectory = true
		m.Browser = browser.New(nil, m.width)
	case Input:
		m.focus = Browser
	case SubDirsYes:
		m.includeSubdirectories = true
	case SubDirsNo:
		m.includeSubdirectories = false
	}
	return m, nil
}

func (m Model) handleSrcBrowserUpdate(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	m.Browser, cmd = m.Browser.Update(msg)
	if m.doExportDirectory {
		m.selectedDir = m.Browser.SelectedDir
	} else {
		m.selectedFile = m.Browser.SelectedFile
	}

	if m.Browser.ShouldClose {
		m.focus = Input
		m.Browser.ShouldClose = false
	}
	return m, cmd
}

func (m Model) handleIncludeSubdirectories(shouldInclude bool) (Model, tea.Cmd) {
	m.includeSubdirectories = shouldInclude
	return m, nil
}

```

--------------------------------------------------------------------------------
/controls/settings/palettes/adaptive/model.go:
--------------------------------------------------------------------------------

```go
package adaptive

import (
	"image/color"
	"strconv"

	"github.com/charmbracelet/bubbles/key"
	"github.com/charmbracelet/bubbles/textinput"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/event"
	"github.com/Zebbeni/ansizalizer/palette"
)

type State int

const (
	CountForm State = iota
	IterForm
	Generate
	Save
)

type Model struct {
	focus  State
	active State

	palette palette.Model

	countInput textinput.Model
	iterInput  textinput.Model

	width, height int

	ShouldClose   bool
	ShouldUnfocus bool
	IsActive      bool
	IsSelected    bool // true if we've selected something (ie. render w/ adaptive)
}

func New(w int) Model {
	return Model{
		focus: CountForm,

		countInput: newInput(CountForm),
		iterInput:  newInput(IterForm),

		ShouldUnfocus: false,
		IsActive:      false,
		IsSelected:    false,

		width: w,
	}
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch m.active {
	case CountForm:
		if m.countInput.Focused() {
			return m.handleCountUpdate(msg)
		}
	case IterForm:
		if m.iterInput.Focused() {
			return m.handleIterUpdate(msg)
		}
	}

	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, event.KeyMap.Enter):
			return m.handleEnter()
		case key.Matches(msg, event.KeyMap.Nav):
			return m.handleNav(msg)
		case key.Matches(msg, event.KeyMap.Esc):
			return m.handleEsc()
		}
	}
	return m, nil
}

func (m Model) View() string {
	title := m.drawTitle()
	inputs := m.drawInputs()
	generate := m.drawGenerateButton()
	if len(m.palette.Colors()) == 0 {
		return lipgloss.JoinVertical(lipgloss.Top, title, inputs, generate)
	}

	palette := lipgloss.NewStyle().Padding(0, 1, 0, 1).Render(m.palette.View())
	saveButton := m.drawSaveButton()
	content := lipgloss.JoinVertical(lipgloss.Top, title, inputs, generate, palette, saveButton)
	return content
}

func (m Model) Info() (int, int) {
	var count, iterations int
	count, _ = strconv.Atoi(m.countInput.Value())
	iterations, _ = strconv.Atoi(m.iterInput.Value())
	return count, iterations
}

func (m Model) GetCurrent() palette.Model {
	return m.palette
}

func (m Model) SetPalette(colors color.Palette, name string) Model {
	m.palette = palette.New(name, colors, m.width-4, 3)
	return m
}

```

--------------------------------------------------------------------------------
/controls/settings/advanced/update.go:
--------------------------------------------------------------------------------

```go
package advanced

import (
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"

	"github.com/Zebbeni/ansizalizer/event"
)

type Direction int

const (
	Left Direction = iota
	Right
	Up
	Down
)

var navMap = map[Direction]map[State]State{
	Right: {
		Sampling: Dithering,
	},
	Left: {
		Dithering: Sampling,
	},
	Down: {
		Sampling:  SamplingControls,
		Dithering: DitheringControls,
	},
	Up: {
		SamplingControls:  Sampling,
		DitheringControls: Dithering,
	},
}

func (m Model) handleSamplingUpdate(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	m.sampling, cmd = m.sampling.Update(msg)

	if m.sampling.ShouldClose {
		m.active = Menu
		m.focus = Sampling
		m.sampling.ShouldClose = false
		m.sampling.IsActive = false
	}
	return m, cmd
}

func (m Model) handleDitheringUpdate(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	m.dithering, cmd = m.dithering.Update(msg)

	if m.dithering.ShouldClose {
		m.active = Menu
		m.focus = Dithering
		m.dithering.ShouldClose = false
		m.dithering.IsActive = false
	}
	return m, cmd
}

func (m Model) handleEsc() (Model, tea.Cmd) {
	m.ShouldClose = true
	return m, nil
}

func (m Model) handleEnter() (Model, tea.Cmd) {
	m.active = m.focus
	return m, nil
}

func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	switch {
	case key.Matches(msg, event.KeyMap.Right):
		if next, hasNext := navMap[Right][m.focus]; hasNext {
			return m.setFocus(next)
		}
	case key.Matches(msg, event.KeyMap.Left):
		if next, hasNext := navMap[Left][m.focus]; hasNext {
			return m.setFocus(next)
		}
	case key.Matches(msg, event.KeyMap.Up):
		if next, hasNext := navMap[Up][m.focus]; hasNext {
			return m.setFocus(next)
		} else {
			m.IsActive = false
			m.ShouldClose = true
		}
	case key.Matches(msg, event.KeyMap.Down):
		if next, hasNext := navMap[Down][m.focus]; hasNext {
			return m.setFocus(next)
		} else {
			m.IsActive = false
			m.ShouldClose = true
		}
	}
	return m, cmd
}

func (m Model) setFocus(focus State) (Model, tea.Cmd) {
	m.focus = focus
	switch m.focus {
	case Sampling:
		m.activeTab = Sampling
	case Dithering:
		m.activeTab = Dithering
	case SamplingControls:
		m.active = SamplingControls
		m.sampling.IsActive = true
	case DitheringControls:
		m.active = DitheringControls
		m.dithering.IsActive = true
	}
	return m, nil
}

```

--------------------------------------------------------------------------------
/controls/settings/size/model.go:
--------------------------------------------------------------------------------

```go
package size

import (
	"strconv"

	"github.com/charmbracelet/bubbles/key"
	"github.com/charmbracelet/bubbles/textinput"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/event"
)

const DEFAULT_CHAR_W_TO_H_RATIO = 0.5

type State int
type Mode int

const (
	Fit Mode = iota
	Stretch
)

const (
	FitButton State = iota
	StretchButton
	WidthForm
	HeightForm
	CharRatioForm
	None
)

type Model struct {
	focus  State
	active State
	mode   Mode

	widthInput     textinput.Model
	heightInput    textinput.Model
	charRatioInput textinput.Model

	ShouldUnfocus bool
	ShouldClose   bool
	IsActive      bool
}

func New() Model {
	return Model{
		focus:          FitButton,
		active:         None,
		mode:           Fit,
		widthInput:     newInput(WidthForm, 50),
		heightInput:    newInput(HeightForm, 40),
		charRatioInput: newFloatInput(CharRatioForm, DEFAULT_CHAR_W_TO_H_RATIO),

		ShouldUnfocus: false,
		ShouldClose:   false,
		IsActive:      false,
	}
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	var cmd1, cmd2 tea.Cmd
	newM := m

	switch m.active {
	case WidthForm:
		if m.widthInput.Focused() {
			newM, cmd1 = newM.handleWidthUpdate(msg)
		}
	case HeightForm:
		if m.heightInput.Focused() {
			newM, cmd1 = newM.handleHeightUpdate(msg)
		}
	case CharRatioForm:
		if m.charRatioInput.Focused() {
			newM, cmd1 = newM.handleCharRatioUpdate(msg)
		}
	}

	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, event.KeyMap.Enter):
			newM, cmd2 = newM.handleEnter()
		case key.Matches(msg, event.KeyMap.Nav):
			newM, cmd2 = newM.handleNav(msg)
		case key.Matches(msg, event.KeyMap.Esc):
			newM, cmd2 = newM.handleEsc()
		}
	}
	return newM, tea.Batch(cmd1, cmd2)
}

func (m Model) View() string {
	buttonRow := m.drawButtons()
	forms := m.drawSizeForms()
	ratioForm := m.drawCharRatioForm()
	return lipgloss.JoinVertical(lipgloss.Left, buttonRow, forms, ratioForm)
}

func (m Model) Info() (Mode, int, int, float64) {
	var width, height int
	width, _ = strconv.Atoi(m.widthInput.Value())
	height, _ = strconv.Atoi(m.heightInput.Value())
	charRatio, err := strconv.ParseFloat(m.charRatioInput.Value(), 64)
	if err != nil {
		charRatio = DEFAULT_CHAR_W_TO_H_RATIO
	}
	return m.mode, width, height, charRatio
}

```

--------------------------------------------------------------------------------
/controls/browser/update.go:
--------------------------------------------------------------------------------

```go
package browser

import (
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"

	"github.com/Zebbeni/ansizalizer/controls/menu"
	"github.com/Zebbeni/ansizalizer/event"
)

func (m Model) handleEnter() (Model, tea.Cmd) {
	return m.updateSelected()
}

func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
	if m.currentList().Index() == 0 && key.Matches(msg, event.KeyMap.Up) {
		m.ShouldClose = true
		return m, nil
	}

	cmds := make([]tea.Cmd, 2)
	m.lists[m.listIndex()], cmds[0] = m.currentList().Update(msg)
	m, cmds[1] = m.updateActive()
	return m, tea.Batch(cmds...)
}

func (m Model) handleEsc() (Model, tea.Cmd) {
	// remove last list if possible (go back to previous)
	if len(m.lists) > 1 {
		m.lists = m.lists[:m.listIndex()]
		return m, nil
	}

	m.ShouldClose = true
	return m, nil
}

func (m Model) updateActive() (Model, tea.Cmd) {
	itm, ok := m.currentList().SelectedItem().(item)
	if !ok {
		panic("Unexpected list item type")
	}

	if itm.isDir && m.ActiveDir != itm.path {
		m.ActiveDir = itm.path
		return m, nil
	}

	if itm.isDir == false && m.ActiveFile != itm.path {
		m.ActiveFile = itm.path
		return m, event.StartRenderToViewCmd
	}

	return m, nil
}

func (m Model) updateSelected() (Model, tea.Cmd) {
	itm, ok := m.currentList().SelectedItem().(item)
	if !ok {
		panic("Unexpected list item type")
	}

	if itm.isDir {
		m.SelectedDir = itm.path
		m = m.addListForDirectory(itm.path)
	} else {
		m.SelectedFile = itm.path
		m.ShouldClose = true
	}

	return m, nil
}

func (m Model) addListForDirectory(dir string) Model {
	newList := menu.New(getItems(m.fileExtensions, dir), m.width)

	newList.SetShowTitle(false)

	//title := filepath.Join(filepath.Base(filepath.Dir(dir)), filepath.Base(dir))

	//newList.Title = fitString(title, m.width-10)
	//newList.Styles.Title = newList.Styles.Title.Copy().Foreground(style.DimmedColor2).UnsetBackground()
	//newList.Styles.TitleBar = newList.Styles.TitleBar.Copy().Padding(0).Height(2)
	newList.SetShowStatusBar(false)
	newList.SetFilteringEnabled(false)
	newList.SetShowFilter(false)
	newList.SetWidth(m.width)

	m.lists = append(m.lists, newList)
	m.SelectedDir = dir

	return m
}

func fitString(value string, width int) string {
	valueRunes := []rune(value)

	start := len(valueRunes) - width - 2
	if start < 0 {
		start = 0
	}

	if len(valueRunes) > width {
		value = "\n.." + string(valueRunes[start:])
	}

	return value
}

```

--------------------------------------------------------------------------------
/controls/export/update.go:
--------------------------------------------------------------------------------

```go
package export

import (
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"

	"github.com/Zebbeni/ansizalizer/event"
)

type Direction int

const (
	Down Direction = iota
	Up
)

var navMap = map[Direction]map[State]State{
	Down: {Source: Destination, Destination: Process},
	Up:   {Destination: Source, Process: Destination},
}

func (m Model) handleSourceUpdate(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	m.Source, cmd = m.Source.Update(msg)

	if m.Source.ShouldClose {
		m.active = None
		m.Source.ShouldClose = false
	}
	if m.Source.ShouldUnfocus {
		return m.handleMenuUpdate(msg)
	}
	return m, cmd
}

func (m Model) handleDestinationUpdate(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	m.Destination, cmd = m.Destination.Update(msg)

	if m.Destination.ShouldClose {
		m.active = None
		m.Destination.ShouldClose = false
	}
	return m, cmd
}

func (m Model) handleEnter() (Model, tea.Cmd) {
	m.active = m.focus
	switch m.active {
	case Source:
		m.Source.IsActive = true
	case Destination:
		m.Destination.IsActive = true
	case Process:
		return m.handleProcess()
	}
	return m, nil
}

func (m Model) handleEsc() (Model, tea.Cmd) {
	m.ShouldClose = true
	return m, nil
}

func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
	switch {
	case key.Matches(msg, event.KeyMap.Down):
		if next, hasNext := navMap[Down][m.focus]; hasNext {
			m.focus = next
		} else {
			m.ShouldClose = true
		}
	case key.Matches(msg, event.KeyMap.Up):
		if next, hasNext := navMap[Up][m.focus]; hasNext {
			m.focus = next
		} else {
			m.ShouldClose = true
		}
	}
	return m, nil
}

func (m Model) handleMenuUpdate(msg tea.Msg) (Model, tea.Cmd) {
	if keyMsg, ok := msg.(tea.KeyMsg); ok {
		return m.handleKeyMsg(keyMsg)
	}
	return m, nil
}

func (m Model) handleKeyMsg(msg tea.KeyMsg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	switch {
	case key.Matches(msg, event.KeyMap.Enter):
		return m.handleEnter()
	case key.Matches(msg, event.KeyMap.Nav):
		return m.handleNav(msg)
	case key.Matches(msg, event.KeyMap.Esc):
		return m.handleEsc()
	}
	return m, cmd
}

func (m Model) handleProcess() (Model, tea.Cmd) {
	sourcePath, isDir, useSubDirs := m.Source.GetSelected()
	destinationPath := m.Destination.GetSelected()
	return m, event.BuildStartExportCmd(event.StartExportMsg{
		SourcePath:      sourcePath,
		DestinationPath: destinationPath,
		IsDir:           isDir,
		UseSubDirs:      useSubDirs,
	})
}

func (m Model) GetDestination() (path string) {
	return m.Destination.GetSelected()
}

```

--------------------------------------------------------------------------------
/controls/settings/palettes/loader/model.go:
--------------------------------------------------------------------------------

```go
package loader

import (
	"bufio"
	"fmt"
	"image/color"
	"os"
	"path/filepath"
	"strings"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/lucasb-eyer/go-colorful"

	"github.com/Zebbeni/ansizalizer/controls/browser"
	"github.com/Zebbeni/ansizalizer/event"
	"github.com/Zebbeni/ansizalizer/palette"
	"github.com/Zebbeni/ansizalizer/style"
)

var (
	paletteExtensions = map[string]bool{".hex": true}
)

type Model struct {
	FileBrowser browser.Model

	paletteFilepath string
	palette         palette.Model

	IsSelected    bool // true if we've selected something (ie. render w/ loader)
	ShouldUnfocus bool

	width int
}

func New(w int) Model {
	fileBrowser := browser.New(paletteExtensions, w-2)

	return Model{
		FileBrowser:   fileBrowser,
		IsSelected:    false,
		ShouldUnfocus: false,
		width:         w,
	}
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd

	m.FileBrowser, cmd = m.FileBrowser.Update(msg)

	if m.FileBrowser.ActiveFile != m.paletteFilepath {
		m.paletteFilepath = m.FileBrowser.ActiveFile

		name := strings.Split(filepath.Base(m.paletteFilepath), ".hex")[0]
		colors, err := parsePaletteFile(m.paletteFilepath)
		if err != nil {
			return m, tea.Batch(cmd, event.BuildDisplayCmd("error parsing paletteFilepath file"))
		}
		m.palette = palette.New(name, colors, m.width-5, 3)

		m.IsSelected = true
		return m, tea.Batch(cmd, event.StartRenderToViewCmd)
	}

	if m.FileBrowser.ShouldClose {
		m.IsSelected = false
		m.FileBrowser.ShouldClose = false
		m.ShouldUnfocus = true
	}

	return m, cmd
}

func (m Model) View() string {
	activePreview := style.DimmedTitle.Render("No palette selected")
	if len(m.palette.Colors()) != 0 {
		activePreview = m.palette.View()
	}
	activePreview = lipgloss.NewStyle().Padding(0, 0, 1, 2).Render(activePreview)

	title := m.drawTitle()
	browser := m.FileBrowser.View()
	return lipgloss.JoinVertical(lipgloss.Top, title, browser, activePreview)
}

func (m Model) GetCurrent() palette.Model {
	return m.palette
}

func parsePaletteFile(filepath string) (color.Palette, error) {
	readFile, err := os.Open(filepath)
	if err != nil {
		return nil, err
	}

	fileScanner := bufio.NewScanner(readFile)
	fileScanner.Split(bufio.ScanLines)

	var col colorful.Color
	p := make(color.Palette, 0, 256)

	for fileScanner.Scan() {
		col, err = colorful.Hex(fmt.Sprintf("#%s", fileScanner.Text()))
		if err != nil {
			return nil, err
		}
		p = append(p, col)
	}

	return p, nil
}

```

--------------------------------------------------------------------------------
/controls/settings/palettes/model.go:
--------------------------------------------------------------------------------

```go
package palettes

import (
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/controls/settings/palettes/adaptive"
	"github.com/Zebbeni/ansizalizer/controls/settings/palettes/loader"
	"github.com/Zebbeni/ansizalizer/controls/settings/palettes/lospec"
	"github.com/Zebbeni/ansizalizer/palette"
)

type State int

// None consists of a few different components that are shown or hidden
// depending on which toggles have been set on / off. The Model state indicates
// which component is currently focused. From top to bottom the components are:

// 1) Limited (on/off)
// 2) Loader (Name) (if Limited) -> [Enter] displays Loader menu
// 3) Dithering (on/off) (if Limited)
// 4) Serpentine (on/off) (if Dithering)
// 5) Matrix (Name) (if Dithering) -> [Enter] displays to Matrix menu

// These can all be part of a single list, but we need to onSelect the list items

const (
	Adapt State = iota
	Load
	Lospec
	AdaptiveControls
	LoadControls
	LospecControls
)

type Model struct {
	selected State
	focus    State // the component taking input
	controls State

	Adapter adaptive.Model
	Loader  loader.Model
	Lospec  lospec.Model

	ShouldClose bool

	IsActive bool

	width int
}

func New(w int) Model {
	m := Model{
		selected:    Load,
		focus:       Load,
		controls:    Load,
		Adapter:     adaptive.New(w),
		Loader:      loader.New(w),
		Lospec:      lospec.New(w),
		ShouldClose: false,
		IsActive:    false,
		width:       w,
	}
	return m
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch m.focus {
	case AdaptiveControls:
		return m.handleAdaptiveUpdate(msg)
	case LoadControls:
		return m.handleLoaderUpdate(msg)
	case LospecControls:
		return m.handleLospecUpdate(msg)
	}
	return m.handleMenuUpdate(msg)
}

func (m Model) View() string {
	buttons := m.drawButtons()
	if m.IsActive == false {
		return buttons
	}

	var controls string
	switch m.controls {
	case Adapt:
		controls = m.Adapter.View()
	case Load:
		controls = m.Loader.View()
	case Lospec:
		controls = m.Lospec.View()
	}
	if len(controls) == 0 {
		return buttons
	}

	return lipgloss.JoinVertical(lipgloss.Top, buttons, controls)
}

func (m Model) IsAdaptive() bool {
	return m.selected == Adapt
}

func (m Model) IsPaletted() bool {
	return m.selected == Load
}

func (m Model) GetCurrentPalette() palette.Model {
	switch m.selected {
	case Load:
		return m.Loader.GetCurrent()
	case Adapt:
		return m.Adapter.GetCurrent()
	case Lospec:
		return m.Lospec.GetCurrent()
	}
	return palette.Model{}
}

```

--------------------------------------------------------------------------------
/controls/settings/update.go:
--------------------------------------------------------------------------------

```go
package settings

import (
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"

	"github.com/Zebbeni/ansizalizer/event"
)

type Direction int

const (
	Down Direction = iota
	Up
)

var navMap = map[Direction]map[State]State{
	Down: {Colors: Characters, Characters: Size, Size: Advanced},
	Up:   {Advanced: Size, Size: Characters, Characters: Colors},
}

func (m Model) handleSettingsUpdate(msg tea.Msg) (Model, tea.Cmd) {
	if keyMsg, ok := msg.(tea.KeyMsg); ok {
		return m.handleKeyMsg(keyMsg)
	}
	return m, nil
}

func (m Model) handleColorsUpdate(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	m.Colors, cmd = m.Colors.Update(msg)

	if m.Colors.ShouldClose {
		m.active = None
		m.Colors.IsActive = false
		m.Colors.ShouldClose = false
	}
	return m, cmd
}

func (m Model) handleCharactersUpdate(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	m.Characters, cmd = m.Characters.Update(msg)

	if m.Characters.ShouldClose {
		m.active = None
		m.Characters.IsActive = false
		m.Characters.ShouldClose = false
	}
	return m, cmd
}

func (m Model) handleSizeUpdate(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	m.Size, cmd = m.Size.Update(msg)
	if m.Size.ShouldClose {
		m.active = None
		m.Size.IsActive = false
		m.Size.ShouldClose = false
	}
	if m.Size.ShouldUnfocus {
		return m.handleSettingsUpdate(msg)
	}
	return m, cmd
}

func (m Model) handleAdvancedUpdate(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	m.Advanced, cmd = m.Advanced.Update(msg)

	if m.Advanced.ShouldClose {
		m.active = None
		m.Advanced.ShouldClose = false
	}
	return m, cmd
}

func (m Model) handleEnter() (Model, tea.Cmd) {
	m.active = m.focus
	switch m.active {
	case Colors:
		m.Colors.IsActive = true
	case Characters:
		m.Characters.IsActive = true
	case Size:
		m.Size.IsActive = true
	case Advanced:
		m.Advanced.IsActive = true
	}
	return m, nil
}

func (m Model) handleEsc() (Model, tea.Cmd) {
	m.ShouldClose = true
	return m, nil
}

func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
	switch {
	case key.Matches(msg, event.KeyMap.Down):
		if next, hasNext := navMap[Down][m.focus]; hasNext {
			m.focus = next
		}
	case key.Matches(msg, event.KeyMap.Up):
		if next, hasNext := navMap[Up][m.focus]; hasNext {
			m.focus = next
		} else {
			m.ShouldClose = true
		}
	}
	return m, nil
}

func (m Model) handleKeyMsg(msg tea.KeyMsg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	switch {
	case key.Matches(msg, event.KeyMap.Enter):
		return m.handleEnter()
	case key.Matches(msg, event.KeyMap.Nav):
		return m.handleNav(msg)
	case key.Matches(msg, event.KeyMap.Esc):
		return m.handleEsc()
	}
	return m, cmd
}

```

--------------------------------------------------------------------------------
/style/box.go:
--------------------------------------------------------------------------------

```go
package style

import (
	"strings"

	"github.com/charmbracelet/lipgloss"
)

type BoxWithLabel struct {
	BoxStyle   lipgloss.Style
	LabelStyle lipgloss.Style
}

func NewDefaultBoxWithLabel() BoxWithLabel {
	return BoxWithLabel{
		BoxStyle: lipgloss.NewStyle().
			Border(lipgloss.RoundedBorder()).
			BorderForeground(lipgloss.Color("63")),

		// You could, of course, also set background and foreground colors here
		// as well.
		LabelStyle: lipgloss.NewStyle().
			AlignHorizontal(lipgloss.Center).
			PaddingTop(0).
			PaddingBottom(0),
	}
}

func (b BoxWithLabel) Render(label, content string, width int) string {
	var (
		// Query the box style for some of its border properties so we can
		// essentially take the top border apart and put it around the label.
		border             lipgloss.Border     = b.BoxStyle.GetBorderStyle()
		topBorderStyler    func(string) string = lipgloss.NewStyle().Foreground(b.BoxStyle.GetBorderTopForeground()).Render
		bottomBorderStyler func(string) string = lipgloss.NewStyle().Foreground(b.BoxStyle.GetBorderBottomForeground()).Render
		topLeft            string              = topBorderStyler(border.TopLeft)
		topRight           string              = topBorderStyler(border.TopRight)
		botLeft            string              = bottomBorderStyler(border.BottomLeft)
		botRight           string              = bottomBorderStyler(border.BottomRight)

		renderedLabel string = b.LabelStyle.Render(label)
	)

	// Render top row with the label
	borderWidth := b.BoxStyle.GetHorizontalBorderSize()
	cellsShort := max(0, width+borderWidth-lipgloss.Width(topLeft+topRight+renderedLabel))

	gap := strings.Repeat(border.Top, cellsShort)
	var gapLeft, gapRight string
	switch b.LabelStyle.GetAlignHorizontal() {
	case lipgloss.Left:
		gapRight = gap
	case lipgloss.Right:
		gapLeft = gap
	case lipgloss.Center:
		gapLeft = strings.Repeat(border.Top, cellsShort/2)
		gapRight = strings.Repeat(border.Top, cellsShort-(cellsShort/2))
	}

	var top, bottom string

	switch b.LabelStyle.GetAlignVertical() {
	case lipgloss.Top:
		strings.Repeat(border.Top, cellsShort)
		top = topLeft + topBorderStyler(gapLeft) + renderedLabel + topBorderStyler(gapRight) + topRight
		bottom = b.BoxStyle.Copy().
			BorderTop(false).
			Width(width).
			Render(content)
	case lipgloss.Bottom:
		strings.Repeat(border.Bottom, cellsShort)
		bottom = botLeft + bottomBorderStyler(gapLeft) + renderedLabel + bottomBorderStyler(gapRight) + botRight
		top = b.BoxStyle.Copy().
			BorderBottom(false).
			Width(width).
			Render(content)
	}

	// Stack the pieces
	return top + "\n" + bottom
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

```

--------------------------------------------------------------------------------
/controls/settings/advanced/dithering/update.go:
--------------------------------------------------------------------------------

```go
package dithering

import (
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"

	"github.com/Zebbeni/ansizalizer/event"
)

type Direction int

const (
	Left Direction = iota
	Right
	Up
	Down
)

var navMap = map[Direction]map[State]State{
	Right: {
		DitherOn:     DitherOff,
		SerpentineOn: SerpentineOff,
	},
	Left: {
		DitherOff:     DitherOn,
		SerpentineOff: SerpentineOn,
	},
	Down: {
		DitherOn:      SerpentineOn,
		DitherOff:     SerpentineOff,
		SerpentineOn:  Matrix,
		SerpentineOff: Matrix,
	},
	Up: {
		SerpentineOn:  DitherOn,
		SerpentineOff: DitherOff,
		Matrix:        SerpentineOn,
	},
}

func (m Model) handleMatrixListUpdate(msg tea.Msg) (Model, tea.Cmd) {
	if keyMsg, ok := msg.(tea.KeyMsg); ok {
		switch {
		case key.Matches(keyMsg, event.KeyMap.Up) && m.list.Index() == 0:
			return m.handleNav(keyMsg)
		case key.Matches(keyMsg, event.KeyMap.Esc):
		case key.Matches(keyMsg, event.KeyMap.Enter):
			var cmd tea.Cmd
			m, cmd = m.setFocus(navMap[Up][Matrix])
			return m, tea.Batch(cmd, event.StartRenderToViewCmd)
		}
	}

	var cmd tea.Cmd
	m.list, cmd = m.list.Update(msg)
	return m, cmd
}

func (m Model) handleEsc() (Model, tea.Cmd) {
	m.ShouldClose = true
	return m, nil
}

func (m Model) handleEnter() (Model, tea.Cmd) {
	switch m.focus {
	case DitherOn:
		m.doDithering = true
	case DitherOff:
		m.doDithering = false
	case SerpentineOn:
		m.doSerpentine = true
	case SerpentineOff:
		m.doSerpentine = false
	}
	return m, event.StartRenderToViewCmd
}

func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	switch {
	case key.Matches(msg, event.KeyMap.Right):
		if next, hasNext := navMap[Right][m.focus]; hasNext {
			return m.setFocus(next)
		}
	case key.Matches(msg, event.KeyMap.Left):
		if next, hasNext := navMap[Left][m.focus]; hasNext {
			return m.setFocus(next)
		}
	case key.Matches(msg, event.KeyMap.Up):
		if next, hasNext := navMap[Up][m.focus]; hasNext {
			return m.setFocus(next)
		} else {
			m.ShouldClose = true
		}
	case key.Matches(msg, event.KeyMap.Down):
		if next, hasNext := navMap[Down][m.focus]; hasNext {
			return m.setFocus(next)
		} else {
			m.ShouldClose = true
		}
	}
	return m, cmd
}

func (m Model) handleKeyMsg(msg tea.KeyMsg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	switch {
	case key.Matches(msg, event.KeyMap.Enter):
		return m.handleEnter()
	case key.Matches(msg, event.KeyMap.Nav):
		return m.handleNav(msg)
	case key.Matches(msg, event.KeyMap.Esc):
		return m.handleEsc()
	}
	return m, cmd
}

func (m Model) setFocus(focus State) (Model, tea.Cmd) {
	m.focus = focus
	if focus != Matrix {
		m.list.SetDelegate(NewDelegate(false))
	} else {
		m.list.SetDelegate(NewDelegate(true))
	}

	return m, nil
}

```

--------------------------------------------------------------------------------
/app/export.go:
--------------------------------------------------------------------------------

```go
package app

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/Zebbeni/ansizalizer/global"
)

const (
	maxExportJobs = 1000
)

type exportJob struct {
	sourcePath      string
	destinationPath string
}

type MaxExportQueueError struct {
	count int
}

func (r *MaxExportQueueError) Error() string {
	return fmt.Sprintf("%d+ export jobs exceed %d max", r.count, maxExportJobs)
}

// this process may get more complicated if we want to do animated gifs,
// since each gif  will require multiple image exports.
func buildExportQueue(dirPath, destPath string, useSubDirs bool) ([]exportJob, error) {
	// for each image file found in the dirPath, append an exportJob object
	// with the source filepath and its corresponding .ansi destination filepath
	entries, err := os.ReadDir(dirPath)
	if err != nil {
		return nil, err
	}

	exportJobs := make([]exportJob, 0, len(entries))
	subDirs := make([]string, 0, len(entries))

	for _, e := range entries {
		sourcePath := filepath.Join(dirPath, e.Name())

		if e.IsDir() {
			subDirs = append(subDirs, sourcePath)
			continue
		}

		ext := filepath.Ext(e.Name())
		if _, ok := global.ImgExtensions[ext]; ok {
			nameWithoutExt := strings.Split(filepath.Base(sourcePath), ".")[0]
			nameWithExt := fmt.Sprintf("%s.ansi", nameWithoutExt)
			destFilePath := filepath.Join(destPath, nameWithExt)
			exportJobs = append(exportJobs, exportJob{
				sourcePath:      sourcePath,
				destinationPath: destFilePath,
			})
		}
	}

	if useSubDirs {
		// call buildExportQueue on each subdirectory in dirPath, creating
		// subdirectories in the destination path to mimic the source directory
		// structure, and providing these subdirectory paths to the build call as well
		for _, subDir := range subDirs {

			subDirName := filepath.Base(subDir)
			subDestPath := filepath.Join(destPath, subDirName)

			var subDirExportJobs []exportJob
			subDirExportJobs, err = buildExportQueue(subDir, subDestPath, true)
			if err != nil {
				return nil, err
			}

			// append resulting exportJob lists to the main list
			exportJobs = append(exportJobs, subDirExportJobs...)
			if len(exportJobs) > maxExportJobs {
				return nil, &MaxExportQueueError{count: len(exportJobs)}
			}

			// skip creating mirrored subdirectories if no files found there
			if len(subDirExportJobs) == 0 {
				continue
			}

			// create the destination folder if it doesn't already exist
			// do this after the recursive call to buildExportQueue. Otherwise,
			// we can hit an infinite loop where our newly created directories
			// get picked up by subsequent buildExportQueue calls, forever.
			if _, err = os.Stat(subDestPath); os.IsNotExist(err) {
				err = os.MkdirAll(subDestPath, os.ModeDir)
				if err != nil {
					return nil, err
				}
			}
		}
	}

	return exportJobs, nil
}

```

--------------------------------------------------------------------------------
/app/process/custom.go:
--------------------------------------------------------------------------------

```go
package process

import (
	"image"
	"math"

	"github.com/charmbracelet/lipgloss"
	"github.com/lucasb-eyer/go-colorful"
	"github.com/makeworld-the-better-one/dither/v2"
	"github.com/nfnt/resize"

	"github.com/Zebbeni/ansizalizer/controls/settings/characters"
	"github.com/Zebbeni/ansizalizer/controls/settings/size"
)

func (m Renderer) processCustom(input image.Image) string {
	imgW, imgH := float32(input.Bounds().Dx()), float32(input.Bounds().Dy())

	dimensionType, width, height, charRatio := m.Settings.Size.Info()
	if dimensionType == size.Fit {
		fitHeight := float32(width) * (imgH / imgW) * float32(charRatio)
		fitWidth := (float32(height) * (imgW / imgH)) / float32(charRatio)
		if fitHeight > float32(height) {
			width = int(fitWidth)
		} else {
			height = int(fitHeight)
		}
	}

	resizeFunc := m.Settings.Advanced.SamplingFunction()
	refImg := resize.Resize(uint(width)*2, uint(height)*2, input, resizeFunc)

	isTrueColor, _, palette := m.Settings.Colors.GetSelected()
	isPaletted := !isTrueColor

	doDither, doSerpentine, matrix := m.Settings.Advanced.Dithering()
	if doDither && isPaletted {
		ditherer := dither.NewDitherer(palette.Colors())
		ditherer.Matrix = matrix
		if doSerpentine {
			ditherer.Serpentine = true
		}
		refImg = ditherer.Dither(refImg)
	}

	_, _, useFgBg, chars := m.Settings.Characters.Selected()
	if len(chars) == 0 {
		return "Enter at least one custom character"
	}

	content := ""
	rows := make([]string, height)
	row := make([]string, width)

	for y := 0; y < height*2; y += 2 {
		for x := 0; x < width*2; x += 2 {
			r1, _ := colorful.MakeColor(refImg.At(x, y))
			r2, _ := colorful.MakeColor(refImg.At(x+1, y))
			r3, _ := colorful.MakeColor(refImg.At(x, y+1))
			r4, _ := colorful.MakeColor(refImg.At(x+1, y+1))

			if useFgBg == characters.TwoColor {
				fg, bg, brightness := m.fgBgBrightness(r1, r2, r3, r4)

				lipFg := lipgloss.Color(fg.Hex())
				lipBg := lipgloss.Color(bg.Hex())
				style := lipgloss.NewStyle().Foreground(lipFg).Background(lipBg).Bold(true)

				index := min(int(brightness*float64(len(chars))), len(chars)-1)
				char := chars[index]
				charString := string(char)

				row[x/2] = style.Render(charString)
			} else {
				fg := m.avgColTrue(r1, r2, r3, r4)
				brightness := math.Min(1.0, math.Abs(fg.DistanceLuv(black)))
				if isPaletted {
					fg, _ = colorful.MakeColor(palette.Colors().Convert(fg))
				}
				lipFg := lipgloss.Color(fg.Hex())
				style := lipgloss.NewStyle().Foreground(lipFg).Bold(true)
				index := min(int(brightness*float64(len(chars))), len(chars)-1)
				char := chars[index]
				charString := string(char)
				row[x/2] = style.Render(charString)
			}
		}
		rows[y/2] = lipgloss.JoinHorizontal(lipgloss.Top, row...)
	}
	content += lipgloss.JoinVertical(lipgloss.Left, rows...)
	return content
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

```

--------------------------------------------------------------------------------
/controls/settings/characters/tabs.go:
--------------------------------------------------------------------------------

```go
package characters

import (
	"strings"

	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/style"
)

var (
	inactiveTabBorder = tabBorderWithBottom("┴", "─", "┴")
	activeTabBorder   = tabBorderWithBottom("┘", " ", "└")
	docStyle          = lipgloss.NewStyle().Padding(0)
	inactiveTabStyle  = lipgloss.NewStyle().Border(inactiveTabBorder, true)
	activeTabStyle    = lipgloss.NewStyle().Border(activeTabBorder, true)
	focusTabStyle     = activeTabStyle.Copy().BorderForeground(style.SelectedColor1)
	windowStyle       = lipgloss.NewStyle().Align(lipgloss.Center).Border(lipgloss.NormalBorder()).UnsetBorderTop().Padding(1, 0)
)

func (m Model) drawCharTabs() string {
	doc := strings.Builder{}
	var renderedTabs []string
	tabs := []State{Ascii, Unicode, Custom}

	borderColor := style.DimmedColor2
	if m.IsActive {
		borderColor = style.NormalColor1
	}

	for i, t := range tabs {
		var tabStyle lipgloss.Style

		isFirst := i == 0
		isLast := i == len(tabs)-1
		isActive := m.focus == t
		showControls := m.charControls == t

		fgColor := style.DimmedColor2
		if m.IsActive {
			if isActive {
				fgColor = style.SelectedColor1
			} else {
				fgColor = style.DimmedColor1
			}
		} else {
			if isActive {
				fgColor = style.NormalColor2
			}
		}

		if showControls {
			tabStyle = activeTabStyle.Copy()
		} else {
			tabStyle = inactiveTabStyle.Copy()
		}

		border, _, _, _, _ := tabStyle.GetBorder()
		if isFirst && showControls {
			border.BottomLeft = "│"
		} else if isFirst && !showControls {
			border.BottomLeft = "├"
		} else if isLast && showControls {
			border.BottomRight = "└"
		} else if isLast && !showControls {
			border.BottomRight = "┴"
		}

		tabStyle = tabStyle.Border(border).BorderForeground(borderColor).Foreground(fgColor)
		renderedTabs = append(renderedTabs, tabStyle.Render(stateNames[t]))
	}

	tabBlock := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...)
	extW, extH := max(m.width-lipgloss.Width(tabBlock)-2, 0), 1

	border := lipgloss.Border{BottomLeft: "─", Bottom: "─", BottomRight: "┐"}

	extendedStyle := windowStyle.Copy().Border(border).BorderForeground(borderColor).Padding(0)
	extended := extendedStyle.Copy().Width(extW).Height(extH).Render("")
	renderedTabs = append(renderedTabs, extended)

	row := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...)
	doc.WriteString(row)
	doc.WriteString("\n")

	charButtons := m.drawCharControls()
	doc.WriteString(windowStyle.Copy().BorderForeground(borderColor).Width(lipgloss.Width(row) - windowStyle.GetHorizontalFrameSize()).Render(charButtons))
	return docStyle.Render(doc.String())
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func tabBorderWithBottom(left, middle, right string) lipgloss.Border {
	border := lipgloss.RoundedBorder()
	border.BottomLeft = left
	border.Bottom = middle
	border.BottomRight = right
	return border
}

```

--------------------------------------------------------------------------------
/controls/settings/size/view.go:
--------------------------------------------------------------------------------

```go
package size

import (
	"github.com/charmbracelet/bubbles/cursor"
	"github.com/charmbracelet/lipgloss"
)

var (
	stateOrder = []State{FitButton, StretchButton}
	stateNames = map[State]string{
		FitButton:     "Fit",
		StretchButton: "Stretch",
		WidthForm:     "Width",
		HeightForm:    "Height",
		CharRatioForm: "Char Size Ratio (Width/Height)",
	}

	inputStyle = lipgloss.NewStyle().Width(14).AlignHorizontal(lipgloss.Left)

	activeColor = lipgloss.Color("#aaaaaa")
	focusColor  = lipgloss.Color("#ffffff")
	normalColor = lipgloss.Color("#555555")
	titleStyle  = lipgloss.NewStyle().
			Foreground(lipgloss.Color("#888888"))
)

func (m Model) drawButtons() string {
	buttons := make([]string, len(stateOrder))
	for i, state := range stateOrder {
		styleColor := normalColor
		if m.IsActive {
			if state == m.focus {
				styleColor = focusColor
			} else if state == m.active {
				styleColor = activeColor
			}
		}
		style := lipgloss.NewStyle().
			BorderStyle(lipgloss.RoundedBorder()).
			BorderForeground(styleColor).
			Foreground(styleColor)
		buttons[i] = style.Copy().Width(12).AlignHorizontal(lipgloss.Center).Render(stateNames[state])
	}
	return lipgloss.JoinHorizontal(lipgloss.Left, buttons...)
}

func (m Model) drawSizeForms() string {
	prompt, text := m.getInputColors(WidthForm)
	m.widthInput.Width = 3
	m.widthInput.PromptStyle = m.widthInput.PromptStyle.Copy().Foreground(prompt)
	m.heightInput.TextStyle = m.heightInput.TextStyle.Copy().Foreground(text)
	if m.widthInput.Focused() {
		m.widthInput.Cursor.SetMode(cursor.CursorBlink)
	} else {
		m.widthInput.Cursor.SetMode(cursor.CursorHide)
	}

	prompt, text = m.getInputColors(HeightForm)
	m.heightInput.PromptStyle = m.heightInput.PromptStyle.Copy().Foreground(prompt)
	m.heightInput.TextStyle = m.heightInput.TextStyle.Copy().Foreground(text)
	if m.heightInput.Focused() {
		m.heightInput.Cursor.SetMode(cursor.CursorBlink)
	} else {
		m.heightInput.Cursor.SetMode(cursor.CursorHide)
	}

	width := inputStyle.Render(m.widthInput.View())
	height := inputStyle.Render(m.heightInput.View())

	return lipgloss.JoinHorizontal(lipgloss.Top, width, height)
}

func (m Model) drawCharRatioForm() string {
	prompt, text := m.getInputColors(CharRatioForm)
	m.charRatioInput.Width = 30
	m.charRatioInput.PromptStyle = m.charRatioInput.PromptStyle.Copy().Width(20).Foreground(prompt)
	m.charRatioInput.TextStyle = m.charRatioInput.TextStyle.Copy().Foreground(text)
	if m.charRatioInput.Focused() {
		m.charRatioInput.Cursor.SetMode(cursor.CursorBlink)
	} else {
		m.charRatioInput.Cursor.SetMode(cursor.CursorHide)
	}

	return inputStyle.Copy().Width(28).AlignHorizontal(lipgloss.Left).PaddingTop(1).Render(m.charRatioInput.View())
}

func (m Model) getInputColors(state State) (lipgloss.Color, lipgloss.Color) {
	if m.focus == state {
		if m.active == state {
			return activeColor, focusColor
		} else {
			return focusColor, activeColor
		}
	}
	return normalColor, normalColor
}

```

--------------------------------------------------------------------------------
/controls/settings/palettes/adaptive/view.go:
--------------------------------------------------------------------------------

```go
package adaptive

import (
	"github.com/charmbracelet/bubbles/cursor"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/style"
)

var (
	stateOrder = []State{CountForm, IterForm}
	stateNames = map[State]string{
		CountForm: "Colors",
		IterForm:  "Passes",
	}

	inputStyle = lipgloss.NewStyle().Width(13).AlignHorizontal(lipgloss.Left)

	activeColor = lipgloss.Color("#aaaaaa")
	focusColor  = lipgloss.Color("#ffffff")
	normalColor = lipgloss.Color("#555555")
	titleStyle  = lipgloss.NewStyle().
			Foreground(lipgloss.Color("#888888"))
)

func (m Model) drawTitle() string {
	title := style.DimmedTitle.Copy().Italic(true).Render("Create palette From image")
	return lipgloss.NewStyle().Width(m.width).PaddingBottom(1).AlignHorizontal(lipgloss.Center).Render(title)
}

func (m Model) drawInputs() string {
	prompt, placeholder := m.getInputColors(CountForm)

	m.countInput.PromptStyle = m.countInput.PromptStyle.Copy().Foreground(prompt)
	m.countInput.PlaceholderStyle = m.countInput.PlaceholderStyle.Copy().Foreground(placeholder)
	if m.countInput.Focused() {
		m.countInput.Cursor.SetMode(cursor.CursorBlink)
	} else {
		m.countInput.Cursor.SetMode(cursor.CursorHide)
	}

	prompt, placeholder = m.getInputColors(IterForm)
	m.iterInput.PromptStyle = m.countInput.PromptStyle.Copy().Foreground(prompt)
	m.iterInput.PlaceholderStyle = m.countInput.PlaceholderStyle.Copy().Foreground(placeholder)
	if m.iterInput.Focused() {
		m.iterInput.Cursor.SetMode(cursor.CursorBlink)
	} else {
		m.iterInput.Cursor.SetMode(cursor.CursorHide)
	}

	countInput := inputStyle.Render(m.countInput.View())
	iterInput := inputStyle.Render(m.iterInput.View())

	return lipgloss.JoinHorizontal(lipgloss.Top, countInput, iterInput)
}

func (m Model) drawGenerateButton() string {
	styleColor := normalColor
	if m.IsActive && m.focus == Generate {
		styleColor = focusColor
	} else if m.active == Generate {
		styleColor = activeColor
	}

	style := lipgloss.NewStyle().
		Width(m.width - 4).
		AlignHorizontal(lipgloss.Center).
		BorderStyle(lipgloss.RoundedBorder()).
		BorderForeground(styleColor).
		Foreground(styleColor)

	button := style.Render("Generate New")
	return lipgloss.NewStyle().Width(m.width - 2).AlignHorizontal(lipgloss.Center).Render(button)
}

// TODO: This is almost the same as drawGenerateButton. See if we can generalize
func (m Model) drawSaveButton() string {
	styleColor := normalColor
	if m.IsActive && m.focus == Save {
		styleColor = focusColor
	} else if m.active == Save {
		styleColor = activeColor
	}

	style := lipgloss.NewStyle().
		Width(m.width - 4).
		AlignHorizontal(lipgloss.Center).
		PaddingTop(1).
		Foreground(styleColor)

	button := style.Render("Save to .hex File")
	return lipgloss.NewStyle().Width(m.width - 2).AlignHorizontal(lipgloss.Center).Render(button)
}

func (m Model) getInputColors(state State) (lipgloss.Color, lipgloss.Color) {
	if m.IsActive {
		if m.focus == state {
			return focusColor, focusColor
		} else if m.active == state {
			return activeColor, activeColor
		}
	}
	return normalColor, normalColor
}

```

--------------------------------------------------------------------------------
/controls/settings/advanced/view.go:
--------------------------------------------------------------------------------

```go
package advanced

import (
	"strings"

	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/style"
)

var (
	inactiveTabBorder = tabBorderWithBottom("┴", "─", "┴")
	activeTabBorder   = tabBorderWithBottom("┘", " ", "└")
	docStyle          = lipgloss.NewStyle().Padding(0)
	inactiveTabStyle  = lipgloss.NewStyle().Border(inactiveTabBorder, true)
	activeTabStyle    = lipgloss.NewStyle().Border(activeTabBorder, true)
	focusTabStyle     = activeTabStyle.Copy().BorderForeground(style.SelectedColor1)
	windowStyle       = lipgloss.NewStyle().Align(lipgloss.Left).Border(lipgloss.NormalBorder()).UnsetBorderTop().Padding(1, 0)
	stateNames        = map[State]string{Sampling: "Sampling", Dithering: "Dithering"}
)

func (m Model) drawTabs() string {
	doc := strings.Builder{}
	var renderedTabs []string
	tabs := []State{Sampling, Dithering}

	borderColor := style.DimmedColor2
	if m.IsActive {
		borderColor = style.NormalColor1
	}

	for i, t := range tabs {
		var tabStyle lipgloss.Style
		isFirst, isLast, isActive, isActiveTab := i == 0, i == len(tabs)-1, m.focus == t, m.activeTab == t

		fgColor := style.DimmedColor2
		if m.IsActive {
			if isActive {
				fgColor = style.SelectedColor1
			} else {
				fgColor = style.DimmedColor1
			}
		} else {
			if isActive {
				fgColor = style.NormalColor2
			}
		}

		if m.activeTab == t {
			tabStyle = activeTabStyle.Copy()
		} else {
			tabStyle = inactiveTabStyle.Copy()
		}

		border, _, _, _, _ := tabStyle.GetBorder()
		if isFirst && isActiveTab {
			border.BottomLeft = "│"
		} else if isFirst && !isActiveTab {
			border.BottomLeft = "├"
		} else if isLast && isActiveTab {
			border.BottomRight = "└"
		} else if isLast && !isActiveTab {
			border.BottomRight = "┴"
		}

		tabStyle = tabStyle.Border(border).BorderForeground(borderColor).Foreground(fgColor)
		renderedTabs = append(renderedTabs, tabStyle.Render(stateNames[t]))
	}

	tabBlock := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...)
	extW, extH := max(m.width-lipgloss.Width(tabBlock)-2, 0), 1

	border := lipgloss.Border{BottomLeft: "─", Bottom: "─", BottomRight: "┐"}

	extendedStyle := windowStyle.Copy().Border(border).BorderForeground(borderColor).Padding(0)
	extended := extendedStyle.Copy().Width(extW).Height(extH).Render("")
	renderedTabs = append(renderedTabs, extended)

	row := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...)
	doc.WriteString(row)
	doc.WriteString("\n")

	content := m.drawTabContent()
	doc.WriteString(windowStyle.Copy().BorderForeground(borderColor).Width(lipgloss.Width(row) - windowStyle.GetHorizontalFrameSize()).Render(content))
	return docStyle.Render(doc.String())
}

func (m Model) drawTabContent() string {
	switch m.activeTab {
	case Sampling:
		return m.sampling.View()
	case Dithering:
		return m.dithering.View()
	}
	return ""
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func tabBorderWithBottom(left, middle, right string) lipgloss.Border {
	border := lipgloss.RoundedBorder()
	border.BottomLeft = left
	border.Bottom = middle
	border.BottomRight = right
	return border
}

```

--------------------------------------------------------------------------------
/controls/settings/palettes/adaptive/update.go:
--------------------------------------------------------------------------------

```go
package adaptive

import (
	"fmt"
	"image/color"
	"os"
	"path/filepath"

	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"

	"github.com/Zebbeni/ansizalizer/event"
)

type Direction int

const (
	Left Direction = iota
	Right
	Up
	Down
)

var navMap = map[Direction]map[State]State{
	Right: {CountForm: IterForm},
	Left:  {IterForm: CountForm},
	Up:    {Generate: CountForm, Save: Generate},
	Down:  {CountForm: Generate, IterForm: Generate, Generate: Save},
}

func (m Model) handleEsc() (Model, tea.Cmd) {
	m.ShouldClose = true
	m.IsSelected = false
	return m, nil
}

func (m Model) handleEnter() (Model, tea.Cmd) {
	m.active = m.focus
	m.IsSelected = true
	switch m.active {
	case CountForm:
		m.countInput.Focus()
		return m, nil
	case IterForm:
		m.iterInput.Focus()
		return m, nil
	case Save:
		return m.savePaletteFile()
	}
	return m, event.StartAdaptingCmd
}

func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	switch {
	case key.Matches(msg, event.KeyMap.Right):
		if next, hasNext := navMap[Right][m.focus]; hasNext {
			m.focus = next
		}
	case key.Matches(msg, event.KeyMap.Left):
		if next, hasNext := navMap[Left][m.focus]; hasNext {
			m.focus = next
		}
	case key.Matches(msg, event.KeyMap.Down):
		if next, hasNext := navMap[Down][m.focus]; hasNext {
			m.focus = next
		} else {
			m.IsSelected = false
			m.ShouldUnfocus = true
		}
	case key.Matches(msg, event.KeyMap.Up):
		if next, hasNext := navMap[Up][m.focus]; hasNext {
			m.focus = next
		} else {
			m.IsSelected = false
			m.ShouldUnfocus = true
		}
	}

	return m, cmd
}

func (m Model) handleCountUpdate(msg tea.Msg) (Model, tea.Cmd) {
	if keyMsg, ok := msg.(tea.KeyMsg); ok {
		switch {
		case key.Matches(keyMsg, event.KeyMap.Enter):
			m.IsSelected = true
			m.countInput.Blur()
			return m, event.StartAdaptingCmd
		case key.Matches(keyMsg, event.KeyMap.Esc):
			m.countInput.Blur()
		}
	}
	var cmd tea.Cmd
	m.countInput, cmd = m.countInput.Update(msg)
	return m, cmd
}

func (m Model) handleIterUpdate(msg tea.Msg) (Model, tea.Cmd) {
	if keyMsg, ok := msg.(tea.KeyMsg); ok {
		switch {
		case key.Matches(keyMsg, event.KeyMap.Enter):
			m.IsSelected = true
			m.iterInput.Blur()
			return m, event.StartAdaptingCmd
		case key.Matches(keyMsg, event.KeyMap.Esc):
			m.iterInput.Blur()
		}
	}
	var cmd tea.Cmd
	m.iterInput, cmd = m.iterInput.Update(msg)
	return m, cmd
}

func (m Model) savePaletteFile() (Model, tea.Cmd) {
	filename := fmt.Sprintf("%s.hex", m.palette.Name())

	f, err := os.Create(filename)

	if err != nil {
		return m, event.BuildDisplayCmd("error saving palette file")
	}

	defer f.Close()

	var hexStrings string

	for _, c := range m.palette.Colors() {
		hexStrings += hexColor(c) + "\n"

		if err != nil {
			return m, event.BuildDisplayCmd("error writing to palette file")
		}
	}

	_, err = f.WriteString(hexStrings)

	dir, _ := os.Getwd()
	msg := fmt.Sprintf("saved %s in /%s", filename, filepath.Base(dir))
	return m, event.BuildDisplayCmd(msg)
}

func hexColor(c color.Color) string {
	rgba := color.RGBAModel.Convert(c).(color.RGBA)
	return fmt.Sprintf("%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B)
}

```

--------------------------------------------------------------------------------
/controls/settings/size/update.go:
--------------------------------------------------------------------------------

```go
package size

import (
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"

	"github.com/Zebbeni/ansizalizer/event"
)

type Direction int

const (
	Left Direction = iota
	Right
	Up
	Down
)

var navMap = map[Direction]map[State]State{
	Right: {FitButton: StretchButton, WidthForm: HeightForm},
	Left:  {StretchButton: FitButton, HeightForm: WidthForm},
	Up:    {WidthForm: FitButton, HeightForm: StretchButton, CharRatioForm: HeightForm},
	Down:  {FitButton: WidthForm, StretchButton: HeightForm, WidthForm: CharRatioForm, HeightForm: CharRatioForm},
}

func (m Model) handleEsc() (Model, tea.Cmd) {
	m.ShouldClose = true
	return m, nil
}

func (m Model) handleEnter() (Model, tea.Cmd) {
	if m.active == m.focus {
		if m.active == FitButton || m.active == StretchButton {
			m.ShouldClose = true
			return m, nil
		} else {
			switch m.active {
			case WidthForm:
				m.widthInput.Blur()
				m.active = None
			case HeightForm:
				m.heightInput.Blur()
				m.active = None
			case CharRatioForm:
				m.charRatioInput.Blur()
				m.active = None
			}
			return m, event.StartRenderToViewCmd
		}
	}

	m.active = m.focus
	switch m.active {
	case FitButton:
		m.mode = Fit
	case StretchButton:
		m.mode = Stretch
	case WidthForm:
		m.widthInput.Focus()
	case HeightForm:
		m.heightInput.Focus()
	case CharRatioForm:
		m.charRatioInput.Focus()
	}
	return m, event.StartRenderToViewCmd
}

func (m Model) handleWidthUpdate(msg tea.Msg) (Model, tea.Cmd) {
	if keyMsg, ok := msg.(tea.KeyMsg); ok {
		switch {
		case key.Matches(keyMsg, event.KeyMap.Enter):
			m.widthInput.Blur()
			return m, event.StartRenderToViewCmd
		case key.Matches(keyMsg, event.KeyMap.Esc):
			m.widthInput.Blur()
		}
	}
	var cmd tea.Cmd
	m.widthInput, cmd = m.widthInput.Update(msg)
	return m, cmd
}

func (m Model) handleHeightUpdate(msg tea.Msg) (Model, tea.Cmd) {
	if keyMsg, ok := msg.(tea.KeyMsg); ok {
		switch {
		case key.Matches(keyMsg, event.KeyMap.Enter):
			m.heightInput.Blur()
			return m, event.StartRenderToViewCmd
		case key.Matches(keyMsg, event.KeyMap.Esc):
			m.heightInput.Blur()
		}
	}
	var cmd tea.Cmd
	m.heightInput, cmd = m.heightInput.Update(msg)
	return m, cmd
}

func (m Model) handleCharRatioUpdate(msg tea.Msg) (Model, tea.Cmd) {
	if keyMsg, ok := msg.(tea.KeyMsg); ok {
		switch {
		case key.Matches(keyMsg, event.KeyMap.Enter):
			m.charRatioInput.Blur()
			return m, event.StartRenderToViewCmd
		case key.Matches(keyMsg, event.KeyMap.Esc):
			m.charRatioInput.Blur()
		}
	}
	var cmd tea.Cmd
	m.charRatioInput, cmd = m.charRatioInput.Update(msg)
	return m, cmd
}

func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	switch {
	case key.Matches(msg, event.KeyMap.Right):
		if next, hasNext := navMap[Right][m.focus]; hasNext {
			m.focus = next
		}
	case key.Matches(msg, event.KeyMap.Left):
		if next, hasNext := navMap[Left][m.focus]; hasNext {
			m.focus = next
		}
	case key.Matches(msg, event.KeyMap.Up):
		if next, hasNext := navMap[Up][m.focus]; hasNext {
			m.focus = next
		} else {
			m.ShouldClose = true
		}
	case key.Matches(msg, event.KeyMap.Down):
		if next, hasNext := navMap[Down][m.focus]; hasNext {
			m.focus = next
		} else {
			m.ShouldClose = true
		}
	}

	return m, cmd
}

```

--------------------------------------------------------------------------------
/controls/settings/palettes/lospec/model.go:
--------------------------------------------------------------------------------

```go
package lospec

import (
	"fmt"

	"github.com/charmbracelet/bubbles/key"
	"github.com/charmbracelet/bubbles/list"
	"github.com/charmbracelet/bubbles/textinput"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/event"
	"github.com/Zebbeni/ansizalizer/palette"
	"github.com/Zebbeni/ansizalizer/style"
)

type State int

const (
	CountForm State = iota
	TagForm
	FilterExact
	FilterMax
	FilterMin
	SortAlphabetical
	SortDownloads
	SortNewest
	List
)

type Model struct {
	focus  State
	active State

	countInput textinput.Model
	tagInput   textinput.Model
	filterType State
	sortType   State

	paletteList            list.Model
	palettes               []list.Item
	palette                palette.Model
	isPaletteListAllocated bool
	highestPageRequested   int
	requestID              int

	ShouldClose   bool
	ShouldUnfocus bool
	IsActive      bool
	IsSelected    bool // true if we've selected something (ie. render w/ lospec)

	width             int
	didInitializeList bool
}

func New(w int) Model {
	return Model{
		focus: CountForm,

		countInput: newInput(CountForm, "16"),
		tagInput:   newInput(TagForm, ""),
		filterType: FilterMin,
		sortType:   SortDownloads,

		isPaletteListAllocated: false,
		highestPageRequested:   0,
		requestID:              0,

		ShouldClose:   false,
		ShouldUnfocus: false,
		IsActive:      false,
		IsSelected:    false,

		width: w,
	}
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch m.active {
	case CountForm:
		if m.countInput.Focused() {
			return m.handleCountFormUpdate(msg)
		}
	case TagForm:
		if m.tagInput.Focused() {
			return m.handleTagFormUpdate(msg)
		}
	}

	switch msg := msg.(type) {
	case event.LospecResponseMsg:
		return m.handleLospecResponse(msg)
	case tea.KeyMsg:
		if m.focus == List {
			return m.handleListUpdate(msg)
		}
		switch {
		case key.Matches(msg, event.KeyMap.Enter):
			return m.handleEnter()
		case key.Matches(msg, event.KeyMap.Nav):
			return m.handleNav(msg)
		case key.Matches(msg, event.KeyMap.Esc):
			return m.handleEsc()
		}
	}

	return m, nil
}

// View draws a control panel like this:
//
// Colors ___ |Exact Max Min
// Tag _____________________
// Sort By |A-Z Downloads New
//
// (Palette List)
// <palette name>
// <preview>
// <...>
// <...>
// ..
func (m Model) View() string {
	title := m.drawTitle()
	colorsInput := m.drawColorsInput()
	filters := m.drawFilterButtons()
	colorFilters := lipgloss.JoinHorizontal(lipgloss.Left, colorsInput, filters)
	tagInput := m.drawTagInput()
	sortButtons := m.drawSortButtons()

	results := fmt.Sprintf("%d results found\npage %d of %d", len(m.paletteList.Items()), m.paletteList.Paginator.Page, m.paletteList.Paginator.TotalPages)
	results = style.DimmedTitle.Copy().Width(m.width).Height(2).AlignHorizontal(lipgloss.Center).Padding(1, 0, 1, 0).Render(results)
	paletteList := m.paletteList.View()
	if len(m.paletteList.Items()) == 0 {
		paletteList = ""
	}
	return lipgloss.JoinVertical(lipgloss.Top, title, colorFilters, tagInput, sortButtons, results, paletteList)
}

func (m Model) LoadInitial() (Model, tea.Cmd) {
	return m.searchLospec(0)
}

func (m Model) GetCurrent() palette.Model {
	return m.palette
}

```

--------------------------------------------------------------------------------
/controls/settings/characters/view.go:
--------------------------------------------------------------------------------

```go
package characters

import (
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/style"
)

var (
	stateOrder         = []State{Ascii, Unicode, Custom}
	asciiButtonOrder   = []State{AsciiAz, AsciiNums, AsciiSpec, AsciiAll}
	unicodeButtonOrder = []State{UnicodeFull, UnicodeHalf, UnicodeQuart, UnicodeShadeLight, UnicodeShadeMed, UnicodeShadeHeavy}

	stateNames = map[State]string{
		Ascii:             "Ascii",
		Unicode:           "Unicode",
		Custom:            "Custom",
		AsciiAz:           "AZ",
		AsciiNums:         "0-9",
		AsciiSpec:         "!$",
		AsciiAll:          "All",
		UnicodeFull:       "█",
		UnicodeHalf:       "▀▄",
		UnicodeQuart:      "▞▟",
		UnicodeShadeLight: "░",
		UnicodeShadeMed:   "▒",
		UnicodeShadeHeavy: "▓",
		OneColor:          "1 Color",
		TwoColor:          "2 Colors",
	}

	activeColor = lipgloss.Color("#aaaaaa")
	focusColor  = lipgloss.Color("#ffffff")
	normalColor = lipgloss.Color("#555555")
	titleStyle  = lipgloss.NewStyle().
			Foreground(lipgloss.Color("#888888"))
)

func (m Model) drawCharControls() string {
	if m.charControls == Custom {
		content := m.drawCustomControls()
		return lipgloss.NewStyle().Width(m.width).AlignHorizontal(lipgloss.Left).Render(content)
	}

	whitespace := 0

	var buttonOrder []State
	switch m.charControls {
	case Ascii:
		buttonOrder = asciiButtonOrder
	case Unicode:
		buttonOrder = unicodeButtonOrder
	}

	buttons := make([]string, len(buttonOrder))
	for i, state := range buttonOrder {
		buttonStyle := style.NormalButtonNode
		if m.IsActive && state == m.focus {
			buttonStyle = style.FocusButtonNode
		} else if state == m.asciiMode || state == m.unicodeMode {
			buttonStyle = style.ActiveButtonNode
		}

		buttons[i] = buttonStyle.Copy().Render(stateNames[state])

		whitespace += lipgloss.Width(buttons[i])
	}

	gapSpace := whitespace / (len(buttons))
	for i, button := range buttons {
		buttons[i] = lipgloss.NewStyle().PaddingRight(gapSpace).Render(button)
	}
	content := lipgloss.JoinHorizontal(lipgloss.Left, buttons...)

	return lipgloss.NewStyle().Width(m.width).AlignHorizontal(lipgloss.Left).Render(content)
}

func (m Model) drawCustomControls() string {
	nodeStyle := style.NormalButtonNode.Copy().PaddingRight(1)
	if m.customInput.Focused() {
		nodeStyle = style.ActiveButtonNode.Copy().PaddingRight(1)
	} else if m.focus == SymbolsForm {
		nodeStyle = style.FocusButtonNode.Copy().PaddingRight(1)
	}
	m.customInput.PromptStyle = nodeStyle.Copy()
	return m.customInput.View()
}

func (m Model) drawColorsButtons() string {
	title := style.DimmedTitle.Copy().PaddingLeft(1).Render("Colors per Char:")

	oneStyle := style.NormalButtonNode
	if m.IsActive && OneColor == m.focus {
		oneStyle = style.FocusButtonNode
	} else if m.useFgBg == OneColor {
		oneStyle = style.ActiveButtonNode
	}
	oneButton := oneStyle.Render("1")
	oneButton = lipgloss.NewStyle().Width(5).AlignHorizontal(lipgloss.Center).Render(oneButton)

	twoStyle := style.NormalButtonNode
	if m.IsActive && TwoColor == m.focus {
		twoStyle = style.FocusButtonNode
	} else if m.useFgBg == TwoColor {
		twoStyle = style.ActiveButtonNode
	}
	twoButton := twoStyle.Render("2")
	twoButton = lipgloss.NewStyle().Width(5).AlignHorizontal(lipgloss.Center).Render(twoButton)

	return lipgloss.JoinHorizontal(lipgloss.Left, title, oneButton, twoButton)
}

```

--------------------------------------------------------------------------------
/controls/export/source/view.go:
--------------------------------------------------------------------------------

```go
package source

import (
	"fmt"
	"path/filepath"

	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/style"
)

var (
	stateNames = map[State]string{
		ExpFile:      "Single File",
		ExpDirectory: "Directory",
	}
)

func (m Model) drawExportTypeOptions() string {
	widthStyle := lipgloss.NewStyle().Width((m.width / 2) - 2).AlignHorizontal(lipgloss.Center)
	optionStyle := style.NormalButton
	if ExpFile == m.focus && m.IsActive {
		optionStyle = style.FocusButton
	} else if m.doExportDirectory == false {
		optionStyle = style.ActiveButton
	}
	singleFileButtonText := widthStyle.Render(stateNames[ExpFile])
	singleFileButton := optionStyle.Render(singleFileButtonText)

	optionStyle = style.NormalButton
	if ExpDirectory == m.focus && m.IsActive {
		optionStyle = style.FocusButton
	} else if m.doExportDirectory {
		optionStyle = style.ActiveButton
	}
	directoryButtonText := widthStyle.Render(stateNames[ExpDirectory])
	directoryButton := optionStyle.Render(directoryButtonText)

	return lipgloss.JoinHorizontal(lipgloss.Center, singleFileButton, directoryButton)
}

func (m Model) drawSubDirOptions() string {
	title := style.DimmedTitle.Copy().Render("Include Subdirectories")

	nodeWidthStyle := lipgloss.NewStyle().Width(m.width / 2).AlignHorizontal(lipgloss.Center)

	yesStyle := style.NormalButtonNode.Copy()
	if m.includeSubdirectories {
		yesStyle = style.ActiveButtonNode.Copy()
	}
	if m.focus == SubDirsYes {
		yesStyle = style.FocusButtonNode.Copy()
	}
	yesNode := nodeWidthStyle.Render(yesStyle.Render("Yes"))

	noStyle := style.NormalButtonNode.Copy()
	if !m.includeSubdirectories {
		noStyle = style.ActiveButtonNode.Copy()
	}
	if m.focus == SubDirsNo {
		noStyle = style.FocusButtonNode.Copy()
	}

	noStyle.Padding(0)
	noNode := nodeWidthStyle.Render(noStyle.Render("No"))

	options := lipgloss.JoinHorizontal(lipgloss.Center, yesNode, noNode)

	widthStyle := lipgloss.NewStyle().Width(m.width).AlignHorizontal(lipgloss.Left).PaddingBottom(1)
	content := lipgloss.JoinVertical(lipgloss.Center, title, options)

	return widthStyle.Render(content)
}

func (m Model) drawPrompt() string {
	return style.DimmedTitle.Copy().AlignHorizontal(lipgloss.Center).Padding(0).Render("Select")
}

func (m Model) drawSelected() string {
	title := style.DimmedTitle.Copy().Render("Selected")

	valueStyle := style.DimmedTitle.Copy()
	if Input == m.focus {
		if m.IsActive {
			valueStyle = style.SelectedTitle.Copy()
		} else {
			valueStyle = style.NormalTitle.Copy()
		}
	}
	valueStyle.Padding(0, 0, 1, 0)

	path := m.Browser.SelectedFile
	if m.doExportDirectory {
		path = m.Browser.SelectedDir
	}

	parent := filepath.Base(filepath.Dir(path))
	selected := filepath.Base(path)
	value := fmt.Sprintf("%s/%s", parent, selected)

	valueRunes := []rune(value)
	if len(valueRunes) > m.width {
		value = string(valueRunes[len(valueRunes)-m.width:])
	}

	valueContent := valueStyle.Render(value)

	widthStyle := lipgloss.NewStyle().Width(m.width).AlignHorizontal(lipgloss.Center)
	content := lipgloss.JoinVertical(lipgloss.Center, title, valueContent)

	return widthStyle.Render(content)
}

func (m Model) drawBrowserTitle() string {
	if m.doExportDirectory {
		return style.DimmedTitle.Copy().Padding(0, 2, 1, 2).Render("Select a directory")
	}
	return style.DimmedTitle.Copy().Padding(0, 2, 1, 2).Render("Select a .png or .jpg file")
}

```

--------------------------------------------------------------------------------
/controls/settings/advanced/dithering/list.go:
--------------------------------------------------------------------------------

```go
package dithering

import (
	"github.com/charmbracelet/bubbles/list"
	"github.com/charmbracelet/lipgloss"
	"github.com/makeworld-the-better-one/dither/v2"

	"github.com/Zebbeni/ansizalizer/style"
)

type MatrixType int

const (
	Atkinson MatrixType = iota
	Burkes
	FloydSteinberg
	FalseFloydSteinberg
	JarvisJudiceNinke
	Sierra
	Sierra2
	Sierra3
	SierraLite
	TwoRowSierra
	Sierra2_4A
	Simple2D
	StevenPigeon
	Stucki
)

var Matrices = []MatrixType{
	Atkinson,
	Burkes,
	FloydSteinberg,
	FalseFloydSteinberg,
	JarvisJudiceNinke,
	Sierra,
	Sierra2,
	Sierra3,
	SierraLite,
	TwoRowSierra,
	Sierra2_4A,
	Simple2D,
	Stucki,
	StevenPigeon,
}

var nameMap = map[MatrixType]string{
	Atkinson:            "Atkinson",
	Burkes:              "Burkes",
	FloydSteinberg:      "FloydSteinberg",
	FalseFloydSteinberg: "FalseFloydSteinberg",
	JarvisJudiceNinke:   "JarvisJudiceNinke",
	Sierra:              "Sierra",
	Sierra2:             "Sierra2",
	Sierra3:             "Sierra3",
	SierraLite:          "SierraLite",
	TwoRowSierra:        "TwoRowSierra",
	Sierra2_4A:          "Sierra2_4A",
	Simple2D:            "Simple2D",
	Stucki:              "Stucki",
	StevenPigeon:        "StevenPigeon",
}

var errorDiffMatrixMap = map[MatrixType]dither.ErrorDiffusionMatrix{
	Atkinson:            dither.Atkinson,
	Burkes:              dither.Burkes,
	FloydSteinberg:      dither.FloydSteinberg,
	FalseFloydSteinberg: dither.FalseFloydSteinberg,
	JarvisJudiceNinke:   dither.JarvisJudiceNinke,
	Sierra:              dither.Sierra,
	Sierra2:             dither.Sierra2,
	Sierra3:             dither.Sierra3,
	SierraLite:          dither.SierraLite,
	TwoRowSierra:        dither.TwoRowSierra,
	Sierra2_4A:          dither.Sierra2_4A,
	Simple2D:            dither.Simple2D,
	Stucki:              dither.Stucki,
	StevenPigeon:        dither.StevenPigeon,
}

func newMatrixMenu(width int) list.Model {
	items := menuItems()
	return newMenu(items, width, len(items))
}

type item struct {
	Type MatrixType
}

func (i item) FilterValue() string {
	return nameMap[i.Type]
}

func (i item) Title() string {
	return nameMap[i.Type]
}

func (i item) Description() string {
	return ""
}

func menuItems() []list.Item {
	items := make([]list.Item, len(Matrices))
	for i, matrix := range Matrices {
		items[i] = item{Type: matrix}
	}
	return items
}

func newMenu(items []list.Item, width, height int) list.Model {
	l := list.New(items, NewDelegate(false), width, height/2)
	l.SetShowHelp(false)
	l.SetFilteringEnabled(false)
	l.SetShowTitle(false)
	l.SetShowPagination(true)
	l.SetShowStatusBar(false)

	l.KeyMap.ForceQuit.Unbind()
	l.KeyMap.Quit.Unbind()

	return l
}

func NewDelegate(isActive bool) list.DefaultDelegate {
	delegate := list.NewDefaultDelegate()
	delegate.SetSpacing(0)
	delegate.ShowDescription = false
	if isActive {
		delegate.Styles = ItemStylesActive()
	} else {
		delegate.Styles = ItemStylesInactive()
	}
	return delegate
}

func ItemStylesActive() (s list.DefaultItemStyles) {
	s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2)
	s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1).
		Border(lipgloss.NormalBorder(), false, false, false, true).
		BorderForeground(style.SelectedColor1)
	s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0)
	return s
}

func ItemStylesInactive() (s list.DefaultItemStyles) {
	s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2)
	s.SelectedTitle = style.NormalTitle.Copy().Padding(0, 1, 0, 2)
	s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0)
	return s
}

```

--------------------------------------------------------------------------------
/controls/settings/palettes/lospec/view.go:
--------------------------------------------------------------------------------

```go
package lospec

import (
	"github.com/charmbracelet/bubbles/cursor"
	"github.com/charmbracelet/lipgloss"

	"github.com/Zebbeni/ansizalizer/style"
)

var (
	stateNames = map[State]string{
		CountForm:        "Colors",
		TagForm:          "Tag",
		FilterExact:      "Exact",
		FilterMax:        "Max",
		FilterMin:        "Min",
		SortAlphabetical: "A-Z",
		SortDownloads:    "Downloads",
		SortNewest:       "Newest",
	}

	filterOrder = []State{FilterExact, FilterMax, FilterMin}
	sortOrder   = []State{SortAlphabetical, SortDownloads, SortNewest}

	activeColor = lipgloss.Color("#aaaaaa")
	focusColor  = lipgloss.Color("#ffffff")
	normalColor = lipgloss.Color("#555555")
	titleStyle  = lipgloss.NewStyle().
			Foreground(lipgloss.Color("#888888"))
)

func (m Model) drawInputs() string {
	colorsInput := m.drawColorsInput()
	tagInput := m.drawTagInput()

	return lipgloss.JoinHorizontal(lipgloss.Left, colorsInput, tagInput)
}

func (m Model) drawTitle() string {
	title := style.DimmedTitle.Copy().Italic(true).Render("Browse Lospec.com")
	return lipgloss.NewStyle().Width(m.width).PaddingBottom(1).AlignHorizontal(lipgloss.Center).Render(title)
}

func (m Model) drawColorsInput() string {
	prompt, placeholder := m.getInputColors(CountForm)

	m.countInput.CharLimit = 3
	m.countInput.Width = 3
	m.countInput.PromptStyle = m.countInput.PromptStyle.Copy().Foreground(prompt)
	m.countInput.TextStyle = m.countInput.TextStyle.Copy().Foreground(prompt).MaxWidth(3)
	m.countInput.PlaceholderStyle = m.countInput.PlaceholderStyle.Copy().Foreground(placeholder)
	if m.countInput.Focused() {
		m.countInput.Cursor.SetMode(cursor.CursorBlink)
	} else {
		m.countInput.Cursor.SetMode(cursor.CursorHide)
	}
	return lipgloss.NewStyle().Width(13).Render(m.countInput.View())
}

func (m Model) drawTagInput() string {
	prompt, placeholder := m.getInputColors(TagForm)

	m.tagInput.Width = m.width - 5
	m.tagInput.PromptStyle = m.countInput.PromptStyle.Copy().Foreground(prompt)
	m.tagInput.PlaceholderStyle = m.countInput.PlaceholderStyle.Copy().Foreground(placeholder)
	if m.tagInput.Focused() {
		m.tagInput.Cursor.SetMode(cursor.CursorBlink)
	} else {
		m.tagInput.Cursor.SetMode(cursor.CursorHide)
	}
	return m.tagInput.View()
}

func (m Model) drawFilterButtons() string {
	buttons := make([]string, len(filterOrder))
	for i, filter := range filterOrder {
		buttonStyle := style.NormalButtonNode
		if filter == m.focus {
			buttonStyle = style.FocusButtonNode
		} else if filter == m.filterType {
			buttonStyle = style.ActiveButtonNode
		}
		buttons[i] = buttonStyle.Render(stateNames[filter])
	}

	return lipgloss.JoinHorizontal(lipgloss.Left, buttons...)
}

func (m Model) drawSortButtons() string {
	title := style.DimmedTitle.Copy().PaddingLeft(1).Render("Sort:")
	buttons := make([]string, len(sortOrder))
	for i, sort := range sortOrder {
		buttonStyle := style.NormalButtonNode
		if sort == m.focus {
			buttonStyle = style.FocusButtonNode
		} else if sort == m.sortType {
			buttonStyle = style.ActiveButtonNode
		}
		buttons[i] = buttonStyle.Render(stateNames[sort])
	}
	buttonContent := lipgloss.JoinHorizontal(lipgloss.Left, buttons...)
	return lipgloss.JoinHorizontal(lipgloss.Left, title, buttonContent)
}

func (m Model) drawPaletteList() string {
	if len(m.paletteList.Items()) == 0 {
		return ""
	}

	return m.paletteList.View()
}

func (m Model) getInputColors(state State) (lipgloss.Color, lipgloss.Color) {
	if m.IsActive {
		if m.focus == state {
			return focusColor, focusColor
		} else if m.active == state {
			return activeColor, activeColor
		}
	}
	return normalColor, normalColor
}

```

--------------------------------------------------------------------------------
/controls/settings/palettes/update.go:
--------------------------------------------------------------------------------

```go
package palettes

import (
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"

	"github.com/Zebbeni/ansizalizer/event"
)

type Direction int

const (
	Left Direction = iota
	Right
	Down
	Up
)

var navMap = map[Direction]map[State]State{
	Right: {Load: Adapt, Adapt: Lospec},
	Left:  {Lospec: Adapt, Adapt: Load},
	Down:  {Adapt: AdaptiveControls, Load: LoadControls, Lospec: LospecControls},
	Up:    {AdaptiveControls: Adapt, LoadControls: Load, LospecControls: Lospec},
}

func (m Model) handleMenuUpdate(msg tea.Msg) (Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, event.KeyMap.Esc):
			return m.handleEsc()
		case key.Matches(msg, event.KeyMap.Enter):
			return m.handleEnter()
		case key.Matches(msg, event.KeyMap.Nav):
			return m.handleNav(msg)
		}
	}
	return m, nil
}

func (m Model) handleAdaptiveUpdate(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	m.Adapter, cmd = m.Adapter.Update(msg)
	if m.Adapter.IsSelected {
		m.selected = Adapt
	} else if m.Adapter.ShouldUnfocus {
		m.Adapter.IsActive = true
		m.Adapter.ShouldUnfocus = false
		m.focus = Adapt
	} else if m.Adapter.ShouldClose {
		m.Adapter.IsActive = true
		m.Adapter.ShouldClose = false
		m.ShouldClose = true
	}
	return m, cmd
}

func (m Model) handleLoaderUpdate(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	m.Loader, cmd = m.Loader.Update(msg)
	if m.Loader.IsSelected {
		m.selected = Load
	}
	if m.Loader.ShouldUnfocus {
		m.Loader.ShouldUnfocus = false
		m.focus = Load
	}
	return m, cmd
}

func (m Model) handleLospecUpdate(msg tea.Msg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	m.Lospec, cmd = m.Lospec.Update(msg)
	if m.Lospec.IsSelected {
		m.selected = Lospec
	} else if m.Lospec.ShouldUnfocus {
		m.Lospec.IsActive = true
		m.Lospec.ShouldUnfocus = false
		m.focus = Lospec
	} else if m.Lospec.ShouldClose {
		m.Lospec.IsActive = true
		m.Lospec.ShouldClose = false
		m.ShouldClose = true
	}
	return m, cmd
}

func (m Model) handleEsc() (Model, tea.Cmd) {
	m.ShouldClose = true
	return m, nil
}

func (m Model) handleEnter() (Model, tea.Cmd) {
	m.selected = m.focus
	// Kick off a new palette generation before rendering if not done yet.
	// Allow the app to trigger a render when the generation is complete.
	if m.IsAdaptive() && len(m.Adapter.GetCurrent().Colors()) == 0 {
		return m, event.StartAdaptingCmd
	}
	return m, event.StartRenderToViewCmd
}

func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	switch {
	case key.Matches(msg, event.KeyMap.Right):
		if next, hasNext := navMap[Right][m.focus]; hasNext {
			return m.setFocus(next)
		}
	case key.Matches(msg, event.KeyMap.Left):
		if next, hasNext := navMap[Left][m.focus]; hasNext {
			return m.setFocus(next)
		}
	case key.Matches(msg, event.KeyMap.Down):
		if next, hasNext := navMap[Down][m.focus]; hasNext {
			return m.setFocus(next)
		} else {
			m.IsActive = false
			m.ShouldClose = true
		}
	case key.Matches(msg, event.KeyMap.Up):
		if next, hasNext := navMap[Up][m.focus]; hasNext {
			return m.setFocus(next)
		} else {
			m.IsActive = false
			m.ShouldClose = true
		}
	}

	return m, cmd
}

func (m Model) setFocus(focus State) (Model, tea.Cmd) {
	var cmd tea.Cmd
	m.focus = focus

	switch m.focus {
	case Adapt:
		m.controls = Adapt
	case Load:
		m.controls = Load
	case Lospec:
		m.controls = Lospec
	case AdaptiveControls:
		m.Adapter.IsActive = true
	case LoadControls:
		m.controls = Load
	case LospecControls:
		m.Lospec.IsActive = true
	}

	if m.controls == Lospec && !m.Lospec.DidInitializeList() {
		m.Lospec, cmd = m.Lospec.InitializeList()
	}

	return m, cmd
}

```

--------------------------------------------------------------------------------
/controls/settings/characters/update.go:
--------------------------------------------------------------------------------

```go
package characters

import (
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"

	"github.com/Zebbeni/ansizalizer/event"
)

type Direction int

const (
	Left Direction = iota
	Right
	Up
	Down
)

var navMap = map[Direction]map[State]State{
	Right: {
		Ascii:             Unicode,
		Unicode:           Custom,
		AsciiAz:           AsciiNums,
		AsciiNums:         AsciiSpec,
		AsciiSpec:         AsciiAll,
		UnicodeFull:       UnicodeHalf,
		UnicodeHalf:       UnicodeQuart,
		UnicodeQuart:      UnicodeShadeLight,
		UnicodeShadeLight: UnicodeShadeMed,
		UnicodeShadeMed:   UnicodeShadeHeavy,
		OneColor:          TwoColor,
	},
	Left: {
		Unicode:           Ascii,
		Custom:            Unicode,
		AsciiAll:          AsciiSpec,
		AsciiSpec:         AsciiNums,
		AsciiNums:         AsciiAz,
		UnicodeShadeHeavy: UnicodeShadeMed,
		UnicodeShadeMed:   UnicodeShadeLight,
		UnicodeShadeLight: UnicodeQuart,
		UnicodeQuart:      UnicodeHalf,
		UnicodeHalf:       UnicodeFull,
		TwoColor:          OneColor,
	},
	Up: {
		Ascii:             OneColor,
		Unicode:           OneColor,
		Custom:            OneColor,
		AsciiAz:           Ascii,
		AsciiNums:         Ascii,
		AsciiSpec:         Ascii,
		AsciiAll:          Ascii,
		UnicodeFull:       Unicode,
		UnicodeHalf:       Unicode,
		UnicodeQuart:      Unicode,
		UnicodeShadeLight: Unicode,
		UnicodeShadeMed:   Unicode,
		UnicodeShadeHeavy: Unicode,
		SymbolsForm:       Custom,
	},
	Down: {
		OneColor: Custom,
		TwoColor: Custom,
		Ascii:    AsciiAz,
		Unicode:  UnicodeShadeMed,
		Custom:   SymbolsForm,
	},
}

var (
	asciiCharModeMap   = map[State]bool{AsciiAz: true, AsciiNums: true, AsciiSpec: true, AsciiAll: true}
	unicodeCharModeMap = map[State]bool{UnicodeFull: true, UnicodeHalf: true, UnicodeQuart: true, UnicodeShadeLight: true, UnicodeShadeMed: true, UnicodeShadeHeavy: true}
)

func (m Model) handleSymbolsFormUpdate(msg tea.Msg) (Model, tea.Cmd) {
	if keyMsg, ok := msg.(tea.KeyMsg); ok {
		switch {
		case key.Matches(keyMsg, event.KeyMap.Enter):
			m.customInput.Blur()
			return m, event.StartRenderToViewCmd
		case key.Matches(keyMsg, event.KeyMap.Esc):
			m.customInput.Blur()
		}
	}

	var cmd tea.Cmd
	m.customInput, cmd = m.customInput.Update(msg)
	return m, cmd
}

func (m Model) handleEsc() (Model, tea.Cmd) {
	m.ShouldClose = true
	return m, nil
}

func (m Model) handleEnter() (Model, tea.Cmd) {
	m.active = m.focus

	switch m.active {
	case Ascii:
		m.mode = Ascii
	case Unicode:
		m.mode = Unicode
	case Custom:
		m.mode = Custom
	case SymbolsForm:
		m.mode = Custom
		m.customInput.Focus()
	case OneColor, TwoColor:
		m.useFgBg = m.active
	default:
		switch m.charControls {
		case Ascii:
			if _, ok := asciiCharModeMap[m.active]; ok {
				m.asciiMode = m.active
				m.mode = Ascii
			}
		case Unicode:
			if _, ok := unicodeCharModeMap[m.active]; ok {
				m.unicodeMode = m.active
				m.mode = Unicode
			}
		}
	}
	return m, event.StartRenderToViewCmd
}

func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {

	var cmd tea.Cmd
	switch {
	case key.Matches(msg, event.KeyMap.Right):
		if next, hasNext := navMap[Right][m.focus]; hasNext {
			return m.setFocus(next)
		}
	case key.Matches(msg, event.KeyMap.Left):
		if next, hasNext := navMap[Left][m.focus]; hasNext {
			return m.setFocus(next)
		}
	case key.Matches(msg, event.KeyMap.Up):
		if next, hasNext := navMap[Up][m.focus]; hasNext {
			return m.setFocus(next)
		} else {
			m.IsActive = false
			m.ShouldClose = true
		}
	case key.Matches(msg, event.KeyMap.Down):
		if next, hasNext := navMap[Down][m.focus]; hasNext {
			return m.setFocus(next)
		} else {
			m.IsActive = false
			m.ShouldClose = true
		}
	}
	return m, cmd
}

func (m Model) setFocus(focus State) (Model, tea.Cmd) {
	m.focus = focus
	switch m.focus {
	case Ascii:
		m.charControls = Ascii
	case Unicode:
		m.charControls = Unicode
	case Custom:
		m.charControls = Custom
	}
	return m, nil
}

```

--------------------------------------------------------------------------------
/app/process/unicode.go:
--------------------------------------------------------------------------------

```go
package process

import (
	"image"
	_ "image/gif"
	_ "image/jpeg"
	_ "image/png"
	"math"

	"github.com/charmbracelet/lipgloss"
	"github.com/lucasb-eyer/go-colorful"
	"github.com/makeworld-the-better-one/dither/v2"
	"github.com/nfnt/resize"

	"github.com/Zebbeni/ansizalizer/controls/settings/characters"
	"github.com/Zebbeni/ansizalizer/controls/settings/size"
)

var unicodeShadeChars = []rune{' ', '░', '▒', '▓'}

func (m Renderer) processUnicode(input image.Image) string {
	imgW, imgH := float32(input.Bounds().Dx()), float32(input.Bounds().Dy())

	dimensionType, width, height, charRatio := m.Settings.Size.Info()
	if dimensionType == size.Fit {
		fitHeight := float32(width) * (imgH / imgW) * float32(charRatio)
		fitWidth := (float32(height) * (imgW / imgH)) / float32(charRatio)
		if fitHeight > float32(height) {
			width = int(fitWidth)
		} else {
			height = int(fitHeight)
		}
	}

	resizeFunc := m.Settings.Advanced.SamplingFunction()
	refImg := resize.Resize(uint(width)*2, uint(height)*2, input, resizeFunc)

	isTrueColor, _, palette := m.Settings.Colors.GetSelected()
	isPaletted := !isTrueColor

	doDither, doSerpentine, matrix := m.Settings.Advanced.Dithering()
	if doDither && isPaletted {
		ditherer := dither.NewDitherer(palette.Colors())
		ditherer.Matrix = matrix
		if doSerpentine {
			ditherer.Serpentine = true
		}
		refImg = ditherer.Dither(refImg)
	}

	content := ""
	rows := make([]string, height)
	row := make([]string, width)
	for y := 0; y < height*2; y += 2 {
		for x := 0; x < width*2; x += 2 {
			// r1 r2
			// r3 r4
			r1, _ := colorful.MakeColor(refImg.At(x, y))
			r2, _ := colorful.MakeColor(refImg.At(x+1, y))
			r3, _ := colorful.MakeColor(refImg.At(x, y+1))
			r4, _ := colorful.MakeColor(refImg.At(x+1, y+1))

			// pick the block, fg and bg color with the lowest total difference
			// convert the colors to ansi, render the block and add it at row[x]
			r, fg, bg := m.getBlock(r1, r2, r3, r4)

			pFg, _ := colorful.MakeColor(fg)
			pBg, _ := colorful.MakeColor(bg)

			lipFg := lipgloss.Color(pFg.Hex())
			lipBg := lipgloss.Color(pBg.Hex())

			style := lipgloss.NewStyle().Foreground(lipFg)
			if _, _, mode, _ := m.Settings.Characters.Selected(); mode == characters.TwoColor {
				style = style.Copy().Background(lipBg)
			}

			row[x/2] = style.Render(string(r))
		}
		rows[y/2] = lipgloss.JoinHorizontal(lipgloss.Top, row...)
	}
	content += lipgloss.JoinVertical(lipgloss.Left, rows...)
	return content
}

// find the best block character and foreground and background colors to match
// a set of 4 pixels. return
func (m Renderer) getBlock(r1, r2, r3, r4 colorful.Color) (r rune, fg, bg colorful.Color) {
	var blockFuncs map[rune]blockFunc
	switch _, charSet, _, _ := m.Settings.Characters.Selected(); charSet {
	case characters.UnicodeFull:
		blockFuncs = m.fullBlockFuncs
	case characters.UnicodeHalf:
		blockFuncs = m.halfBlockFuncs
	case characters.UnicodeQuart:
		blockFuncs = m.quarterBlockFuncs
	case characters.UnicodeShadeLight:
		blockFuncs = m.shadeLightBlockFuncs
	case characters.UnicodeShadeMed:
		blockFuncs = m.shadeMedBlockFuncs
	case characters.UnicodeShadeHeavy:
		blockFuncs = m.shadeHeavyBlockFuncs
	}

	minDist := 100.0
	for bRune, bFunc := range blockFuncs {
		f, b, dist := bFunc(r1, r2, r3, r4)
		if dist < minDist {
			minDist = dist
			r, fg, bg = bRune, f, b
		}
	}
	return
}

func (m Renderer) avgCol(colors ...colorful.Color) (colorful.Color, float64) {
	rSum, gSum, bSum := 0.0, 0.0, 0.0
	for _, col := range colors {
		rSum += col.R
		gSum += col.G
		bSum += col.B
	}
	count := float64(len(colors))
	avg := colorful.Color{R: rSum / count, G: gSum / count, B: bSum / count}

	if m.Settings.Colors.IsLimited() {
		_, _, palette := m.Settings.Colors.GetSelected()

		paletteAvg := palette.Colors().Convert(avg)
		avg, _ = colorful.MakeColor(paletteAvg)
	}

	// compute sum of squares
	totalDist := 0.0
	for _, col := range colors {
		totalDist += math.Pow(col.DistanceCIEDE2000(avg), 2)
	}
	return avg, totalDist
}

```

--------------------------------------------------------------------------------
/app/process/ascii.go:
--------------------------------------------------------------------------------

```go
package process

import (
	"image"
	"math"

	"github.com/charmbracelet/lipgloss"
	"github.com/lucasb-eyer/go-colorful"
	"github.com/makeworld-the-better-one/dither/v2"
	"github.com/nfnt/resize"

	"github.com/Zebbeni/ansizalizer/controls/settings/characters"
	"github.com/Zebbeni/ansizalizer/controls/settings/size"
)

// A list of Ascii characters by ascending brightness
var asciiChars = []rune(" `.-':_,^=;><+!rc*/z?sLTv)J7(|Fi{C}fI31tlu[neoZ5Yxjya]2ESwqkP6h9d4VpOGbUAKXHm8RD#$Bg0MNWQ%&@")
var asciiAZChars = []rune(" rczsLTvJFiCfItluneoZYxjyaESwqkPhdVpOGbUAKXHmRDBgMNWQ")
var asciiNumChars = []rune(" 7315269480")
var asciiSpecChars = []rune(" `.-':_,^=;><+!*/?)(|{}[]#$%&@")

func (m Renderer) processAscii(input image.Image) string {
	imgW, imgH := float32(input.Bounds().Dx()), float32(input.Bounds().Dy())

	dimensionType, width, height, charRatio := m.Settings.Size.Info()
	if dimensionType == size.Fit {
		fitHeight := float32(width) * (imgH / imgW) * float32(charRatio)
		fitWidth := (float32(height) * (imgW / imgH)) / float32(charRatio)
		if fitHeight > float32(height) {
			width = int(fitWidth)
		} else {
			height = int(fitHeight)
		}
	}

	resizeFunc := m.Settings.Advanced.SamplingFunction()
	refImg := resize.Resize(uint(width)*2, uint(height)*2, input, resizeFunc)

	isTrueColor, _, palette := m.Settings.Colors.GetSelected()
	isPaletted := !isTrueColor

	doDither, doSerpentine, matrix := m.Settings.Advanced.Dithering()
	if doDither && isPaletted {
		ditherer := dither.NewDitherer(palette.Colors())
		ditherer.Matrix = matrix
		if doSerpentine {
			ditherer.Serpentine = true
		}
		refImg = ditherer.Dither(refImg)
	}

	var chars []rune
	_, charMode, useFgBg, _ := m.Settings.Characters.Selected()
	switch charMode {
	case characters.AsciiAz:
		chars = asciiAZChars
	case characters.AsciiNums:
		chars = asciiNumChars
	case characters.AsciiSpec:
		chars = asciiSpecChars
	case characters.AsciiAll:
		chars = asciiChars
	}

	content := ""
	rows := make([]string, height)
	row := make([]string, width)

	for y := 0; y < height*2; y += 2 {
		for x := 0; x < width*2; x += 2 {
			r1, isTrans1 := colorful.MakeColor(refImg.At(x, y))
			r2, isTrans2 := colorful.MakeColor(refImg.At(x+1, y))
			r3, isTrans3 := colorful.MakeColor(refImg.At(x, y+1))
			r4, isTrans4 := colorful.MakeColor(refImg.At(x+1, y+1))

			if isTrans1 || isTrans2 || isTrans3 || isTrans4 {
				isTrans2 = !isTrans2 == false
			}

			if useFgBg == characters.TwoColor {
				fg, bg, brightness := m.fgBgBrightness(r1, r2, r3, r4)

				lipFg := lipgloss.Color(fg.Hex())
				lipBg := lipgloss.Color(bg.Hex())
				style := lipgloss.NewStyle().Foreground(lipFg).Background(lipBg).Bold(true)

				index := min(int(brightness*float64(len(chars))), len(chars)-1)
				char := chars[index]
				charString := string(char)

				row[x/2] = style.Render(charString)
			} else {
				fg := m.avgColTrue(r1, r2, r3, r4)
				brightness := math.Min(1.0, math.Abs(fg.DistanceLuv(black)))
				if !isTrueColor {
					fg, _ = colorful.MakeColor(palette.Colors().Convert(fg))
				}
				lipFg := lipgloss.Color(fg.Hex())
				style := lipgloss.NewStyle().Foreground(lipFg).Bold(true)

				index := min(int(brightness*float64(len(chars))), len(chars)-1)
				char := chars[index]
				charString := string(char)
				row[x/2] = style.Render(charString)
			}
		}
		rows[y/2] = lipgloss.JoinHorizontal(lipgloss.Top, row...)
	}
	content += lipgloss.JoinVertical(lipgloss.Left, rows...)
	return content
}

func (m Renderer) fgBgBrightness(c ...colorful.Color) (fg, bg colorful.Color, b float64) {
	// find the darkest and lightest among given colors
	light, dark := lightDark(c...)

	avg := m.avgColTrue(c...)
	avgCol, _ := colorful.MakeColor(avg)

	//distLight := avgCol.DistanceLuv(light)
	distDark := avgCol.DistanceLuv(dark)
	distTotal := light.DistanceLuv(dark)
	var brightness float64
	if distTotal == 0 {
		brightness = 0
	} else {
		brightness = math.Min(1.0, math.Abs(distDark/distTotal))
	}

	// if paletted:
	//   convert the darkest to its closest paletted color
	//   convert the lightest to its closest paletted color (excluding the previously found color)
	if m.Settings.Colors.IsLimited() {
		light, dark = m.getLightDarkPaletted(light, dark)
	}

	return light, dark, brightness
}

func (m Renderer) avgColTrue(colors ...colorful.Color) colorful.Color {
	rSum, gSum, bSum := 0.0, 0.0, 0.0
	for _, col := range colors {
		rSum += col.R
		gSum += col.G
		bSum += col.B
	}
	count := float64(len(colors))
	avg := colorful.Color{R: rSum / count, G: gSum / count, B: bSum / count}

	return avg
}

func lightDark(c ...colorful.Color) (light, dark colorful.Color) {
	mostLight, mostDark := 0.0, 1.0
	for _, col := range c {
		_, _, l := col.Hsl()
		if l < mostDark {
			mostDark = l
			dark = col
		}
		if l > mostLight {
			mostLight = l
			light = col
		}
	}
	return
}

```

--------------------------------------------------------------------------------
/controls/settings/palettes/loader/values.go:
--------------------------------------------------------------------------------

```go
package loader

import (
	"fmt"
	"image/color"

	"github.com/lucasb-eyer/go-colorful"
	"github.com/muesli/termenv"
)

func BlackAndWhite() color.Palette {
	return color.Palette{
		color.RGBA{R: 0, G: 0, B: 0, A: 255},
		color.RGBA{R: 255, G: 255, B: 255, A: 255},
	}
}

func AnsiVga16() color.Palette {
	return color.Palette{
		color.RGBA{R: 0, G: 0, B: 0, A: 255},
		color.RGBA{R: 170, G: 0, B: 0, A: 255},
		color.RGBA{R: 0, G: 170, B: 0, A: 255},
		color.RGBA{R: 170, G: 85, B: 0, A: 255},
		color.RGBA{R: 0, G: 0, B: 170, A: 255},
		color.RGBA{R: 170, G: 0, B: 170, A: 255},
		color.RGBA{R: 0, G: 170, B: 170, A: 255},
		color.RGBA{R: 170, G: 170, B: 170, A: 255},
		color.RGBA{R: 85, G: 85, B: 85, A: 255},
		color.RGBA{R: 255, G: 85, B: 85, A: 255},
		color.RGBA{R: 85, G: 255, B: 85, A: 255},
		color.RGBA{R: 255, G: 255, B: 85, A: 255},
		color.RGBA{R: 85, G: 85, B: 255, A: 255},
		color.RGBA{R: 255, G: 85, B: 255, A: 255},
		color.RGBA{R: 85, G: 255, B: 255, A: 255},
		color.RGBA{R: 255, G: 255, B: 255, A: 255},
	}
}

func AnsiWinConsole16() color.Palette {
	return color.Palette{
		color.RGBA{R: 0, G: 0, B: 0, A: 255},
		color.RGBA{R: 128, G: 0, B: 0, A: 255},
		color.RGBA{R: 0, G: 128, B: 0, A: 255},
		color.RGBA{R: 128, G: 128, B: 0, A: 255},
		color.RGBA{R: 0, G: 0, B: 128, A: 255},
		color.RGBA{R: 128, G: 0, B: 128, A: 255},
		color.RGBA{R: 0, G: 128, B: 128, A: 255},
		color.RGBA{R: 192, G: 192, B: 192, A: 255},
		color.RGBA{R: 128, G: 128, B: 128, A: 255},
		color.RGBA{R: 255, G: 0, B: 0, A: 255},
		color.RGBA{R: 0, G: 255, B: 0, A: 255},
		color.RGBA{R: 255, G: 255, B: 0, A: 255},
		color.RGBA{R: 0, G: 0, B: 255, A: 255},
		color.RGBA{R: 255, G: 0, B: 255, A: 255},
		color.RGBA{R: 0, G: 255, B: 255, A: 255},
		color.RGBA{R: 255, G: 255, B: 255, A: 255},
	}
}

func AnsiWinPowershell16() color.Palette {
	return color.Palette{
		color.RGBA{R: 12, G: 12, B: 12, A: 255},
		color.RGBA{R: 197, G: 15, B: 31, A: 255},
		color.RGBA{R: 19, G: 161, B: 14, A: 255},
		color.RGBA{R: 193, G: 156, B: 0, A: 255},
		color.RGBA{R: 0, G: 55, B: 218, A: 255},
		color.RGBA{R: 136, G: 23, B: 152, A: 255},
		color.RGBA{R: 58, G: 150, B: 221, A: 255},
		color.RGBA{R: 204, G: 204, B: 204, A: 255},
		color.RGBA{R: 118, G: 118, B: 118, A: 255},
		color.RGBA{R: 231, G: 72, B: 86, A: 255},
		color.RGBA{R: 22, G: 198, B: 12, A: 255},
		color.RGBA{R: 249, G: 241, B: 165, A: 255},
		color.RGBA{R: 59, G: 120, B: 255, A: 255},
		color.RGBA{R: 180, G: 0, B: 158, A: 255},
		color.RGBA{R: 97, G: 214, B: 214, A: 255},
		color.RGBA{R: 242, G: 242, B: 242, A: 255},
	}
}

func Ansi16() color.Palette {
	p := make(color.Palette, 0, 16)
	for i := 0; i < 16; i++ {
		ansi := termenv.ANSI.Color(fmt.Sprintf("%d", i))
		col := termenv.ConvertToRGB(ansi)
		p = append(p, col)
	}
	return p
}

func Ansi256() color.Palette {
	p := make(color.Palette, 0, 256)
	for i := 0; i < 256; i++ {
		ansi := termenv.ANSI256.Color(fmt.Sprintf("%d", i))
		col := termenv.ConvertToRGB(ansi)
		p = append(p, col)
	}
	return p
}

func KlarikFilmic() color.Palette {
	hexes := []string{
		"#ffffff",
		"#d6dfdf",
		"#b5c4c1",
		"#8fa6a0",
		"#6f837e",
		"#536a66",
		"#2b3b3e",
		"#162424",
		"#000000",
		"#250a1d",
		"#3f1526",
		"#5a2535",
		"#82363f",
		"#a64e54",
		"#b66868",
		"#c08780",
		"#ceaea4",
		"#b2897c",
		"#9a6a5d",
		"#7c4d3f",
		"#5b2e2b",
		"#3d181b",
		"#280b15",
		"#895938",
		"#b1834e",
		"#bb995f",
		"#caac7a",
		"#d3c59f",
		"#a8ad80",
		"#84935a",
		"#5a7645",
		"#305630",
		"#1a3725",
		"#0e2724",
		"#152f3c",
		"#2d4e59",
		"#4b7674",
		"#628e87",
		"#7ca294",
		"#a5bbae",
		"#bacbc9",
		"#a1b7bf",
		"#778faa",
		"#5e6d92",
		"#424372",
		"#352959",
		"#2c173d",
		"#492854",
		"#6e3f72",
		"#935c8d",
		"#ae7d9e",
		"#c6a7b5",
		"#ac7b90",
		"#8f516c",
		"#73415a",
		"#542846",
		"#3f1831",
	}
	return hexesToColorPalette(hexes)
}

func Mudstone() color.Palette {
	hexes := []string{
		"#1b1611",
		"#1f253c",
		"#423c32",
		"#465d32",
		"#6e3f24",
		"#6b624e",
		"#90752e",
		"#cda465",
	}
	return hexesToColorPalette(hexes)
}

func IsleOfTheDead() color.Palette {
	hexes := []string{
		"#0b0b0b",
		"#454848",
		"#4f514f",
		"#5a5a5a",
		"#666666",
		"#3e3f3f",
		"#373838",
		"#242421",
		"#2c2d25",
		"#36382a",
		"#1b1b17",
		"#313333",
		"#858585",
		"#a0a0a0",
		"#717171",
		"#2c2d2d",
		"#121210",
		"#3f4132",
		"#aeaeae",
		"#575a4a",
		"#737359",
		"#858562",
		"#93906c",
		"#686652",
		"#a9a681",
		"#48534d",
		"#252928",
		"#857d62",
		"#aea282",
		"#d0cec1",
		"#c0b9a5",
		"#58503b",
		"#7a6b54",
		"#413a28",
		"#53493a",
		"#685a44",
		"#443b2e",
		"#1a201e",
		"#362e23",
		"#7a704d",
		"#222b31",
		"#364550",
	}
	return hexesToColorPalette(hexes)
}

func hexesToColorPalette(hexes []string) color.Palette {
	var colorPalette color.Palette
	for _, h := range hexes {
		c, _ := colorful.Hex(h)
		colorPalette = append(colorPalette, c)
	}
	return colorPalette
}

```

--------------------------------------------------------------------------------
/controls/settings/palettes/lospec/update.go:
--------------------------------------------------------------------------------

```go
package lospec

import (
	"fmt"
	"image/color"
	"strconv"

	"github.com/charmbracelet/bubbles/key"
	"github.com/charmbracelet/bubbles/list"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/lucasb-eyer/go-colorful"

	"github.com/Zebbeni/ansizalizer/event"
	"github.com/Zebbeni/ansizalizer/palette"
	"github.com/Zebbeni/ansizalizer/style"
)

// TODO: Direction is redefined in multiple places

type Direction int

type Param int

const (
	Left Direction = iota
	Right
	Up
	Down
)

var (
	navMap = map[Direction]map[State]State{
		Right: {CountForm: FilterExact, FilterExact: FilterMax, FilterMax: FilterMin, SortAlphabetical: SortDownloads, SortDownloads: SortNewest},
		Left:  {TagForm: CountForm, FilterMin: FilterMax, FilterMax: FilterExact, FilterExact: CountForm, SortNewest: SortDownloads, SortDownloads: SortAlphabetical},
		Up:    {TagForm: CountForm, SortAlphabetical: TagForm, SortDownloads: TagForm, SortNewest: TagForm, List: SortAlphabetical},
		Down:  {CountForm: TagForm, FilterExact: TagForm, FilterMax: TagForm, FilterMin: TagForm, TagForm: SortAlphabetical, SortAlphabetical: List, SortDownloads: List, SortNewest: List},
	}
	filterParams = map[State]string{
		FilterExact: "exact",
		FilterMax:   "max",
		FilterMin:   "min",
	}
	sortParams = map[State]string{
		SortAlphabetical: "alphabetical",
		SortDownloads:    "downloads",
		SortNewest:       "newest",
	}
)

func (m Model) handleEsc() (Model, tea.Cmd) {
	m.ShouldClose = true
	m.IsSelected = false
	m.ShouldUnfocus = true
	return m, nil
}

func (m Model) handleEnter() (Model, tea.Cmd) {
	m.active = m.focus
	switch m.focus {
	case CountForm:
		m.countInput.Focus()
		return m, nil
	case TagForm:
		m.tagInput.Focus()
		return m, nil
	case FilterExact, FilterMax, FilterMin:
		m.filterType = m.focus
		return m.searchLospec(0)
	case SortAlphabetical, SortDownloads, SortNewest:
		m.sortType = m.focus
		return m.searchLospec(0)
	case List:
		m.palette, _ = m.paletteList.SelectedItem().(palette.Model)
		m.IsSelected = true
		return m, event.StartRenderToViewCmd
	}
	return m, nil
}

func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
	switch {
	case key.Matches(msg, event.KeyMap.Right):
		if next, hasNext := navMap[Right][m.focus]; hasNext {
			m.focus = next
		}
	case key.Matches(msg, event.KeyMap.Left):
		if next, hasNext := navMap[Left][m.focus]; hasNext {
			m.focus = next
		}
	case key.Matches(msg, event.KeyMap.Down):
		if next, hasNext := navMap[Down][m.focus]; hasNext {
			m.focus = next
		}
	case key.Matches(msg, event.KeyMap.Up):
		if next, hasNext := navMap[Up][m.focus]; hasNext {
			m.focus = next
		} else {
			m.IsSelected = false
			m.ShouldUnfocus = true
		}
	}
	return m, nil
}

func (m Model) handleLospecResponse(msg event.LospecResponseMsg) (Model, tea.Cmd) {
	var cmd tea.Cmd
	// return early if response no longer matches current requestID
	if msg.ID != m.requestID {
		return m, cmd
	}

	// if we haven't initialized and allocated an array of palettes for the current request series, do that first
	if !m.isPaletteListAllocated {
		m.palettes = make([]list.Item, msg.Data.TotalCount)
		m.paletteList = CreateList(m.palettes, m.width-2)
		m.paletteList.Styles.Title = style.DimmedTitle
		m.paletteList.Styles.TitleBar = m.paletteList.Styles.TitleBar.Padding(0).Width(m.width).AlignHorizontal(lipgloss.Center)
		m.isPaletteListAllocated = true
	}

	// use the page number*10 (assumes 10 palettes per page) to populate palettes
	for i, p := range msg.Data.Palettes {
		colors := make([]color.Color, len(p.Colors))
		var err error

		for colorIndex, c := range p.Colors {
			colors[colorIndex], err = colorful.Hex(fmt.Sprintf("#%s", c))
			if err != nil {
				return m, event.BuildDisplayCmd("error converting hex value")
			}
		}

		idx := (msg.Page * 10) + i
		m.palettes[idx] = palette.New(p.Title, colors, m.width-4, 2)
	}

	m.paletteList.SetItems(m.palettes)

	return m, cmd
}

func (m Model) handleCountFormUpdate(msg tea.Msg) (Model, tea.Cmd) {
	if keyMsg, ok := msg.(tea.KeyMsg); ok {
		switch {
		case key.Matches(keyMsg, event.KeyMap.Enter):
			m.countInput.Blur()
			return m.searchLospec(0)
		case key.Matches(keyMsg, event.KeyMap.Esc):
			m.countInput.Blur()
		}
	}
	var cmd tea.Cmd
	m.countInput, cmd = m.countInput.Update(msg)
	return m, cmd
}

func (m Model) handleTagFormUpdate(msg tea.Msg) (Model, tea.Cmd) {
	if keyMsg, ok := msg.(tea.KeyMsg); ok {
		switch {
		case key.Matches(keyMsg, event.KeyMap.Enter):
			m.tagInput.Blur()
			return m.searchLospec(0)
		case key.Matches(keyMsg, event.KeyMap.Esc):
			m.tagInput.Blur()
		}
	}
	var cmd tea.Cmd
	m.tagInput, cmd = m.tagInput.Update(msg)
	return m, cmd
}

func (m Model) handleListUpdate(msg tea.Msg) (Model, tea.Cmd) {
	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}

	switch {
	case key.Matches(keyMsg, event.KeyMap.Enter):
		return m.handleEnter()
	case key.Matches(keyMsg, event.KeyMap.Up) && m.paletteList.Index() == 0:
		return m.handleNav(keyMsg)
	case key.Matches(keyMsg, event.KeyMap.Esc):
		m.focus = TagForm
	}

	var cmd tea.Cmd
	if len(m.paletteList.Items()) > 0 {
		m.paletteList, cmd = m.paletteList.Update(msg)
	}

	if m.paletteList.Index() < (m.highestPageRequested-1)*10 {
		return m, cmd
	}

	m.highestPageRequested += 1
	return m.searchLospec(m.highestPageRequested)
}

func (m Model) searchLospec(page int) (Model, tea.Cmd) {
	if page == 0 {
		m.requestID += 1
		m.highestPageRequested = 0
		m.isPaletteListAllocated = false
	}

	colors, _ := strconv.Atoi(m.countInput.Value())
	tag := m.tagInput.Value()
	filterType := filterParams[m.filterType]
	sortingType := sortParams[m.sortType]

	urlString := "https://lospec.com/palette-list/load?colorNumber=%d&tag=%s&colorNumberFilterType=%s&sortingType=%s&page=%d"
	url := fmt.Sprintf(urlString, colors, tag, filterType, sortingType, page)
	return m, event.BuildLospecRequestCmd(event.LospecRequestMsg{
		URL:  url,
		ID:   m.requestID,
		Page: page,
	})
}

```
Page 1/2FirstPrevNextLast