#
tokens: 49553/50000 74/88 files (page 1/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 2. Use http://codebase.md/Zebbeni/ansizalizer?lines=true&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:
--------------------------------------------------------------------------------

```
 1 | # Images test directory
 2 | images/*
 3 | 
 4 | # Binaries for programs and plugins
 5 | *.exe
 6 | *.exe~
 7 | *.dll
 8 | *.so
 9 | *.dylib
10 | *.idea/
11 | *.hex
12 | *.ansi
13 | 
14 | # Test binary, built with `go test -c`
15 | *.test
16 | 
17 | # Output of the go coverage tool, specifically when used with LiteIDE
18 | *.out
19 | 
20 | # Dependency directories (remove the comment below to include it)
21 | # vendor/
22 | 
```

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

```markdown
 1 | # ANSIZALIZER
 2 | A TUI to convert Images to ANSI strings using bubbletea
 3 | 
 4 | ![Screenshot 2024-04-02 150412](https://github.com/Zebbeni/ansizalizer/assets/3377325/141c3662-7e70-4e82-ac0c-5db77adbf1c7)
 5 | 
 6 | ## Features
 7 | - A keyboard-navigable Text-based UI
 8 | - File browser: Search .png and .jpeg image files and preview in real-time
 9 | - Export ANSI image strings to '.ansi' text files or copy directly to your Clipboard
10 | - Save files individually or Batch Process All Images in a chosen directory
11 | - Browse Lospec.com for cool color palettes
12 | 
13 | ## Render Options
14 | - Set output Width and Height of rendered text images (in characters)
15 | - Choose character sets to use in output (ASCII, Unicode, or Custom)
16 | - Render images with "true" colors or convert using Limited Color Palettes
17 | - Generate new color palettes by sampling previewed image files
18 | - Use Advanced settings to tweak pixel Sampling mode and Dithering options
19 | 
20 | ![Screenshot 2024-04-02 155820](https://github.com/Zebbeni/ansizalizer/assets/3377325/24095f45-5c73-4654-a5e1-b491cda9dc66)
21 | 
22 | ## To Run
23 | 
24 | **On Windows:**
25 | ```bash
26 | go install
27 | go build
28 | start ansizalizer.exe
29 | ```
30 | 
31 | **On Mac/Linux:**
32 | ```bash
33 | go install
34 | go build
35 | ./ansizalizer
36 | ```
37 | 
38 | ![Screenshot 2024-04-02 155006](https://github.com/Zebbeni/ansizalizer/assets/3377325/d41df628-6c84-44e0-aa34-f7fcb72ed827)
39 | 
40 | ## FAQ / Troubleshooting
41 | **Q: The UI isn't rendering correctly**
42 | 
43 | 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.
44 | 
45 | **Q: My images look squashed / stretched**
46 | 
47 | 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.
48 | 
49 | **Q: My exported .ansi files take up more space than the original image**
50 | 
51 | 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.
52 | 
```

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

```markdown
 1 | MIT License
 2 | 
 3 | Copyright (c) 2024 Andrew Albers
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 
```

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

```go
1 | //go:build darwin
2 | 
3 | package env
4 | 
5 | const PollForSizeChange = false
```

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

```go
1 | //go:build linux
2 | 
3 | package env
4 | 
5 | const PollForSizeChange = false
6 | 
```

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

```go
1 | //go:build windows
2 | 
3 | package env
4 | 
5 | const PollForSizeChange = true
6 | 
```

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

```go
1 | package global
2 | 
3 | var (
4 | 	ImgExtensions = map[string]bool{".png": true, ".jpg": true, ".jpeg": true}
5 | )
6 | 
```

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

```go
 1 | package loader
 2 | 
 3 | import (
 4 | 	"github.com/Zebbeni/ansizalizer/palette"
 5 | )
 6 | 
 7 | type item struct {
 8 | 	palette palette.Model
 9 | }
10 | 
11 | func (i item) FilterValue() string {
12 | 	return i.palette.Name()
13 | }
14 | 
15 | func (i item) Title() string {
16 | 	return i.palette.Name()
17 | }
18 | 
19 | func (i item) Description() string {
20 | 	return i.palette.View()
21 | }
22 | 
```

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

```go
 1 | package main
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"os"
 6 | 
 7 | 	tea "github.com/charmbracelet/bubbletea"
 8 | 
 9 | 	"github.com/Zebbeni/ansizalizer/app"
10 | 	"github.com/Zebbeni/ansizalizer/event"
11 | )
12 | 
13 | func init() {
14 | 	event.InitKeyMap()
15 | }
16 | 
17 | func main() {
18 | 	m := app.New()
19 | 	p := tea.NewProgram(m)
20 | 	if _, err := p.Run(); err != nil {
21 | 		fmt.Println("Error running program:", err)
22 | 		os.Exit(1)
23 | 	}
24 | }
25 | 
```

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

```go
 1 | package settings
 2 | 
 3 | type State int
 4 | 
 5 | const (
 6 | 	None State = iota
 7 | 	Colors
 8 | 	Characters
 9 | 	Size
10 | 	Advanced
11 | )
12 | 
13 | var States = []State{
14 | 	Colors,
15 | 	Characters,
16 | 	Size,
17 | 	Advanced,
18 | }
19 | 
20 | var stateOrder = []State{Colors, Characters, Size, Advanced}
21 | 
22 | var stateTitles = map[State]string{
23 | 	Colors:     "Colors",
24 | 	Characters: "Characters",
25 | 	Size:       "Size",
26 | 	Advanced:   "Advanced",
27 | }
28 | 
```

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

```go
 1 | package viewer
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"path/filepath"
 6 | 
 7 | 	tea "github.com/charmbracelet/bubbletea"
 8 | 
 9 | 	"github.com/Zebbeni/ansizalizer/event"
10 | )
11 | 
12 | func (m Model) handleFinishRenderMsg(msg event.FinishRenderToViewMsg) (Model, tea.Cmd) {
13 | 	m.WaitingOnRender = false
14 | 	m.imgString = msg.ImgString
15 | 
16 | 	displayMsg := fmt.Sprintf("viewing %s/%s with %s palette", filepath.Base(filepath.Dir(msg.FilePath)), filepath.Base(msg.FilePath), msg.ColorsString)
17 | 	return m, event.BuildDisplayCmd(displayMsg)
18 | }
19 | 
```

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

```go
 1 | package settings
 2 | 
 3 | //type item struct {
 4 | //	name  string
 5 | //	state State
 6 | //}
 7 | //
 8 | //func (i item) FilterValue() string {
 9 | //	return i.name
10 | //}
11 | //
12 | //func (i item) Title() string {
13 | //	return i.name
14 | //}
15 | //
16 | //func (i item) Description() string {
17 | //	return ""
18 | //}
19 | 
20 | //func newMenu() list.Model {
21 | //	items := []list.Item{
22 | //		item{name: "Loader", state: Loader},
23 | //		item{name: "Advanced", state: Advanced},
24 | //		//item{name: "Limited", state: Limited},
25 | //		//item{name: "Characters", state: Characters},
26 | //	}
27 | //	return menu.New(items)
28 | //}
29 | 
```

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

```go
 1 | package adaptive
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 
 6 | 	"github.com/charmbracelet/bubbles/textinput"
 7 | 	"github.com/charmbracelet/lipgloss"
 8 | )
 9 | 
10 | var (
11 | 	promptStyle      = lipgloss.NewStyle().Width(8).PaddingLeft(1)
12 | 	placeholderStyle = lipgloss.NewStyle()
13 | )
14 | 
15 | func newInput(state State) textinput.Model {
16 | 	textinput.New()
17 | 	input := textinput.New()
18 | 	input.Prompt = stateNames[state]
19 | 	input.PromptStyle = promptStyle
20 | 	input.PlaceholderStyle = placeholderStyle
21 | 	input.Cursor.Blink = true
22 | 	input.CharLimit = 3
23 | 	input.SetValue(fmt.Sprintf("16"))
24 | 	return input
25 | }
26 | 
```

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

```go
 1 | package sampling
 2 | 
 3 | import "github.com/nfnt/resize"
 4 | 
 5 | var Functions = []resize.InterpolationFunction{
 6 | 	resize.NearestNeighbor,
 7 | 	resize.Bicubic,
 8 | 	resize.Bilinear,
 9 | 	resize.Lanczos2,
10 | 	resize.Lanczos3,
11 | 	resize.MitchellNetravali,
12 | }
13 | 
14 | var nameMap = map[resize.InterpolationFunction]string{
15 | 	resize.NearestNeighbor:   "Nearest Neighbor",
16 | 	resize.Bicubic:           "Bicubic",
17 | 	resize.Bilinear:          "Bilinear",
18 | 	resize.Lanczos2:          "Lanczos2",
19 | 	resize.Lanczos3:          "Lanczos3",
20 | 	resize.MitchellNetravali: "MitchellNetravali",
21 | }
22 | 
```

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

```go
 1 | package viewer
 2 | 
 3 | import (
 4 | 	tea "github.com/charmbracelet/bubbletea"
 5 | 
 6 | 	"github.com/Zebbeni/ansizalizer/controls/settings"
 7 | 	"github.com/Zebbeni/ansizalizer/event"
 8 | )
 9 | 
10 | type Model struct {
11 | 	imgString string
12 | 	settings  settings.Model
13 | 
14 | 	WaitingOnRender bool
15 | }
16 | 
17 | func New() Model {
18 | 	return Model{}
19 | }
20 | 
21 | func (m Model) Init() tea.Cmd {
22 | 	return nil
23 | }
24 | 
25 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
26 | 	switch msg := msg.(type) {
27 | 	case event.FinishRenderToViewMsg:
28 | 		return m.handleFinishRenderMsg(msg)
29 | 	}
30 | 	return m, nil
31 | }
32 | 
33 | func (m Model) View() string {
34 | 	if m.WaitingOnRender {
35 | 		return ""
36 | 	}
37 | 	return m.imgString
38 | }
39 | 
```

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

```go
 1 | package characters
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/bubbles/textinput"
 5 | 	"github.com/charmbracelet/lipgloss"
 6 | 
 7 | 	"github.com/Zebbeni/ansizalizer/style"
 8 | )
 9 | 
10 | var (
11 | 	promptStyle      = lipgloss.NewStyle().Padding(0, 1, 0, 1)
12 | 	placeholderStyle = lipgloss.NewStyle()
13 | )
14 | 
15 | // TODO: This is basically the same as we have in adaptive. Maybe generalize?
16 | func newInput(prompt string, value string) textinput.Model {
17 | 	textinput.New()
18 | 	input := textinput.New()
19 | 	input.Prompt = prompt
20 | 	input.PromptStyle = style.NormalButtonNode.Copy().Padding(0, 1, 0, 0)
21 | 	input.PlaceholderStyle = placeholderStyle
22 | 	input.Cursor.Blink = true
23 | 	input.SetValue(value)
24 | 	return input
25 | }
26 | 
```

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

```go
 1 | package palettes
 2 | 
 3 | import (
 4 | 	"github.com/makeworld-the-better-one/dither/v2"
 5 | )
 6 | 
 7 | type Matrix struct {
 8 | 	Name   string
 9 | 	Method dither.ErrorDiffusionMatrix
10 | }
11 | 
12 | func getMatrixMenuItems() []Matrix {
13 | 	return []Matrix{
14 | 		Matrix{Name: "Simple2D", Method: dither.Simple2D},
15 | 		Matrix{Name: "FloydSteinberg", Method: dither.FloydSteinberg},
16 | 		Matrix{Name: "JarvisJudiceNinke", Method: dither.JarvisJudiceNinke},
17 | 		Matrix{Name: "Atkinson", Method: dither.Atkinson},
18 | 		Matrix{Name: "Stucki", Method: dither.Stucki},
19 | 		Matrix{Name: "Burkes", Method: dither.Burkes},
20 | 		Matrix{Name: "Sierra", Method: dither.Sierra},
21 | 		Matrix{Name: "StevenPigeon", Method: dither.StevenPigeon},
22 | 	}
23 | }
24 | 
```

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

```go
 1 | package adapt
 2 | 
 3 | import (
 4 | 	"bufio"
 5 | 	"image"
 6 | 	"image/color"
 7 | 	"os"
 8 | 	"path/filepath"
 9 | 	"strings"
10 | 
11 | 	"github.com/mccutchen/palettor"
12 | 
13 | 	"github.com/Zebbeni/ansizalizer/controls/settings/palettes/adaptive"
14 | )
15 | 
16 | func GeneratePalette(m adaptive.Model, imgFilePath string) (color.Palette, string) {
17 | 	if imgFilePath == "" {
18 | 		return nil, ""
19 | 	}
20 | 
21 | 	var img image.Image
22 | 	imgFile, err := os.Open(imgFilePath)
23 | 	if err != nil {
24 | 		return nil, ""
25 | 	}
26 | 	defer imgFile.Close()
27 | 	imageReader := bufio.NewReader(imgFile)
28 | 	img, _, err = image.Decode(imageReader)
29 | 	if err != nil {
30 | 		return nil, ""
31 | 	}
32 | 
33 | 	count, iterations := m.Info()
34 | 	palette, err := palettor.Extract(count, iterations, img)
35 | 
36 | 	name := strings.Split(filepath.Base(imgFilePath), ".")[0]
37 | 
38 | 	return palette.Colors(), name
39 | }
40 | 
```

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

```go
 1 | package app
 2 | 
 3 | import "github.com/charmbracelet/bubbles/list"
 4 | 
 5 | type item struct {
 6 | 	name  string
 7 | 	state State
 8 | }
 9 | 
10 | func (i item) FilterValue() string {
11 | 	return i.name
12 | }
13 | 
14 | func (i item) Title() string {
15 | 	return i.name
16 | }
17 | 
18 | func (i item) Description() string {
19 | 	return ""
20 | }
21 | 
22 | func newMenu() list.Model {
23 | 	items := []list.Item{
24 | 		item{name: "File", state: Browser},
25 | 		item{name: "Settings", state: Settings},
26 | 	}
27 | 	menu := list.New(items, NewDelegate(), 20, 20)
28 | 	menu.SetShowHelp(false)
29 | 	menu.SetShowFilter(false)
30 | 	menu.SetShowTitle(false)
31 | 	menu.SetShowStatusBar(false)
32 | 
33 | 	menu.KeyMap.ForceQuit.Unbind()
34 | 	menu.KeyMap.Quit.Unbind()
35 | 	return menu
36 | }
37 | 
38 | func NewDelegate() list.DefaultDelegate {
39 | 	delegate := list.NewDefaultDelegate()
40 | 	delegate.SetSpacing(0)
41 | 	delegate.ShowDescription = false
42 | 	return delegate
43 | }
44 | 
```

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

```go
 1 | package lospec
 2 | 
 3 | import (
 4 | 	tea "github.com/charmbracelet/bubbletea"
 5 | 	"github.com/charmbracelet/lipgloss"
 6 | 
 7 | 	"github.com/charmbracelet/bubbles/textinput"
 8 | )
 9 | 
10 | var (
11 | 	promptStyle      = lipgloss.NewStyle().Padding(0, 1, 0, 1)
12 | 	placeholderStyle = lipgloss.NewStyle()
13 | )
14 | 
15 | // TODO: This is basically the same as we have in adaptive. Maybe generalize?
16 | func newInput(state State, value string) textinput.Model {
17 | 	textinput.New()
18 | 	input := textinput.New()
19 | 	input.Prompt = stateNames[state]
20 | 	input.PromptStyle = promptStyle
21 | 	input.PlaceholderStyle = placeholderStyle
22 | 	input.Cursor.Blink = true
23 | 	input.SetValue(value)
24 | 	return input
25 | }
26 | 
27 | func (m Model) InitializeList() (Model, tea.Cmd) {
28 | 	m.didInitializeList = true
29 | 	return m.searchLospec(0)
30 | }
31 | 
32 | func (m Model) DidInitializeList() bool {
33 | 	return m.didInitializeList
34 | }
35 | 
```

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

```go
 1 | package display
 2 | 
 3 | import (
 4 | 	tea "github.com/charmbracelet/bubbletea"
 5 | 	"github.com/charmbracelet/lipgloss"
 6 | 
 7 | 	"github.com/Zebbeni/ansizalizer/event"
 8 | 	"github.com/Zebbeni/ansizalizer/style"
 9 | )
10 | 
11 | type Model struct {
12 | 	msg   string
13 | 	width int
14 | }
15 | 
16 | func New() Model {
17 | 	return Model{}
18 | }
19 | 
20 | func (m Model) Init() tea.Cmd {
21 | 	return nil
22 | }
23 | 
24 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
25 | 	switch msg := msg.(type) {
26 | 	case event.DisplayMsg:
27 | 		m.msg = string(msg)
28 | 	}
29 | 	return m, nil
30 | }
31 | 
32 | func (m Model) View() string {
33 | 	// TODO: Switch style based on event type (warning, info, etc.)
34 | 	displayStyle := style.ExtraDimTitle.Copy().Width(m.width - 2)
35 | 	return displayStyle.Border(lipgloss.RoundedBorder()).BorderForeground(style.ExtraDimColor).Render(m.msg)
36 | }
37 | 
38 | func (m Model) SetWidth(w int) Model {
39 | 	m.width = w
40 | 	return m
41 | }
42 | 
```

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

```go
 1 | package settings
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/lipgloss"
 5 | 
 6 | 	"github.com/Zebbeni/ansizalizer/style"
 7 | )
 8 | 
 9 | var (
10 | 	activeColor = lipgloss.Color("#aaaaaa")
11 | 	focusColor  = lipgloss.Color("#ffffff")
12 | 	normalColor = lipgloss.Color("#555555")
13 | )
14 | 
15 | func (m Model) renderWithBorder(content string, state State) string {
16 | 	renderColor := normalColor
17 | 	if m.active == state {
18 | 		renderColor = activeColor
19 | 	} else if m.focus == state {
20 | 		renderColor = focusColor
21 | 	}
22 | 
23 | 	textStyle := lipgloss.NewStyle().
24 | 		AlignHorizontal(lipgloss.Center).
25 | 		Padding(0, 1, 0, 1).
26 | 		Foreground(renderColor)
27 | 	borderStyle := lipgloss.NewStyle().
28 | 		Border(lipgloss.RoundedBorder()).
29 | 		BorderForeground(renderColor)
30 | 
31 | 	renderer := style.BoxWithLabel{
32 | 		BoxStyle:   borderStyle,
33 | 		LabelStyle: textStyle,
34 | 	}
35 | 
36 | 	return renderer.Render(stateTitles[state], content, m.width-2)
37 | }
38 | 
```

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

```go
 1 | package sampling
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/bubbles/key"
 5 | 	tea "github.com/charmbracelet/bubbletea"
 6 | 
 7 | 	"github.com/Zebbeni/ansizalizer/event"
 8 | )
 9 | 
10 | func (m Model) handleEsc() (Model, tea.Cmd) {
11 | 	m.ShouldClose = true
12 | 	m.list.SetDelegate(NewDelegate(false))
13 | 	return m, nil
14 | }
15 | 
16 | func (m Model) handleEnter() (Model, tea.Cmd) {
17 | 	m.ShouldClose = true
18 | 	m.list.SetDelegate(NewDelegate(false))
19 | 	return m, nil
20 | }
21 | 
22 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
23 | 	if key.Matches(msg, event.KeyMap.Up) && m.list.Index() == 0 {
24 | 		m.list.SetDelegate(NewDelegate(false))
25 | 		m.ShouldClose = true
26 | 		return m, nil
27 | 	}
28 | 
29 | 	var cmd tea.Cmd
30 | 	m.list, cmd = m.list.Update(msg)
31 | 	m.list.SetDelegate(NewDelegate(true))
32 | 	selectedItem := m.list.SelectedItem().(item)
33 | 
34 | 	if selectedItem.Function == m.Function {
35 | 		return m, cmd
36 | 	}
37 | 
38 | 	m.Function = selectedItem.Function
39 | 
40 | 	return m, tea.Batch(cmd, event.StartRenderToViewCmd)
41 | }
42 | 
```

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

```go
 1 | package size
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"strconv"
 6 | 
 7 | 	"github.com/charmbracelet/bubbles/textinput"
 8 | 	"github.com/charmbracelet/lipgloss"
 9 | )
10 | 
11 | var (
12 | 	promptStyle      = lipgloss.NewStyle().Width(8).Padding(0, 0, 0, 1)
13 | 	placeholderStyle = lipgloss.NewStyle()
14 | 
15 | 	floatPromptStyle      = lipgloss.NewStyle().Padding(0, 1)
16 | 	floatPlaceholderStyle = lipgloss.NewStyle()
17 | )
18 | 
19 | func newInput(state State, value int) textinput.Model {
20 | 	textinput.New()
21 | 	input := textinput.New()
22 | 	input.Prompt = stateNames[state]
23 | 	input.PromptStyle = promptStyle
24 | 	input.PlaceholderStyle = placeholderStyle
25 | 	input.CharLimit = 3
26 | 	input.SetValue(strconv.Itoa(value))
27 | 	return input
28 | }
29 | 
30 | func newFloatInput(state State, value float64) textinput.Model {
31 | 	textinput.New()
32 | 	input := textinput.New()
33 | 	input.Prompt = stateNames[state]
34 | 	input.PromptStyle = floatPromptStyle
35 | 	input.PlaceholderStyle = floatPlaceholderStyle
36 | 	input.CharLimit = 5
37 | 	input.SetValue(fmt.Sprintf("%1.2f", value))
38 | 	return input
39 | }
40 | 
```

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

```go
 1 | package colors
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/lipgloss"
 5 | 
 6 | 	"github.com/Zebbeni/ansizalizer/style"
 7 | )
 8 | 
 9 | func (m Model) drawPaletteToggles() string {
10 | 	title := style.DimmedTitle.Copy().PaddingLeft(1).Render("Mode:")
11 | 
12 | 	trueColorStyle := style.NormalButtonNode
13 | 	if m.IsActive && m.focus == UseTrueColor {
14 | 		trueColorStyle = style.FocusButtonNode
15 | 	} else if m.mode == UseTrueColor {
16 | 		trueColorStyle = style.ActiveButtonNode
17 | 	}
18 | 	trueColorNode := trueColorStyle.Render("True Color")
19 | 	trueColorNode = lipgloss.NewStyle().PaddingLeft(1).Render(trueColorNode)
20 | 
21 | 	palettedStyle := style.NormalButtonNode
22 | 	if m.IsActive && m.focus == UsePalette {
23 | 		palettedStyle = style.FocusButtonNode
24 | 	} else if m.mode == UsePalette {
25 | 		palettedStyle = style.ActiveButtonNode
26 | 	}
27 | 	palettedNode := palettedStyle.Render("Palette")
28 | 	palettedNode = lipgloss.NewStyle().PaddingLeft(1).Render(palettedNode)
29 | 
30 | 	return lipgloss.JoinHorizontal(lipgloss.Left, title, trueColorNode, palettedNode)
31 | }
32 | 
```

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

```go
 1 | package palette
 2 | 
 3 | import (
 4 | 	"image/color"
 5 | 	"math"
 6 | 
 7 | 	"github.com/charmbracelet/lipgloss"
 8 | 	"github.com/lucasb-eyer/go-colorful"
 9 | )
10 | 
11 | func Palette(palette color.Palette, w, h int) string {
12 | 	runes := make([]string, len(palette)/2+1)
13 | 	rows := make([]string, 0, h)
14 | 	for idx := 0; idx < len(palette); idx += 2 {
15 | 		var fg, bg colorful.Color
16 | 		var lipFg, lipBg lipgloss.Color
17 | 
18 | 		fg, _ = colorful.MakeColor(palette[idx])
19 | 		lipFg = lipgloss.Color(fg.Hex())
20 | 		style := lipgloss.NewStyle().Foreground(lipFg)
21 | 
22 | 		if idx+1 < len(palette) {
23 | 			bg, _ = colorful.MakeColor(palette[idx+1])
24 | 			lipBg = lipgloss.Color(bg.Hex())
25 | 			style = style.Copy().Background(lipBg)
26 | 		}
27 | 		runes[idx/2] = style.Render(string('▀'))
28 | 	}
29 | 	for i := 0; i < h; i++ {
30 | 		start := w * i
31 | 		if start >= len(runes) {
32 | 			break
33 | 		}
34 | 		stop := int(math.Min(float64(w*(i+1)), float64(len(runes))))
35 | 		rows = append(rows, "")
36 | 		rows[i] = lipgloss.JoinHorizontal(lipgloss.Left, runes[start:stop]...)
37 | 	}
38 | 	return lipgloss.JoinVertical(lipgloss.Left, rows...)
39 | }
40 | 
```

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

```go
 1 | package destination
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"path/filepath"
 6 | 
 7 | 	"github.com/charmbracelet/lipgloss"
 8 | 
 9 | 	"github.com/Zebbeni/ansizalizer/style"
10 | )
11 | 
12 | func (m Model) drawSelected() string {
13 | 	title := style.DimmedTitle.Copy().Render("Selected")
14 | 
15 | 	valueStyle := style.DimmedTitle.Copy()
16 | 
17 | 	if Input == m.focus {
18 | 		if m.IsActive {
19 | 			valueStyle = style.SelectedTitle.Copy()
20 | 		} else {
21 | 			valueStyle = style.NormalTitle.Copy()
22 | 		}
23 | 	}
24 | 	valueStyle.Padding(0, 0, 1, 0)
25 | 
26 | 	path := m.Browser.SelectedDir
27 | 
28 | 	parent := filepath.Base(filepath.Dir(path))
29 | 	selected := filepath.Base(path)
30 | 	value := fmt.Sprintf("%s/%s", parent, selected)
31 | 
32 | 	valueRunes := []rune(value)
33 | 	if len(valueRunes) > m.width {
34 | 		value = string(valueRunes[len(valueRunes)-m.width:])
35 | 	}
36 | 
37 | 	valueContent := valueStyle.Render(value)
38 | 
39 | 	valueWidth := m.width
40 | 	widthStyle := lipgloss.NewStyle().Width(valueWidth).AlignHorizontal(lipgloss.Center)
41 | 	content := lipgloss.JoinVertical(lipgloss.Center, title, valueContent)
42 | 
43 | 	return widthStyle.Render(content)
44 | }
45 | 
46 | func drawBrowserTitle() string {
47 | 	return style.DimmedTitle.Copy().Padding(0, 2, 1, 2).Render("Select a directory")
48 | }
49 | 
```

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

```go
 1 | package destination
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/bubbles/key"
 5 | 	tea "github.com/charmbracelet/bubbletea"
 6 | 
 7 | 	"github.com/Zebbeni/ansizalizer/event"
 8 | )
 9 | 
10 | type Direction int
11 | 
12 | const (
13 | 	Up Direction = iota
14 | 	Down
15 | )
16 | 
17 | var (
18 | 	navMap = map[Direction]map[State]State{
19 | 		Down: {Input: Browser},
20 | 		Up:   {Browser: Input},
21 | 	}
22 | )
23 | 
24 | func (m Model) handleEsc() (Model, tea.Cmd) {
25 | 	m.ShouldClose = true
26 | 	m.IsActive = false
27 | 	return m, nil
28 | }
29 | 
30 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
31 | 	switch {
32 | 	case key.Matches(msg, event.KeyMap.Down):
33 | 		if next, hasNext := navMap[Down][m.focus]; hasNext {
34 | 			m.focus = next
35 | 		}
36 | 	case key.Matches(msg, event.KeyMap.Up):
37 | 		if next, hasNext := navMap[Up][m.focus]; hasNext {
38 | 			m.focus = next
39 | 		} else {
40 | 			m.ShouldClose = true
41 | 		}
42 | 	}
43 | 	return m, nil
44 | }
45 | 
46 | func (m Model) handleEnter() (Model, tea.Cmd) {
47 | 	switch m.focus {
48 | 	case Input:
49 | 		m.focus = Browser
50 | 	}
51 | 	return m, nil
52 | }
53 | 
54 | func (m Model) handleDstBrowserUpdate(msg tea.Msg) (Model, tea.Cmd) {
55 | 	var cmd tea.Cmd
56 | 	m.Browser, cmd = m.Browser.Update(msg)
57 | 	m.selectedDir = m.Browser.SelectedDir
58 | 
59 | 	if m.Browser.ShouldClose {
60 | 		m.focus = Input
61 | 		m.Browser.ShouldClose = false
62 | 	}
63 | 	return m, cmd
64 | }
65 | 
```

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

```go
 1 | package process
 2 | 
 3 | import (
 4 | 	"bufio"
 5 | 	"image"
 6 | 	"os"
 7 | 
 8 | 	"github.com/lucasb-eyer/go-colorful"
 9 | 
10 | 	"github.com/Zebbeni/ansizalizer/controls/settings"
11 | 	"github.com/Zebbeni/ansizalizer/controls/settings/characters"
12 | )
13 | 
14 | var (
15 | 	black = colorful.Color{}
16 | )
17 | 
18 | func RenderImageFile(s settings.Model, imgFilePath string) string {
19 | 	if imgFilePath == "" {
20 | 		return "Browse an image to render"
21 | 	}
22 | 
23 | 	var img image.Image
24 | 	imgFile, err := os.Open(imgFilePath)
25 | 	if err != nil {
26 | 		return "Could not open image " + imgFilePath
27 | 	}
28 | 	defer imgFile.Close()
29 | 	imageReader := bufio.NewReader(imgFile)
30 | 	img, _, err = image.Decode(imageReader)
31 | 	if err != nil {
32 | 		return "Could not decode image " + imgFilePath
33 | 	}
34 | 
35 | 	renderer := New(s)
36 | 	imgString := renderer.process(img)
37 | 	return imgString
38 | }
39 | 
40 | func (m Renderer) process(input image.Image) string {
41 | 	isTrueColor, _, palette := m.Settings.Colors.GetSelected()
42 | 	if !isTrueColor && len(palette.Colors()) == 0 {
43 | 		return "Choose a color palette"
44 | 	}
45 | 	mode, _, _, _ := m.Settings.Characters.Selected()
46 | 	switch mode {
47 | 	case characters.Ascii:
48 | 		return m.processAscii(input)
49 | 	case characters.Unicode:
50 | 		return m.processUnicode(input)
51 | 	case characters.Custom:
52 | 		return m.processCustom(input)
53 | 	}
54 | 	return "Choose a character type"
55 | }
56 | 
```

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

```go
 1 | package lospec
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/bubbles/list"
 5 | 	"github.com/charmbracelet/lipgloss"
 6 | 
 7 | 	"github.com/Zebbeni/ansizalizer/style"
 8 | )
 9 | 
10 | func CreateList(items []list.Item, w int) list.Model {
11 | 	newList := list.New(items, NewDelegate(), w, 22)
12 | 
13 | 	newList.KeyMap.ForceQuit.Unbind()
14 | 	newList.KeyMap.Quit.Unbind()
15 | 	newList.SetShowHelp(false)
16 | 	newList.SetShowStatusBar(false)
17 | 	newList.SetShowTitle(false)
18 | 	newList.SetFilteringEnabled(false)
19 | 
20 | 	return newList
21 | }
22 | 
23 | func NewDelegate() list.DefaultDelegate {
24 | 	delegate := list.NewDefaultDelegate()
25 | 	delegate.SetSpacing(0)
26 | 	delegate.ShowDescription = true
27 | 	delegate.Styles = ItemStyles()
28 | 	return delegate
29 | }
30 | 
31 | func ItemStyles() (s list.DefaultItemStyles) {
32 | 	s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2)
33 | 	s.NormalDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2)
34 | 
35 | 	s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1).
36 | 		Border(lipgloss.NormalBorder(), false, false, false, true).
37 | 		BorderForeground(style.SelectedColor1)
38 | 	s.SelectedDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2)
39 | 
40 | 	s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0)
41 | 	s.DimmedDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2)
42 | 
43 | 	return s
44 | }
45 | 
```

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

```go
 1 | package event
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/bubbles/key"
 5 | )
 6 | 
 7 | type Map struct {
 8 | 	Enter key.Binding
 9 | 	Nav   key.Binding
10 | 	Right key.Binding
11 | 	Left  key.Binding
12 | 	Up    key.Binding
13 | 	Down  key.Binding
14 | 	Copy  key.Binding
15 | 	Save  key.Binding
16 | 	Esc   key.Binding
17 | }
18 | 
19 | var KeyMap Map
20 | 
21 | func InitKeyMap() {
22 | 	KeyMap = Map{
23 | 		Enter: key.NewBinding(
24 | 			key.WithKeys("return", "enter"),
25 | 			key.WithHelp("↲/enter", "select/focus menu"),
26 | 		),
27 | 		Nav: key.NewBinding(
28 | 			key.WithKeys("up", "down", "right", "left"),
29 | 			key.WithHelp("↕/↔", "navigate"),
30 | 		),
31 | 		Right: key.NewBinding(
32 | 			key.WithKeys("right"),
33 | 		),
34 | 		Left: key.NewBinding(
35 | 			key.WithKeys("left"),
36 | 		),
37 | 		Up: key.NewBinding(
38 | 			key.WithKeys("up"),
39 | 		),
40 | 		Down: key.NewBinding(
41 | 			key.WithKeys("down"),
42 | 		),
43 | 		Copy: key.NewBinding(
44 | 			key.WithKeys("ctrl+c"),
45 | 			key.WithHelp("ctrl+c", "copy to clipboard")),
46 | 		Save: key.NewBinding(
47 | 			key.WithKeys("ctrl+s"),
48 | 			key.WithHelp("ctrl+s", "save to file")),
49 | 		Esc: key.NewBinding(
50 | 			key.WithKeys("esc"),
51 | 			key.WithHelp("esc", "back/exit menu"),
52 | 		),
53 | 	}
54 | }
55 | 
56 | func (k Map) ShortHelp() []key.Binding {
57 | 	return []key.Binding{k.Nav, k.Enter, k.Esc, k.Copy, k.Save}
58 | }
59 | 
60 | func (k Map) FullHelp() [][]key.Binding {
61 | 	return [][]key.Binding{{k.Nav, k.Enter, k.Esc, k.Copy, k.Save}}
62 | }
63 | 
```

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

```go
 1 | package sampling
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/bubbles/key"
 5 | 	"github.com/charmbracelet/bubbles/list"
 6 | 	tea "github.com/charmbracelet/bubbletea"
 7 | 	"github.com/charmbracelet/lipgloss"
 8 | 	"github.com/nfnt/resize"
 9 | 
10 | 	"github.com/Zebbeni/ansizalizer/event"
11 | 	"github.com/Zebbeni/ansizalizer/style"
12 | )
13 | 
14 | type Model struct {
15 | 	Function resize.InterpolationFunction
16 | 
17 | 	list list.Model
18 | 
19 | 	IsActive    bool
20 | 	ShouldClose bool
21 | }
22 | 
23 | func New(w int) Model {
24 | 	items := menuItems()
25 | 	selected := items[0].(item)
26 | 	menu := newMenu(items, w, len(items))
27 | 
28 | 	return Model{
29 | 		Function:    selected.Function,
30 | 		list:        menu,
31 | 		IsActive:    false,
32 | 		ShouldClose: false,
33 | 	}
34 | }
35 | 
36 | func (m Model) Init() tea.Cmd {
37 | 	return nil
38 | }
39 | 
40 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
41 | 	switch msg := msg.(type) {
42 | 	case tea.KeyMsg:
43 | 		switch {
44 | 		case key.Matches(msg, event.KeyMap.Esc):
45 | 			return m.handleEsc()
46 | 		case key.Matches(msg, event.KeyMap.Enter):
47 | 			return m.handleEnter()
48 | 		case key.Matches(msg, event.KeyMap.Nav):
49 | 			return m.handleNav(msg)
50 | 		}
51 | 	}
52 | 	return m, nil
53 | }
54 | 
55 | func (m Model) View() string {
56 | 	prompt := style.DimmedTitle.Copy().Render("Select Method")
57 | 	menu := m.list.View()
58 | 	content := lipgloss.JoinVertical(lipgloss.Left, prompt, menu)
59 | 	return lipgloss.NewStyle().Padding(0, 1).Render(content)
60 | }
61 | 
```

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

```go
 1 | package app
 2 | 
 3 | import (
 4 | 	"os"
 5 | 	"time"
 6 | 
 7 | 	"golang.org/x/term"
 8 | 
 9 | 	tea "github.com/charmbracelet/bubbletea"
10 | )
11 | 
12 | // There is (currently) no support on Windows for detecting resize events, so
13 | // we instead poll at regular intervals to check if the terminal size changed.
14 | // If a resize is detected in this way, we send a WindowSizeMsg with the new
15 | // dimensions to bubbletea, and handle it in the Model event handler
16 | type checkSizeMsg int
17 | 
18 | const (
19 | 	resizeCheckDuration = time.Second / 4
20 | )
21 | 
22 | func (m Model) handleSizeMsg(msg tea.WindowSizeMsg) (Model, tea.Cmd) {
23 | 	w, h := msg.Width, msg.Height
24 | 	m.w, m.h = w, h
25 | 	m.display = m.display.SetWidth(m.rPanelWidth())
26 | 
27 | 	tea.ClearScreen()
28 | 	return m, nil
29 | }
30 | 
31 | func (m Model) handleCheckSizeMsg() (Model, tea.Cmd) {
32 | 	w, h, _ := term.GetSize(int(os.Stdout.Fd()))
33 | 	if w == m.w && h == m.h {
34 | 		return m, pollForSizeChange
35 | 	}
36 | 	updateSizeCmd := func() tea.Msg {
37 | 		return tea.WindowSizeMsg{Width: w, Height: h}
38 | 	}
39 | 	return m, tea.Batch(pollForSizeChange, updateSizeCmd)
40 | }
41 | 
42 | func pollForSizeChange() tea.Msg {
43 | 	time.Sleep(resizeCheckDuration)
44 | 	return checkSizeMsg(1)
45 | }
46 | 
47 | func (m Model) leftPanelHeight() int {
48 | 	return m.h - helpHeight
49 | }
50 | 
51 | func (m Model) rPanelWidth() int {
52 | 	return m.w - controlsWidth
53 | }
54 | 
55 | func (m Model) rPanelHeight() int {
56 | 	return m.h - helpHeight
57 | }
58 | 
```

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

```go
 1 | package export
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/lipgloss"
 5 | 
 6 | 	"github.com/Zebbeni/ansizalizer/style"
 7 | )
 8 | 
 9 | var (
10 | 	activeColor = lipgloss.Color("#aaaaaa")
11 | 	focusColor  = lipgloss.Color("#ffffff")
12 | 	normalColor = lipgloss.Color("#555555")
13 | )
14 | 
15 | func (m Model) renderWithBorder(content string, state State) string {
16 | 	renderColor := normalColor
17 | 	if m.active == state {
18 | 		renderColor = activeColor
19 | 	} else if m.focus == state {
20 | 		renderColor = focusColor
21 | 	}
22 | 
23 | 	textStyle := lipgloss.NewStyle().
24 | 		AlignHorizontal(lipgloss.Center).
25 | 		Padding(0, 1, 0, 1).
26 | 		Foreground(renderColor)
27 | 	borderStyle := lipgloss.NewStyle().
28 | 		Border(lipgloss.RoundedBorder()).
29 | 		BorderForeground(renderColor)
30 | 
31 | 	renderer := style.BoxWithLabel{
32 | 		BoxStyle:   borderStyle,
33 | 		LabelStyle: textStyle,
34 | 	}
35 | 
36 | 	return renderer.Render(stateTitles[state], content, m.width-2)
37 | }
38 | 
39 | func (m Model) drawProcessButton() string {
40 | 	buttonStyle := style.NormalButton
41 | 	if m.focus == Process {
42 | 		buttonStyle = style.FocusButton
43 | 	}
44 | 
45 | 	centerStyle := lipgloss.NewStyle().AlignHorizontal(lipgloss.Center)
46 | 
47 | 	internalStyle := centerStyle.Copy().Width(m.width - 2)
48 | 	title := internalStyle.Render(stateTitles[Process])
49 | 	button := buttonStyle.Render(title)
50 | 
51 | 	return centerStyle.Copy().Width(m.width).AlignHorizontal(lipgloss.Center).Render(button)
52 | }
53 | 
```

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

```go
 1 | package loader
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/bubbles/list"
 5 | 	"github.com/charmbracelet/lipgloss"
 6 | 
 7 | 	"github.com/Zebbeni/ansizalizer/style"
 8 | )
 9 | 
10 | const (
11 | 	maxWidth          = 30
12 | 	maxNormalHeight   = 1
13 | 	maxSelectedHeight = 2
14 | )
15 | 
16 | // NewItemStyles returns style definitions for a default item.
17 | // DefaultItemView for when these come into play.
18 | func NewItemStyles() (s list.DefaultItemStyles) {
19 | 
20 | 	s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2)
21 | 	s.NormalDesc = style.DimmedParagraph.Copy().MaxHeight(maxNormalHeight).Padding(0, 0, 0, 2)
22 | 
23 | 	s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1).
24 | 		Border(lipgloss.NormalBorder(), false, false, false, true).
25 | 		BorderForeground(style.SelectedColor1)
26 | 	s.SelectedDesc = style.SelectedTitle.Copy().MaxHeight(maxSelectedHeight).Padding(0, 0, 0, 1).
27 | 		Border(lipgloss.NormalBorder(), false, false, false, true).
28 | 		BorderForeground(style.SelectedColor1)
29 | 
30 | 	s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0)
31 | 	s.DimmedDesc = style.DimmedParagraph.Copy().MaxHeight(maxNormalHeight).Padding(0, 0, 0, 2)
32 | 
33 | 	return s
34 | }
35 | 
36 | func (m Model) drawTitle() string {
37 | 	title := style.DimmedTitle.Copy().Italic(true).Render("Load from .hex file")
38 | 	return lipgloss.NewStyle().Width(m.width).PaddingBottom(1).AlignHorizontal(lipgloss.Center).Render(title)
39 | }
40 | 
```

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

```go
 1 | package menu
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/bubbles/list"
 5 | 	"github.com/charmbracelet/lipgloss"
 6 | 
 7 | 	"github.com/Zebbeni/ansizalizer/style"
 8 | )
 9 | 
10 | func New(items []list.Item, w int) list.Model {
11 | 	newList := list.New(items, NewDelegate(), w, 18)
12 | 
13 | 	newList.KeyMap.ForceQuit.Unbind()
14 | 	newList.KeyMap.Quit.Unbind()
15 | 	newList.SetShowHelp(false)
16 | 	newList.SetShowStatusBar(false)
17 | 	newList.SetShowTitle(false)
18 | 	newList.SetFilteringEnabled(false)
19 | 
20 | 	return newList
21 | }
22 | 
23 | func NewDelegate() list.DefaultDelegate {
24 | 	delegate := list.NewDefaultDelegate()
25 | 	delegate.SetSpacing(0)
26 | 	delegate.ShowDescription = false
27 | 	delegate.Styles = ItemStyles()
28 | 	return delegate
29 | }
30 | 
31 | func ItemStyles() (s list.DefaultItemStyles) {
32 | 	s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2)
33 | 	s.NormalDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2)
34 | 
35 | 	s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1).
36 | 		Border(lipgloss.NormalBorder(), false, false, false, true).
37 | 		BorderForeground(style.SelectedColor1)
38 | 	s.NormalDesc = style.SelectedTitle.Copy().MaxHeight(1).Padding(0, 0, 0, 2).
39 | 		Border(lipgloss.NormalBorder(), false, false, false, true).
40 | 		BorderForeground(style.SelectedColor1)
41 | 
42 | 	s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0)
43 | 	s.DimmedDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2)
44 | 
45 | 	return s
46 | }
47 | 
```

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

```go
 1 | package palettes
 2 | 
 3 | import "github.com/charmbracelet/lipgloss"
 4 | 
 5 | var (
 6 | 	stateOrder = []State{Load, Adapt, Lospec}
 7 | 	stateNames = map[State]string{
 8 | 		Load:   "Load",
 9 | 		Adapt:  "Sample",
10 | 		Lospec: "Lospec",
11 | 	}
12 | 
13 | 	activeStyle = lipgloss.NewStyle().
14 | 			BorderStyle(lipgloss.RoundedBorder()).
15 | 			BorderForeground(lipgloss.Color("#aaaaaa")).
16 | 			Foreground(lipgloss.Color("#aaaaaa"))
17 | 	focusStyle = lipgloss.NewStyle().
18 | 			BorderStyle(lipgloss.RoundedBorder()).
19 | 			BorderForeground(lipgloss.Color("#ffffff")).
20 | 			Foreground(lipgloss.Color("#ffffff"))
21 | 	normalStyle = lipgloss.NewStyle().
22 | 			BorderStyle(lipgloss.RoundedBorder()).
23 | 			BorderForeground(lipgloss.Color("#555555")).
24 | 			Foreground(lipgloss.Color("#555555"))
25 | 	titleStyle = lipgloss.NewStyle().
26 | 			Foreground(lipgloss.Color("#888888"))
27 | )
28 | 
29 | func (m Model) drawTitle() string {
30 | 	return titleStyle.Copy().Italic(true).Width(m.width).Align(lipgloss.Center).Render("Colors")
31 | }
32 | 
33 | func (m Model) drawButtons() string {
34 | 	buttons := make([]string, len(stateOrder))
35 | 	for i, state := range stateOrder {
36 | 		style := normalStyle
37 | 		if m.IsActive && state == m.focus {
38 | 			style = focusStyle
39 | 		} else if state == m.selected {
40 | 			style = activeStyle
41 | 		}
42 | 		buttons[i] = style.Copy().AlignHorizontal(lipgloss.Center).Padding(0, 1).Render(stateNames[state])
43 | 	}
44 | 	return lipgloss.JoinHorizontal(lipgloss.Left, buttons...)
45 | }
46 | 
```

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

```go
 1 | package browser
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"os"
 6 | 	"path/filepath"
 7 | 
 8 | 	"github.com/charmbracelet/bubbles/list"
 9 | )
10 | 
11 | type item struct {
12 | 	name  string
13 | 	path  string
14 | 	isDir bool
15 | 	isTop bool
16 | }
17 | 
18 | func (i item) FilterValue() string {
19 | 	return i.name
20 | }
21 | 
22 | func (i item) Title() string {
23 | 	if i.isTop {
24 | 		return "↑"
25 | 	}
26 | 	if i.isDir {
27 | 		return fmt.Sprintf("%s/", i.name)
28 | 	}
29 | 	return i.name
30 | }
31 | 
32 | func (i item) Description() string {
33 | 	if i.isDir {
34 | 		return "directory"
35 | 	}
36 | 	return "file"
37 | }
38 | 
39 | func getItems(extensions map[string]bool, dir string) []list.Item {
40 | 	entries, err := os.ReadDir(dir)
41 | 	if err != nil {
42 | 		fmt.Println("Error reading directory entries:", err)
43 | 		os.Exit(1)
44 | 	}
45 | 
46 | 	parentPath := filepath.Dir(dir)
47 | 	parentName := filepath.Base(parentPath)
48 | 	parentItem := item{name: parentName, path: parentPath, isDir: true, isTop: true}
49 | 
50 | 	dirItems := []list.Item{parentItem}
51 | 	fileItems := make([]list.Item, 0)
52 | 
53 | 	for _, e := range entries {
54 | 		path := fmt.Sprintf("%s/%s", dir, e.Name())
55 | 
56 | 		if e.IsDir() {
57 | 			name := e.Name()
58 | 			dirItem := item{name: name, path: path, isDir: true, isTop: false}
59 | 			dirItems = append(dirItems, dirItem)
60 | 			continue
61 | 		}
62 | 
63 | 		ext := filepath.Ext(e.Name())
64 | 		if _, ok := extensions[ext]; ok {
65 | 			fileItem := item{name: e.Name(), path: path, isDir: false, isTop: false}
66 | 			fileItems = append(fileItems, fileItem)
67 | 		}
68 | 	}
69 | 
70 | 	return append(dirItems, fileItems...)
71 | }
72 | 
```

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

```go
 1 | package export
 2 | 
 3 | import (
 4 | 	tea "github.com/charmbracelet/bubbletea"
 5 | 	"github.com/charmbracelet/lipgloss"
 6 | 
 7 | 	"github.com/Zebbeni/ansizalizer/controls/export/destination"
 8 | 	"github.com/Zebbeni/ansizalizer/controls/export/source"
 9 | )
10 | 
11 | type State int
12 | 
13 | const (
14 | 	None State = iota
15 | 	Source
16 | 	Destination
17 | 	Process
18 | )
19 | 
20 | var (
21 | 	stateTitles = map[State]string{
22 | 		Source:      "Source",
23 | 		Destination: "Destination",
24 | 		Process:     "Process",
25 | 	}
26 | )
27 | 
28 | type Model struct {
29 | 	active State
30 | 	focus  State
31 | 
32 | 	Source      source.Model
33 | 	Destination destination.Model
34 | 
35 | 	ShouldClose   bool
36 | 	ShouldUnfocus bool
37 | 
38 | 	width int
39 | }
40 | 
41 | func New(w int) Model {
42 | 	return Model{
43 | 		focus:         Source,
44 | 		active:        None,
45 | 		Source:        source.New(w - 2),
46 | 		Destination:   destination.New(w - 2),
47 | 		ShouldClose:   false,
48 | 		ShouldUnfocus: false,
49 | 		width:         w,
50 | 	}
51 | }
52 | 
53 | func (m Model) Init() tea.Cmd {
54 | 	return nil
55 | }
56 | 
57 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
58 | 	switch m.active {
59 | 	case Source:
60 | 		return m.handleSourceUpdate(msg)
61 | 	case Destination:
62 | 		return m.handleDestinationUpdate(msg)
63 | 	}
64 | 
65 | 	keyMsg, ok := msg.(tea.KeyMsg)
66 | 	if !ok {
67 | 		return m, nil
68 | 	}
69 | 
70 | 	return m.handleKeyMsg(keyMsg)
71 | }
72 | 
73 | func (m Model) View() string {
74 | 	src := m.renderWithBorder(m.Source.View(), Source)
75 | 	dst := m.renderWithBorder(m.Destination.View(), Destination)
76 | 	process := m.drawProcessButton()
77 | 	return lipgloss.JoinVertical(lipgloss.Left, src, dst, process)
78 | }
79 | 
```

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

```go
 1 | package dithering
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/bubbles/list"
 5 | 	tea "github.com/charmbracelet/bubbletea"
 6 | 	"github.com/charmbracelet/lipgloss"
 7 | 	"github.com/makeworld-the-better-one/dither/v2"
 8 | )
 9 | 
10 | type State int
11 | 
12 | const (
13 | 	DitherOn State = iota
14 | 	DitherOff
15 | 	SerpentineOn
16 | 	SerpentineOff
17 | 	Matrix
18 | )
19 | 
20 | type Model struct {
21 | 	focus State
22 | 
23 | 	doDithering  bool
24 | 	doSerpentine bool
25 | 	matrix       dither.ErrorDiffusionMatrix
26 | 
27 | 	list list.Model
28 | 
29 | 	IsActive    bool
30 | 	ShouldClose bool
31 | 
32 | 	width int
33 | }
34 | 
35 | func New(w int) Model {
36 | 	return Model{
37 | 		focus:        DitherOff,
38 | 		doDithering:  false,
39 | 		doSerpentine: false,
40 | 		matrix:       dither.FloydSteinberg,
41 | 		list:         newMatrixMenu(w),
42 | 		ShouldClose:  false,
43 | 		IsActive:     false,
44 | 		width:        w,
45 | 	}
46 | }
47 | 
48 | func (m Model) Init() tea.Cmd {
49 | 	return nil
50 | }
51 | 
52 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
53 | 	if m.focus == Matrix {
54 | 		return m.handleMatrixListUpdate(msg)
55 | 	}
56 | 
57 | 	if keyMsg, ok := msg.(tea.KeyMsg); ok {
58 | 		return m.handleKeyMsg(keyMsg)
59 | 	}
60 | 	return m, nil
61 | }
62 | 
63 | func (m Model) View() string {
64 | 	ditheringOpts := m.drawDitheringOptions()
65 | 	serpentineOpts := m.drawSerpentineOptions()
66 | 	matrixList := m.drawMatrix()
67 | 	content := lipgloss.JoinVertical(lipgloss.Left, ditheringOpts, serpentineOpts, matrixList)
68 | 	return lipgloss.NewStyle().Padding(0, 1).Render(content)
69 | }
70 | 
71 | func (m Model) Settings() (bool, bool, dither.ErrorDiffusionMatrix) {
72 | 	return m.doDithering, m.doSerpentine, m.matrix
73 | }
74 | 
```

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

```go
 1 | package browser
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"os"
 6 | 	"path/filepath"
 7 | 
 8 | 	"github.com/charmbracelet/bubbles/key"
 9 | 	"github.com/charmbracelet/bubbles/list"
10 | 	tea "github.com/charmbracelet/bubbletea"
11 | 	"github.com/charmbracelet/lipgloss"
12 | 
13 | 	"github.com/Zebbeni/ansizalizer/event"
14 | )
15 | 
16 | type Model struct {
17 | 	SelectedDir  string
18 | 	SelectedFile string
19 | 	ActiveDir    string
20 | 	ActiveFile   string
21 | 
22 | 	lists          []list.Model
23 | 	fileExtensions map[string]bool
24 | 
25 | 	ShouldClose bool
26 | 
27 | 	width int
28 | }
29 | 
30 | func New(exts map[string]bool, w int) Model {
31 | 	dir, err := os.Getwd()
32 | 	if err != nil {
33 | 		fmt.Println("Error getting starting directory:", err)
34 | 		os.Exit(1)
35 | 	}
36 | 
37 | 	m := Model{
38 | 		width:          w,
39 | 		fileExtensions: exts,
40 | 	}
41 | 	m = m.addListForDirectory(dir)
42 | 
43 | 	return m
44 | }
45 | 
46 | func (m Model) Init() tea.Cmd {
47 | 	return nil
48 | }
49 | 
50 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
51 | 	switch msg := msg.(type) {
52 | 	case tea.KeyMsg:
53 | 		switch {
54 | 		case key.Matches(msg, event.KeyMap.Esc):
55 | 			return m.handleEsc()
56 | 		case key.Matches(msg, event.KeyMap.Nav):
57 | 			return m.handleNav(msg)
58 | 		case key.Matches(msg, event.KeyMap.Enter):
59 | 			return m.handleEnter()
60 | 		}
61 | 	}
62 | 	return m, nil
63 | }
64 | 
65 | func (m Model) currentList() list.Model {
66 | 	return m.lists[m.listIndex()]
67 | }
68 | 
69 | func (m Model) listIndex() int {
70 | 	return len(m.lists) - 1
71 | }
72 | 
73 | func (m Model) View() string {
74 | 	browser := m.currentList().View()
75 | 	return lipgloss.JoinVertical(lipgloss.Left, browser)
76 | }
77 | 
78 | func (m Model) ActiveFilename() string {
79 | 	return filepath.Base(m.ActiveFile)
80 | }
81 | 
```

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

```go
 1 | package destination
 2 | 
 3 | import (
 4 | 	"os"
 5 | 
 6 | 	"github.com/charmbracelet/bubbles/key"
 7 | 	tea "github.com/charmbracelet/bubbletea"
 8 | 	"github.com/charmbracelet/lipgloss"
 9 | 
10 | 	"github.com/Zebbeni/ansizalizer/controls/browser"
11 | 	"github.com/Zebbeni/ansizalizer/event"
12 | )
13 | 
14 | type State int
15 | 
16 | const (
17 | 	Input State = iota
18 | 	Browser
19 | )
20 | 
21 | type Model struct {
22 | 	focus State
23 | 
24 | 	Browser browser.Model
25 | 
26 | 	selectedDir string
27 | 
28 | 	ShouldClose   bool
29 | 	ShouldUnfocus bool
30 | 
31 | 	IsActive bool
32 | 
33 | 	width int
34 | }
35 | 
36 | func New(w int) Model {
37 | 	filepath, _ := os.Getwd()
38 | 
39 | 	return Model{
40 | 		focus: Input,
41 | 
42 | 		Browser: browser.New(nil, w-2),
43 | 
44 | 		selectedDir: filepath,
45 | 
46 | 		width:       w,
47 | 		ShouldClose: false,
48 | 	}
49 | }
50 | 
51 | func (m Model) Init() tea.Cmd {
52 | 	return nil
53 | }
54 | 
55 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
56 | 	var cmd tea.Cmd
57 | 	switch m.focus {
58 | 	case Browser:
59 | 		return m.handleDstBrowserUpdate(msg)
60 | 	}
61 | 
62 | 	switch msg := msg.(type) {
63 | 	case tea.KeyMsg:
64 | 		switch {
65 | 		case key.Matches(msg, event.KeyMap.Esc):
66 | 			return m.handleEsc()
67 | 		case key.Matches(msg, event.KeyMap.Nav):
68 | 			return m.handleNav(msg)
69 | 		case key.Matches(msg, event.KeyMap.Enter):
70 | 			return m.handleEnter()
71 | 		}
72 | 	}
73 | 	return m, cmd
74 | }
75 | 
76 | func (m Model) View() string {
77 | 	content := make([]string, 0, 5)
78 | 
79 | 	selected := lipgloss.NewStyle().PaddingTop(1).Render(m.drawSelected())
80 | 	content = append(content, selected)
81 | 
82 | 	if m.focus == Browser {
83 | 		content = append(content, m.Browser.View())
84 | 	}
85 | 
86 | 	return lipgloss.JoinVertical(lipgloss.Left, content...)
87 | }
88 | 
89 | func (m Model) GetSelected() string {
90 | 	return m.selectedDir
91 | }
92 | 
```

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

```go
 1 | package settings
 2 | 
 3 | import (
 4 | 	tea "github.com/charmbracelet/bubbletea"
 5 | 	"github.com/charmbracelet/lipgloss"
 6 | 
 7 | 	"github.com/Zebbeni/ansizalizer/controls/settings/advanced"
 8 | 	"github.com/Zebbeni/ansizalizer/controls/settings/characters"
 9 | 	"github.com/Zebbeni/ansizalizer/controls/settings/colors"
10 | 	"github.com/Zebbeni/ansizalizer/controls/settings/size"
11 | )
12 | 
13 | type Model struct {
14 | 	active State
15 | 	focus  State
16 | 
17 | 	Colors     colors.Model
18 | 	Characters characters.Model
19 | 	Size       size.Model
20 | 	Advanced   advanced.Model
21 | 
22 | 	ShouldUnfocus bool
23 | 	ShouldClose   bool
24 | 
25 | 	width int
26 | }
27 | 
28 | func New(w int) Model {
29 | 	return Model{
30 | 		active: None,
31 | 		focus:  Colors,
32 | 
33 | 		Colors:     colors.New(w),
34 | 		Characters: characters.New(w - 2),
35 | 		Size:       size.New(),
36 | 		Advanced:   advanced.New(w - 2),
37 | 
38 | 		ShouldUnfocus: false,
39 | 		ShouldClose:   false,
40 | 
41 | 		width: w,
42 | 	}
43 | }
44 | 
45 | func (m Model) Init() tea.Cmd {
46 | 	return nil
47 | }
48 | 
49 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
50 | 	switch m.active {
51 | 	case Colors:
52 | 		return m.handleColorsUpdate(msg)
53 | 	case Characters:
54 | 		return m.handleCharactersUpdate(msg)
55 | 	case Size:
56 | 		return m.handleSizeUpdate(msg)
57 | 	case Advanced:
58 | 		return m.handleAdvancedUpdate(msg)
59 | 	}
60 | 
61 | 	keyMsg, ok := msg.(tea.KeyMsg)
62 | 	if !ok {
63 | 		return m, nil
64 | 	}
65 | 
66 | 	return m.handleKeyMsg(keyMsg)
67 | }
68 | 
69 | func (m Model) View() string {
70 | 	colorCtrls := m.Colors.View()
71 | 	charCtrls := m.Characters.View()
72 | 	sizeCtrls := m.Size.View()
73 | 	sampCtrls := m.Advanced.View()
74 | 
75 | 	col := m.renderWithBorder(colorCtrls, Colors)
76 | 	char := m.renderWithBorder(charCtrls, Characters)
77 | 	siz := m.renderWithBorder(sizeCtrls, Size)
78 | 	sam := m.renderWithBorder(sampCtrls, Advanced)
79 | 
80 | 	return lipgloss.JoinVertical(lipgloss.Top, col, char, siz, sam)
81 | }
82 | 
```

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

```go
 1 | package advanced
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/bubbles/key"
 5 | 	tea "github.com/charmbracelet/bubbletea"
 6 | 	"github.com/makeworld-the-better-one/dither/v2"
 7 | 	"github.com/nfnt/resize"
 8 | 
 9 | 	"github.com/Zebbeni/ansizalizer/controls/settings/advanced/dithering"
10 | 	"github.com/Zebbeni/ansizalizer/controls/settings/advanced/sampling"
11 | 	"github.com/Zebbeni/ansizalizer/event"
12 | )
13 | 
14 | type State int
15 | 
16 | const (
17 | 	Menu State = iota
18 | 	Sampling
19 | 	Dithering
20 | 	SamplingControls
21 | 	DitheringControls
22 | )
23 | 
24 | type Model struct {
25 | 	focus       State
26 | 	active      State
27 | 	activeTab   State
28 | 	sampling    sampling.Model
29 | 	dithering   dithering.Model
30 | 	ShouldClose bool
31 | 	IsActive    bool
32 | 	width       int
33 | }
34 | 
35 | func New(w int) Model {
36 | 	return Model{
37 | 		focus:       Sampling,
38 | 		active:      Menu,
39 | 		activeTab:   Sampling,
40 | 		sampling:    sampling.New(w - 2),
41 | 		dithering:   dithering.New(w - 2),
42 | 		ShouldClose: false,
43 | 		IsActive:    false,
44 | 		width:       w,
45 | 	}
46 | }
47 | 
48 | func (m Model) Init() tea.Cmd {
49 | 	return nil
50 | }
51 | 
52 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
53 | 	switch m.active {
54 | 	case SamplingControls:
55 | 		return m.handleSamplingUpdate(msg)
56 | 	case DitheringControls:
57 | 		return m.handleDitheringUpdate(msg)
58 | 	}
59 | 
60 | 	switch msg := msg.(type) {
61 | 	case tea.KeyMsg:
62 | 		switch {
63 | 		case key.Matches(msg, event.KeyMap.Enter):
64 | 			return m.handleEnter()
65 | 		case key.Matches(msg, event.KeyMap.Nav):
66 | 			return m.handleNav(msg)
67 | 		case key.Matches(msg, event.KeyMap.Esc):
68 | 			return m.handleEsc()
69 | 		}
70 | 	}
71 | 	return m, nil
72 | }
73 | 
74 | func (m Model) View() string {
75 | 	return m.drawTabs()
76 | }
77 | 
78 | func (m Model) SamplingFunction() resize.InterpolationFunction {
79 | 	return m.sampling.Function
80 | }
81 | 
82 | func (m Model) Dithering() (bool, bool, dither.ErrorDiffusionMatrix) {
83 | 	return m.dithering.Settings()
84 | }
85 | 
```

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

```go
 1 | package dithering
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/lipgloss"
 5 | 
 6 | 	"github.com/Zebbeni/ansizalizer/style"
 7 | )
 8 | 
 9 | func (m Model) drawDitheringOptions() string {
10 | 	prompt := style.DimmedTitle.Render("Use Dithering:")
11 | 	prompt = lipgloss.NewStyle().Width(15).Render(prompt)
12 | 
13 | 	nodeStyle := style.NormalButtonNode
14 | 	if m.IsActive && m.focus == DitherOn {
15 | 		nodeStyle = style.FocusButtonNode
16 | 	} else if m.doDithering {
17 | 		nodeStyle = style.ActiveButtonNode
18 | 	}
19 | 	onNode := lipgloss.NewStyle().Width(4).Render(nodeStyle.Copy().Render("On"))
20 | 
21 | 	nodeStyle = style.NormalButtonNode
22 | 	if m.IsActive && m.focus == DitherOff {
23 | 		nodeStyle = style.FocusButtonNode
24 | 	} else if !m.doDithering {
25 | 		nodeStyle = style.ActiveButtonNode
26 | 	}
27 | 	offNode := nodeStyle.Copy().Render("Off")
28 | 
29 | 	return lipgloss.JoinHorizontal(lipgloss.Left, prompt, onNode, offNode)
30 | }
31 | 
32 | func (m Model) drawSerpentineOptions() string {
33 | 	prompt := style.DimmedTitle.Render("Do Serpentine:")
34 | 	prompt = lipgloss.NewStyle().Width(15).Render(prompt)
35 | 
36 | 	nodeStyle := style.NormalButtonNode
37 | 	if m.IsActive && m.focus == SerpentineOn {
38 | 		nodeStyle = style.FocusButtonNode
39 | 	} else if m.doSerpentine {
40 | 		nodeStyle = style.ActiveButtonNode
41 | 	}
42 | 	onNode := lipgloss.NewStyle().Width(4).Render(nodeStyle.Copy().Render("On"))
43 | 
44 | 	nodeStyle = style.NormalButtonNode
45 | 	if m.IsActive && m.focus == SerpentineOff {
46 | 		nodeStyle = style.FocusButtonNode
47 | 	} else if !m.doSerpentine {
48 | 		nodeStyle = style.ActiveButtonNode
49 | 	}
50 | 	offNode := nodeStyle.Copy().Render("Off")
51 | 
52 | 	return lipgloss.JoinHorizontal(lipgloss.Left, prompt, onNode, offNode)
53 | }
54 | 
55 | func (m Model) drawMatrix() string {
56 | 	prompt := style.DimmedTitle.Copy().PaddingTop(1).Render("Select Matrix")
57 | 	return lipgloss.JoinVertical(lipgloss.Left, prompt, m.list.View())
58 | }
59 | 
```

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

```go
 1 | package controls
 2 | 
 3 | import (
 4 | 	"os"
 5 | 
 6 | 	"github.com/charmbracelet/bubbles/key"
 7 | 	tea "github.com/charmbracelet/bubbletea"
 8 | 
 9 | 	"github.com/Zebbeni/ansizalizer/event"
10 | )
11 | 
12 | type Direction int
13 | 
14 | const (
15 | 	Left Direction = iota
16 | 	Right
17 | 	Up
18 | 	Down
19 | )
20 | 
21 | var navMap = map[Direction]map[State]State{
22 | 	Right: {Browse: Settings, Settings: Export},
23 | 	Left:  {Export: Settings, Settings: Browse},
24 | }
25 | 
26 | func (m Model) handleOpenUpdate(msg tea.Msg) (Model, tea.Cmd) {
27 | 	var cmd tea.Cmd
28 | 	m.FileBrowser, cmd = m.FileBrowser.Update(msg)
29 | 
30 | 	if m.FileBrowser.ShouldClose {
31 | 		m.FileBrowser.ShouldClose = false
32 | 		m.active = Menu
33 | 	}
34 | 	return m, cmd
35 | }
36 | 
37 | func (m Model) handleSettingsUpdate(msg tea.Msg) (Model, tea.Cmd) {
38 | 	var cmd tea.Cmd
39 | 	m.Settings, cmd = m.Settings.Update(msg)
40 | 
41 | 	if m.Settings.ShouldClose {
42 | 		m.Settings.ShouldClose = false
43 | 		m.active = Menu
44 | 	}
45 | 
46 | 	return m, cmd
47 | }
48 | 
49 | func (m Model) handleExportUpdate(msg tea.Msg) (Model, tea.Cmd) {
50 | 	var cmd tea.Cmd
51 | 	m.Export, cmd = m.Export.Update(msg)
52 | 
53 | 	if m.Export.ShouldClose {
54 | 		m.Export.ShouldClose = false
55 | 		m.active = Menu
56 | 	}
57 | 
58 | 	return m, cmd
59 | }
60 | 
61 | func (m Model) handleMenuUpdate(msg tea.Msg) (Model, tea.Cmd) {
62 | 	m.active = Menu
63 | 	switch msg := msg.(type) {
64 | 	case tea.KeyMsg:
65 | 		switch {
66 | 		case key.Matches(msg, event.KeyMap.Enter):
67 | 			m.active = m.focus
68 | 
69 | 		case key.Matches(msg, event.KeyMap.Nav):
70 | 			switch {
71 | 			case key.Matches(msg, event.KeyMap.Right):
72 | 				if next, hasNext := navMap[Right][m.focus]; hasNext {
73 | 					m.focus = next
74 | 				}
75 | 			case key.Matches(msg, event.KeyMap.Left):
76 | 				if next, hasNext := navMap[Left][m.focus]; hasNext {
77 | 					m.focus = next
78 | 				}
79 | 			}
80 | 
81 | 		case key.Matches(msg, event.KeyMap.Esc):
82 | 			// Quit program if top-level menu is active and escape pressed
83 | 			tea.Quit()
84 | 			os.Exit(0)
85 | 		}
86 | 	}
87 | 	return m, nil
88 | }
89 | 
```

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

```go
 1 | package controls
 2 | 
 3 | import (
 4 | 	tea "github.com/charmbracelet/bubbletea"
 5 | 	"github.com/charmbracelet/lipgloss"
 6 | 
 7 | 	"github.com/Zebbeni/ansizalizer/controls/browser"
 8 | 	"github.com/Zebbeni/ansizalizer/controls/export"
 9 | 	"github.com/Zebbeni/ansizalizer/controls/settings"
10 | 	"github.com/Zebbeni/ansizalizer/global"
11 | )
12 | 
13 | type State int
14 | 
15 | const (
16 | 	Menu State = iota
17 | 	Browse
18 | 	Settings
19 | 	Export
20 | 
21 | 	numButtons = 3
22 | )
23 | 
24 | var (
25 | 	stateOrder = []State{Browse, Settings, Export}
26 | 	stateNames = map[State]string{
27 | 		Browse:   "Browse",
28 | 		Settings: "Settings",
29 | 		Export:   "Export",
30 | 	}
31 | )
32 | 
33 | type Model struct {
34 | 	active State
35 | 	focus  State
36 | 
37 | 	FileBrowser browser.Model
38 | 	Settings    settings.Model
39 | 	Export      export.Model
40 | 
41 | 	width int
42 | }
43 | 
44 | func New(w int) Model {
45 | 	return Model{
46 | 		active: Menu,
47 | 		focus:  Browse,
48 | 
49 | 		FileBrowser: browser.New(global.ImgExtensions, w),
50 | 		Settings:    settings.New(w),
51 | 		Export:      export.New(w),
52 | 
53 | 		width: w,
54 | 	}
55 | }
56 | 
57 | func (m Model) Init() tea.Cmd {
58 | 	return nil
59 | }
60 | 
61 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
62 | 	switch m.active {
63 | 	case Browse:
64 | 		return m.handleOpenUpdate(msg)
65 | 	case Settings:
66 | 		return m.handleSettingsUpdate(msg)
67 | 	case Export:
68 | 		return m.handleExportUpdate(msg)
69 | 	}
70 | 	return m.handleMenuUpdate(msg)
71 | }
72 | 
73 | // View displays a row of 3 buttons above 1 of 3 control panels:
74 | // Browse | Settings | Export
75 | func (m Model) View() string {
76 | 	title := m.drawTitle()
77 | 
78 | 	// draw the top three buttons
79 | 	buttons := m.drawButtons()
80 | 	var controls string
81 | 
82 | 	switch m.active {
83 | 	case Browse:
84 | 		browserTitle := m.drawBrowserTitle()
85 | 		controls = lipgloss.JoinVertical(lipgloss.Left, browserTitle, m.FileBrowser.View())
86 | 	case Settings:
87 | 		controls = m.Settings.View()
88 | 	case Export:
89 | 		controls = m.Export.View()
90 | 	}
91 | 
92 | 	return lipgloss.JoinVertical(lipgloss.Top, title, buttons, controls)
93 | }
94 | 
```

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

```go
 1 | package sampling
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/bubbles/list"
 5 | 	"github.com/charmbracelet/lipgloss"
 6 | 	"github.com/nfnt/resize"
 7 | 
 8 | 	"github.com/Zebbeni/ansizalizer/style"
 9 | )
10 | 
11 | type item struct {
12 | 	name     string
13 | 	Function resize.InterpolationFunction
14 | }
15 | 
16 | func (i item) FilterValue() string {
17 | 	return i.name
18 | }
19 | 
20 | func (i item) Title() string {
21 | 	return i.name
22 | }
23 | 
24 | func (i item) Description() string {
25 | 	return ""
26 | }
27 | 
28 | func menuItems() []list.Item {
29 | 	items := make([]list.Item, len(nameMap))
30 | 	for i, f := range Functions {
31 | 		items[i] = item{name: nameMap[f], Function: f}
32 | 	}
33 | 	return items
34 | }
35 | 
36 | func newMenu(items []list.Item, width, height int) list.Model {
37 | 	l := list.New(items, NewDelegate(false), width, height)
38 | 	l.SetShowHelp(false)
39 | 	l.SetFilteringEnabled(false)
40 | 	l.SetShowTitle(false)
41 | 	l.SetShowPagination(false)
42 | 	l.SetShowStatusBar(false)
43 | 
44 | 	l.KeyMap.ForceQuit.Unbind()
45 | 	l.KeyMap.Quit.Unbind()
46 | 
47 | 	return l
48 | }
49 | 
50 | func NewDelegate(isActive bool) list.DefaultDelegate {
51 | 	delegate := list.NewDefaultDelegate()
52 | 	delegate.SetSpacing(0)
53 | 	delegate.ShowDescription = false
54 | 	if isActive {
55 | 		delegate.Styles = ItemStylesActive()
56 | 	} else {
57 | 		delegate.Styles = ItemStylesInactive()
58 | 	}
59 | 	return delegate
60 | }
61 | 
62 | func ItemStylesActive() (s list.DefaultItemStyles) {
63 | 	s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2)
64 | 	s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1).
65 | 		Border(lipgloss.NormalBorder(), false, false, false, true).
66 | 		BorderForeground(style.SelectedColor1)
67 | 	s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0)
68 | 	return s
69 | }
70 | 
71 | func ItemStylesInactive() (s list.DefaultItemStyles) {
72 | 	s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2)
73 | 	s.SelectedTitle = style.NormalTitle.Copy().Padding(0, 1, 0, 2)
74 | 	s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0)
75 | 	return s
76 | }
77 | 
```

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

```go
 1 | package app
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 
 6 | 	"github.com/charmbracelet/bubbles/help"
 7 | 	"github.com/charmbracelet/bubbles/viewport"
 8 | 	"github.com/charmbracelet/lipgloss"
 9 | 
10 | 	"github.com/Zebbeni/ansizalizer/event"
11 | 	"github.com/Zebbeni/ansizalizer/style"
12 | )
13 | 
14 | const (
15 | 	displayHeight = 3
16 | 	helpHeight    = 1
17 | 
18 | 	controlsWidth = 30
19 | )
20 | 
21 | func (m Model) renderControls() string {
22 | 	viewport := viewport.New(controlsWidth, m.leftPanelHeight())
23 | 
24 | 	leftContent := m.controls.View()
25 | 
26 | 	viewport.SetContent(lipgloss.NewStyle().
27 | 		Width(controlsWidth).
28 | 		Height(m.leftPanelHeight()).
29 | 		Render(leftContent))
30 | 	return viewport.View()
31 | }
32 | 
33 | func (m Model) renderViewer() string {
34 | 	imgString := m.viewer.View()
35 | 	imgWidth, imgHeight := lipgloss.Size(imgString)
36 | 
37 | 	imgViewer := imgString
38 | 
39 | 	// only render box label border around content if big enough.
40 | 	if imgHeight > 1 && imgWidth > 4 {
41 | 		boxLabelRenderer := style.BoxWithLabel{
42 | 			BoxStyle:   lipgloss.NewStyle().BorderForeground(style.ExtraDimColor).Border(lipgloss.RoundedBorder()),
43 | 			LabelStyle: lipgloss.NewStyle().Foreground(style.ExtraDimColor).AlignHorizontal(lipgloss.Center).AlignVertical(lipgloss.Bottom),
44 | 		}
45 | 		imgViewer = boxLabelRenderer.Render(fmt.Sprintf("%dx%d", imgWidth, imgHeight), imgString, imgWidth)
46 | 	}
47 | 
48 | 	renderViewport := viewport.New(m.rPanelWidth()-2, m.rPanelHeight()-displayHeight-2)
49 | 
50 | 	vpRightStyle := lipgloss.NewStyle().Align(lipgloss.Center).AlignVertical(lipgloss.Center)
51 | 	rightContent := vpRightStyle.Copy().Width(m.rPanelWidth() - 2).Height(m.rPanelHeight() - 4).Render(imgViewer)
52 | 	renderViewport.SetContent(rightContent)
53 | 
54 | 	content := renderViewport.View()
55 | 
56 | 	return style.NormalButton.Copy().BorderForeground(style.DimmedColor1).Render(content)
57 | }
58 | 
59 | func (m Model) renderHelp() string {
60 | 	helpBar := help.New()
61 | 	helpContent := helpBar.View(event.KeyMap)
62 | 	return lipgloss.NewStyle().PaddingLeft(1).Render(helpContent)
63 | }
64 | 
```

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

```go
 1 | package style
 2 | 
 3 | import "github.com/charmbracelet/lipgloss"
 4 | 
 5 | var (
 6 | 	NormalColor1   = lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#aaaaaa"}
 7 | 	NormalColor2   = lipgloss.AdaptiveColor{Light: "#3a3a3a", Dark: "#888888"}
 8 | 	SelectedColor1 = lipgloss.AdaptiveColor{Light: "#444444", Dark: "#ffffff"}
 9 | 	SelectedColor2 = lipgloss.AdaptiveColor{Light: "#666666", Dark: "#dddddd"}
10 | 	ExtraDimColor  = lipgloss.AdaptiveColor{Light: "#bbbbbb", Dark: "#444444"}
11 | 	DimmedColor1   = lipgloss.AdaptiveColor{Light: "#999999", Dark: "#777777"}
12 | 	DimmedColor2   = lipgloss.AdaptiveColor{Light: "#aaaaaa", Dark: "#666666"}
13 | 
14 | 	NormalTitle     = lipgloss.NewStyle().Foreground(NormalColor1)
15 | 	NormalParagraph = lipgloss.NewStyle().Foreground(NormalColor2)
16 | 
17 | 	SelectedTitle     = lipgloss.NewStyle().Foreground(SelectedColor1)
18 | 	SelectedParagraph = lipgloss.NewStyle().Foreground(SelectedColor2)
19 | 
20 | 	DimmedTitle     = lipgloss.NewStyle().Foreground(DimmedColor1)
21 | 	ExtraDimTitle   = lipgloss.NewStyle().Foreground(ExtraDimColor)
22 | 	DimmedParagraph = lipgloss.NewStyle().Foreground(DimmedColor2)
23 | 
24 | 	ActiveButton = lipgloss.NewStyle().
25 | 			BorderStyle(lipgloss.RoundedBorder()).
26 | 			BorderForeground(NormalColor1).
27 | 			Foreground(NormalColor1)
28 | 	FocusButton = lipgloss.NewStyle().
29 | 			BorderStyle(lipgloss.RoundedBorder()).
30 | 			BorderForeground(SelectedColor1).
31 | 			Foreground(SelectedColor1)
32 | 	NormalButton = lipgloss.NewStyle().
33 | 			BorderStyle(lipgloss.RoundedBorder()).
34 | 			BorderForeground(DimmedColor1).
35 | 			Foreground(DimmedColor1)
36 | 
37 | 	ActiveButtonNode = lipgloss.NewStyle().
38 | 				PaddingLeft(1).
39 | 				Foreground(NormalColor1)
40 | 	FocusButtonNode = lipgloss.NewStyle().
41 | 			Border(lipgloss.RoundedBorder(), false, false, false, true).
42 | 			BorderForeground(SelectedColor1).
43 | 			Foreground(SelectedColor1).
44 | 			Padding(0)
45 | 	NormalButtonNode = lipgloss.NewStyle().
46 | 				PaddingLeft(1).
47 | 				Foreground(DimmedColor1)
48 | )
49 | 
```

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

```go
 1 | package palette
 2 | 
 3 | import (
 4 | 	"image/color"
 5 | 	"math"
 6 | 
 7 | 	tea "github.com/charmbracelet/bubbletea"
 8 | 	"github.com/charmbracelet/lipgloss"
 9 | 	"github.com/lucasb-eyer/go-colorful"
10 | 
11 | 	"github.com/Zebbeni/ansizalizer/style"
12 | )
13 | 
14 | type Model struct {
15 | 	name   string
16 | 	colors color.Palette
17 | 	width  int
18 | 	height int
19 | }
20 | 
21 | func New(name string, colors color.Palette, w, h int) Model {
22 | 	return Model{
23 | 		name:   name,
24 | 		colors: colors,
25 | 		width:  w,
26 | 		height: h,
27 | 	}
28 | }
29 | 
30 | func (m Model) Init() tea.Cmd {
31 | 	return nil
32 | }
33 | 
34 | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
35 | 	return m, nil
36 | }
37 | 
38 | func (m Model) View() string {
39 | 	title := style.SelectedTitle.Render(m.name)
40 | 	description := m.Description()
41 | 
42 | 	return lipgloss.JoinVertical(lipgloss.Top, title, description)
43 | }
44 | 
45 | func (m Model) FilterValue() string {
46 | 	return m.name
47 | }
48 | 
49 | func (m Model) Title() string {
50 | 	return m.name
51 | }
52 | 
53 | func (m Model) Description() string {
54 | 	runes := make([]string, len(m.colors)/2+1)
55 | 	rows := make([]string, 0, m.height)
56 | 	for idx := 0; idx < len(m.colors); idx += 2 {
57 | 		var fg, bg colorful.Color
58 | 		var lipFg, lipBg lipgloss.Color
59 | 
60 | 		fg, _ = colorful.MakeColor(m.colors[idx])
61 | 		lipFg = lipgloss.Color(fg.Hex())
62 | 		blockStyle := lipgloss.NewStyle().Foreground(lipFg)
63 | 
64 | 		if idx+1 < len(m.colors) {
65 | 			bg, _ = colorful.MakeColor(m.colors[idx+1])
66 | 			lipBg = lipgloss.Color(bg.Hex())
67 | 			blockStyle = blockStyle.Copy().Background(lipBg)
68 | 		}
69 | 		runes[idx/2] = blockStyle.Render(string('▀'))
70 | 	}
71 | 	for i := 0; i < m.height; i++ {
72 | 		start := m.width * i
73 | 		if start >= len(runes) {
74 | 			break
75 | 		}
76 | 		stop := int(math.Min(float64(m.width*(i+1)), float64(len(runes))))
77 | 		rows = append(rows, "")
78 | 		rows[i] = lipgloss.JoinHorizontal(lipgloss.Left, runes[start:stop]...)
79 | 	}
80 | 
81 | 	return lipgloss.JoinVertical(lipgloss.Left, rows...)
82 | }
83 | 
84 | func (m Model) Name() string {
85 | 	return m.name
86 | }
87 | 
88 | func (m Model) Colors() color.Palette {
89 | 	colorsCopy := make([]color.Color, len(m.colors))
90 | 	copy(colorsCopy, m.colors)
91 | 	return colorsCopy
92 | }
93 | 
```

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

```go
 1 | package colors
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/bubbles/key"
 5 | 	tea "github.com/charmbracelet/bubbletea"
 6 | 	"github.com/charmbracelet/lipgloss"
 7 | 
 8 | 	"github.com/Zebbeni/ansizalizer/controls/settings/palettes"
 9 | 	"github.com/Zebbeni/ansizalizer/event"
10 | 	"github.com/Zebbeni/ansizalizer/palette"
11 | )
12 | 
13 | type State int
14 | 
15 | const (
16 | 	UsePalette State = iota
17 | 	UseTrueColor
18 | 	Palette
19 | )
20 | 
21 | type Model struct {
22 | 	focus           State
23 | 	mode            State
24 | 	width           int
25 | 	PaletteControls palettes.Model
26 | 
27 | 	IsActive    bool
28 | 	ShouldClose bool
29 | }
30 | 
31 | func New(w int) Model {
32 | 	return Model{
33 | 		focus:           UseTrueColor,
34 | 		mode:            UseTrueColor,
35 | 		width:           w,
36 | 		PaletteControls: palettes.New(w),
37 | 		IsActive:        false,
38 | 		ShouldClose:     false,
39 | 	}
40 | }
41 | 
42 | func (m Model) Init() tea.Cmd {
43 | 	return nil
44 | }
45 | 
46 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
47 | 	switch m.focus {
48 | 	case Palette:
49 | 		return m.handlePaletteUpdate(msg)
50 | 	}
51 | 
52 | 	switch msg := msg.(type) {
53 | 	case tea.KeyMsg:
54 | 		switch {
55 | 		case key.Matches(msg, event.KeyMap.Enter):
56 | 			return m.handleEnter()
57 | 		case key.Matches(msg, event.KeyMap.Nav):
58 | 			return m.handleNav(msg)
59 | 		case key.Matches(msg, event.KeyMap.Esc):
60 | 			return m.handleEsc()
61 | 		}
62 | 	}
63 | 	return m, nil
64 | }
65 | 
66 | func (m Model) View() string {
67 | 	paletteToggles := m.drawPaletteToggles()
68 | 	if m.mode == UseTrueColor {
69 | 		return paletteToggles
70 | 	}
71 | 
72 | 	paletteTabs := m.PaletteControls.View()
73 | 	return lipgloss.JoinVertical(lipgloss.Left, paletteToggles, paletteTabs)
74 | }
75 | 
76 | // GetSelected returns isPaletted, isAdaptive, and the palette (if applicable)
77 | func (m Model) GetSelected() (bool, bool, palette.Model) {
78 | 	colorPalette := m.PaletteControls.GetCurrentPalette()
79 | 
80 | 	if m.mode == UseTrueColor {
81 | 		return true, false, colorPalette
82 | 	}
83 | 
84 | 	return false, m.PaletteControls.IsAdaptive(), colorPalette
85 | }
86 | 
87 | func (m Model) GetCurrentPalette() palette.Model {
88 | 	return m.PaletteControls.GetCurrentPalette()
89 | }
90 | 
91 | func (m Model) IsLimited() bool {
92 | 	return m.mode == UsePalette
93 | }
94 | 
```

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

```go
  1 | package colors
  2 | 
  3 | import (
  4 | 	"github.com/charmbracelet/bubbles/key"
  5 | 	tea "github.com/charmbracelet/bubbletea"
  6 | 
  7 | 	"github.com/Zebbeni/ansizalizer/event"
  8 | )
  9 | 
 10 | type Direction int
 11 | 
 12 | const (
 13 | 	Left Direction = iota
 14 | 	Right
 15 | 	Up
 16 | 	Down
 17 | )
 18 | 
 19 | var navMap = map[Direction]map[State]State{
 20 | 	Right: {
 21 | 		UseTrueColor: UsePalette,
 22 | 	},
 23 | 	Left: {
 24 | 		UsePalette: UseTrueColor,
 25 | 	},
 26 | 	Up: {
 27 | 		Palette: UsePalette,
 28 | 	},
 29 | 	Down: {
 30 | 		UseTrueColor: Palette,
 31 | 		UsePalette:   Palette,
 32 | 	},
 33 | }
 34 | 
 35 | func (m Model) handlePaletteUpdate(msg tea.Msg) (Model, tea.Cmd) {
 36 | 	var cmd tea.Cmd
 37 | 	m.PaletteControls, cmd = m.PaletteControls.Update(msg)
 38 | 
 39 | 	if m.PaletteControls.ShouldClose {
 40 | 		m.PaletteControls.IsActive = false
 41 | 		m.PaletteControls.ShouldClose = false
 42 | 		m.focus = UsePalette
 43 | 	}
 44 | 	return m, cmd
 45 | }
 46 | 
 47 | func (m Model) handleEnter() (Model, tea.Cmd) {
 48 | 	switch m.focus {
 49 | 	case UsePalette:
 50 | 		m.mode = UsePalette
 51 | 	case UseTrueColor:
 52 | 		m.mode = UseTrueColor
 53 | 	}
 54 | 	return m, nil
 55 | }
 56 | 
 57 | func (m Model) handleEsc() (Model, tea.Cmd) {
 58 | 	return m, nil
 59 | }
 60 | 
 61 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
 62 | 	var cmd tea.Cmd
 63 | 	switch {
 64 | 	case key.Matches(msg, event.KeyMap.Right):
 65 | 		if next, hasNext := navMap[Right][m.focus]; hasNext {
 66 | 			return m.setFocus(next)
 67 | 		}
 68 | 	case key.Matches(msg, event.KeyMap.Left):
 69 | 		if next, hasNext := navMap[Left][m.focus]; hasNext {
 70 | 			return m.setFocus(next)
 71 | 		}
 72 | 	case key.Matches(msg, event.KeyMap.Up):
 73 | 		if next, hasNext := navMap[Up][m.focus]; hasNext {
 74 | 			return m.setFocus(next)
 75 | 		} else {
 76 | 			m.IsActive = false
 77 | 			m.ShouldClose = true
 78 | 		}
 79 | 	case key.Matches(msg, event.KeyMap.Down):
 80 | 		if next, hasNext := navMap[Down][m.focus]; hasNext {
 81 | 			return m.setFocus(next)
 82 | 		} else {
 83 | 			m.IsActive = false
 84 | 			m.ShouldClose = true
 85 | 		}
 86 | 	}
 87 | 	return m, cmd
 88 | }
 89 | 
 90 | func (m Model) setFocus(focus State) (Model, tea.Cmd) {
 91 | 	if m.mode == UseTrueColor && focus == Palette {
 92 | 		return m, nil
 93 | 	}
 94 | 
 95 | 	m.focus = focus
 96 | 	switch m.focus {
 97 | 	case Palette:
 98 | 		m.PaletteControls.IsActive = true
 99 | 	}
100 | 
101 | 	return m, nil
102 | }
103 | 
```

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

```go
  1 | package source
  2 | 
  3 | import (
  4 | 	"github.com/charmbracelet/bubbles/key"
  5 | 	tea "github.com/charmbracelet/bubbletea"
  6 | 	"github.com/charmbracelet/lipgloss"
  7 | 
  8 | 	"github.com/Zebbeni/ansizalizer/controls/browser"
  9 | 	"github.com/Zebbeni/ansizalizer/event"
 10 | )
 11 | 
 12 | type State int
 13 | 
 14 | const (
 15 | 	ExpFile State = iota
 16 | 	ExpDirectory
 17 | 	Input
 18 | 	Browser
 19 | 	SubDirsYes
 20 | 	SubDirsNo
 21 | )
 22 | 
 23 | type Model struct {
 24 | 	focus State
 25 | 
 26 | 	doExportDirectory     bool
 27 | 	includeSubdirectories bool
 28 | 
 29 | 	Browser      browser.Model
 30 | 	selectedDir  string
 31 | 	selectedFile string
 32 | 
 33 | 	ShouldClose   bool
 34 | 	ShouldUnfocus bool
 35 | 
 36 | 	IsActive bool
 37 | 
 38 | 	width int
 39 | }
 40 | 
 41 | func New(w int) Model {
 42 | 	browserModel := browser.New(nil, w-2)
 43 | 
 44 | 	return Model{
 45 | 		focus: ExpDirectory,
 46 | 
 47 | 		Browser: browserModel,
 48 | 
 49 | 		doExportDirectory:     true,
 50 | 		includeSubdirectories: false,
 51 | 
 52 | 		selectedDir:  "",
 53 | 		selectedFile: "",
 54 | 
 55 | 		width:       w,
 56 | 		ShouldClose: false,
 57 | 	}
 58 | }
 59 | 
 60 | func (m Model) Init() tea.Cmd {
 61 | 	return nil
 62 | }
 63 | 
 64 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
 65 | 	var cmd tea.Cmd
 66 | 	switch m.focus {
 67 | 	case Browser:
 68 | 		return m.handleSrcBrowserUpdate(msg)
 69 | 	}
 70 | 
 71 | 	switch msg := msg.(type) {
 72 | 	case tea.KeyMsg:
 73 | 		switch {
 74 | 		case key.Matches(msg, event.KeyMap.Esc):
 75 | 			return m.handleEsc()
 76 | 		case key.Matches(msg, event.KeyMap.Nav):
 77 | 			return m.handleNav(msg)
 78 | 		case key.Matches(msg, event.KeyMap.Enter):
 79 | 			return m.handleEnter()
 80 | 		}
 81 | 	}
 82 | 	return m, cmd
 83 | }
 84 | 
 85 | func (m Model) View() string {
 86 | 	content := make([]string, 0, 5)
 87 | 	content = append(content, m.drawExportTypeOptions())
 88 | 
 89 | 	selected := lipgloss.NewStyle().PaddingTop(1).Render(m.drawSelected())
 90 | 	content = append(content, selected)
 91 | 
 92 | 	if m.focus == Browser {
 93 | 		content = append(content, m.Browser.View())
 94 | 	}
 95 | 
 96 | 	if m.doExportDirectory {
 97 | 		content = append(content, m.drawSubDirOptions())
 98 | 	}
 99 | 
100 | 	return lipgloss.JoinVertical(lipgloss.Left, content...)
101 | }
102 | 
103 | func (m Model) GetSelected() (path string, isDir, useSubDirs bool) {
104 | 	if m.doExportDirectory {
105 | 		isDir = true
106 | 		path = m.selectedDir
107 | 		useSubDirs = m.includeSubdirectories
108 | 	} else {
109 | 		path = m.selectedFile
110 | 		isDir = false
111 | 		useSubDirs = false
112 | 	}
113 | 	return
114 | }
115 | 
```

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

```go
  1 | package event
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"image/color"
  6 | 
  7 | 	tea "github.com/charmbracelet/bubbletea"
  8 | )
  9 | 
 10 | type StartRenderToViewMsg bool
 11 | 
 12 | func StartRenderToViewCmd() tea.Msg {
 13 | 	return StartRenderToViewMsg(true)
 14 | }
 15 | 
 16 | type FinishRenderToViewMsg struct {
 17 | 	FilePath     string
 18 | 	ImgString    string
 19 | 	ColorsString string
 20 | }
 21 | 
 22 | type StartRenderToExportMsg bool
 23 | 
 24 | func StartRenderToExportCmd() tea.Msg {
 25 | 	return StartRenderToExportMsg(true)
 26 | }
 27 | 
 28 | type FinishRenderToExportMsg struct {
 29 | 	FilePath     string
 30 | 	ImgString    string
 31 | 	ColorsString string
 32 | }
 33 | 
 34 | func BuildFinishRenderToExportCmd(msg FinishRenderToExportMsg) tea.Cmd {
 35 | 	return func() tea.Msg { return msg }
 36 | }
 37 | 
 38 | type StartAdaptingMsg bool
 39 | 
 40 | func StartAdaptingCmd() tea.Msg {
 41 | 	return StartAdaptingMsg(true)
 42 | }
 43 | 
 44 | type FinishAdaptingMsg struct {
 45 | 	Name   string
 46 | 	Colors color.Palette
 47 | }
 48 | 
 49 | type StartExportMsg struct {
 50 | 	SourcePath      string
 51 | 	DestinationPath string
 52 | 	IsDir           bool
 53 | 	UseSubDirs      bool
 54 | }
 55 | 
 56 | func BuildStartExportCmd(msg StartExportMsg) tea.Cmd {
 57 | 	return func() tea.Msg { return msg }
 58 | }
 59 | 
 60 | type FinishExportMsg bool
 61 | 
 62 | func FinishExportingCmd() tea.Msg {
 63 | 	return FinishExportMsg(true)
 64 | }
 65 | 
 66 | // DisplayMsg could eventually contain a type
 67 | // that indicates what style to use (warning, error, etc.)
 68 | type DisplayMsg string
 69 | 
 70 | func BuildDisplayCmd(msg string) tea.Cmd {
 71 | 	return func() tea.Msg { return DisplayMsg(msg) }
 72 | }
 73 | 
 74 | func ClearDisplayCmd() tea.Msg {
 75 | 	return DisplayMsg("")
 76 | }
 77 | 
 78 | // LospecRequestMsg is a url request used to get a list of
 79 | type LospecRequestMsg struct {
 80 | 	ID   int
 81 | 	Page int
 82 | 	URL  string
 83 | }
 84 | 
 85 | func BuildLospecRequestCmd(msg LospecRequestMsg) tea.Cmd {
 86 | 	display := fmt.Sprintf("loading palettes")
 87 | 	return tea.Batch(func() tea.Msg { return msg }, BuildDisplayCmd(display))
 88 | }
 89 | 
 90 | type LospecData struct {
 91 | 	Palettes []struct {
 92 | 		Colors []string `json:"colors"`
 93 | 		Title  string   `json:"title"`
 94 | 	} `json:"palettes"`
 95 | 	TotalCount int `json:"totalCount"`
 96 | }
 97 | 
 98 | type LospecResponseMsg struct {
 99 | 	ID   int
100 | 	Page int
101 | 	Data LospecData
102 | }
103 | 
104 | func BuildLospecResponseCmd(msg LospecResponseMsg) tea.Cmd {
105 | 	return tea.Batch(func() tea.Msg { return msg }, ClearDisplayCmd)
106 | }
107 | 
```

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

```go
  1 | package characters
  2 | 
  3 | import (
  4 | 	"github.com/charmbracelet/bubbles/key"
  5 | 	"github.com/charmbracelet/bubbles/textinput"
  6 | 	tea "github.com/charmbracelet/bubbletea"
  7 | 	"github.com/charmbracelet/lipgloss"
  8 | 
  9 | 	"github.com/Zebbeni/ansizalizer/event"
 10 | )
 11 | 
 12 | type State int
 13 | 
 14 | const (
 15 | 	Ascii State = iota
 16 | 	Unicode
 17 | 	Custom
 18 | 	AsciiAz
 19 | 	AsciiNums
 20 | 	AsciiSpec
 21 | 	AsciiAll
 22 | 	UnicodeFull
 23 | 	UnicodeHalf
 24 | 	UnicodeQuart
 25 | 	UnicodeShadeLight
 26 | 	UnicodeShadeMed
 27 | 	UnicodeShadeHeavy
 28 | 	SymbolsForm
 29 | 	OneColor
 30 | 	TwoColor
 31 | )
 32 | 
 33 | type Model struct {
 34 | 	focus        State
 35 | 	active       State
 36 | 	mode         State
 37 | 	charControls State
 38 | 	unicodeMode  State
 39 | 	asciiMode    State
 40 | 	useFgBg      State
 41 | 	customInput  textinput.Model
 42 | 	ShouldClose  bool
 43 | 	IsActive     bool
 44 | 	width        int
 45 | }
 46 | 
 47 | func New(w int) Model {
 48 | 	return Model{
 49 | 		focus:        Unicode,
 50 | 		active:       Unicode,
 51 | 		mode:         Unicode,
 52 | 		charControls: Unicode,
 53 | 		asciiMode:    AsciiAz,
 54 | 		unicodeMode:  UnicodeHalf,
 55 | 		useFgBg:      TwoColor,
 56 | 		customInput:  newInput("Symbols", "/%A"),
 57 | 		ShouldClose:  false,
 58 | 		IsActive:     false,
 59 | 		width:        w,
 60 | 	}
 61 | }
 62 | 
 63 | func (m Model) Init() tea.Cmd {
 64 | 	return nil
 65 | }
 66 | 
 67 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
 68 | 	switch m.active {
 69 | 	case SymbolsForm:
 70 | 		if m.customInput.Focused() {
 71 | 			return m.handleSymbolsFormUpdate(msg)
 72 | 		}
 73 | 	}
 74 | 
 75 | 	switch msg := msg.(type) {
 76 | 	case tea.KeyMsg:
 77 | 		switch {
 78 | 		case key.Matches(msg, event.KeyMap.Enter):
 79 | 			return m.handleEnter()
 80 | 		case key.Matches(msg, event.KeyMap.Nav):
 81 | 			return m.handleNav(msg)
 82 | 		case key.Matches(msg, event.KeyMap.Esc):
 83 | 			return m.handleEsc()
 84 | 		}
 85 | 	}
 86 | 	return m, nil
 87 | }
 88 | 
 89 | func (m Model) View() string {
 90 | 	colorsButtons := m.drawColorsButtons()
 91 | 	charTabs := m.drawCharTabs()
 92 | 	return lipgloss.JoinVertical(lipgloss.Top, colorsButtons, charTabs)
 93 | }
 94 | 
 95 | // Selected returns the mode, charMode, whether to use two colors, and the
 96 | // current set of custom-defined characters
 97 | func (m Model) Selected() (State, State, State, []rune) {
 98 | 	var charMode State
 99 | 
100 | 	switch m.mode {
101 | 	case Unicode:
102 | 		charMode = m.unicodeMode
103 | 	case Ascii:
104 | 		charMode = m.asciiMode
105 | 	case Custom:
106 | 		charMode = Custom
107 | 	}
108 | 
109 | 	return m.mode, charMode, m.useFgBg, []rune(m.customInput.Value())
110 | }
111 | 
```

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

```go
  1 | package source
  2 | 
  3 | import (
  4 | 	"github.com/charmbracelet/bubbles/key"
  5 | 	tea "github.com/charmbracelet/bubbletea"
  6 | 
  7 | 	"github.com/Zebbeni/ansizalizer/controls/browser"
  8 | 	"github.com/Zebbeni/ansizalizer/event"
  9 | 	"github.com/Zebbeni/ansizalizer/global"
 10 | )
 11 | 
 12 | type Direction int
 13 | 
 14 | const (
 15 | 	Left Direction = iota
 16 | 	Right
 17 | 	Up
 18 | 	Down
 19 | )
 20 | 
 21 | var (
 22 | 	navMap = map[Direction]map[State]State{
 23 | 		Right: {ExpFile: ExpDirectory, SubDirsYes: SubDirsNo},
 24 | 		Left:  {ExpDirectory: ExpFile, SubDirsNo: SubDirsYes},
 25 | 		Down:  {ExpFile: Input, ExpDirectory: Input, Input: SubDirsYes},
 26 | 		Up:    {Input: ExpFile, SubDirsYes: Input, SubDirsNo: Input},
 27 | 	}
 28 | )
 29 | 
 30 | func (m Model) handleEsc() (Model, tea.Cmd) {
 31 | 	m.ShouldClose = true
 32 | 	m.IsActive = false
 33 | 	return m, nil
 34 | }
 35 | 
 36 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
 37 | 	switch {
 38 | 	case key.Matches(msg, event.KeyMap.Right):
 39 | 		if next, hasNext := navMap[Right][m.focus]; hasNext {
 40 | 			m.focus = next
 41 | 		}
 42 | 	case key.Matches(msg, event.KeyMap.Left):
 43 | 		if next, hasNext := navMap[Left][m.focus]; hasNext {
 44 | 			m.focus = next
 45 | 		}
 46 | 	case key.Matches(msg, event.KeyMap.Down):
 47 | 		if next, hasNext := navMap[Down][m.focus]; hasNext {
 48 | 			m.focus = next
 49 | 		} else {
 50 | 			m.ShouldClose = true
 51 | 		}
 52 | 	case key.Matches(msg, event.KeyMap.Up):
 53 | 		if next, hasNext := navMap[Up][m.focus]; hasNext {
 54 | 			m.focus = next
 55 | 		} else {
 56 | 			m.ShouldClose = true
 57 | 		}
 58 | 	}
 59 | 	return m, nil
 60 | }
 61 | 
 62 | func (m Model) handleEnter() (Model, tea.Cmd) {
 63 | 	switch m.focus {
 64 | 	case ExpFile:
 65 | 		m.focus = Browser
 66 | 		m.doExportDirectory = false
 67 | 		m.Browser = browser.New(global.ImgExtensions, m.width)
 68 | 	case ExpDirectory:
 69 | 		m.focus = Browser
 70 | 		m.doExportDirectory = true
 71 | 		m.Browser = browser.New(nil, m.width)
 72 | 	case Input:
 73 | 		m.focus = Browser
 74 | 	case SubDirsYes:
 75 | 		m.includeSubdirectories = true
 76 | 	case SubDirsNo:
 77 | 		m.includeSubdirectories = false
 78 | 	}
 79 | 	return m, nil
 80 | }
 81 | 
 82 | func (m Model) handleSrcBrowserUpdate(msg tea.Msg) (Model, tea.Cmd) {
 83 | 	var cmd tea.Cmd
 84 | 	m.Browser, cmd = m.Browser.Update(msg)
 85 | 	if m.doExportDirectory {
 86 | 		m.selectedDir = m.Browser.SelectedDir
 87 | 	} else {
 88 | 		m.selectedFile = m.Browser.SelectedFile
 89 | 	}
 90 | 
 91 | 	if m.Browser.ShouldClose {
 92 | 		m.focus = Input
 93 | 		m.Browser.ShouldClose = false
 94 | 	}
 95 | 	return m, cmd
 96 | }
 97 | 
 98 | func (m Model) handleIncludeSubdirectories(shouldInclude bool) (Model, tea.Cmd) {
 99 | 	m.includeSubdirectories = shouldInclude
100 | 	return m, nil
101 | }
102 | 
```

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

```go
  1 | package adaptive
  2 | 
  3 | import (
  4 | 	"image/color"
  5 | 	"strconv"
  6 | 
  7 | 	"github.com/charmbracelet/bubbles/key"
  8 | 	"github.com/charmbracelet/bubbles/textinput"
  9 | 	tea "github.com/charmbracelet/bubbletea"
 10 | 	"github.com/charmbracelet/lipgloss"
 11 | 
 12 | 	"github.com/Zebbeni/ansizalizer/event"
 13 | 	"github.com/Zebbeni/ansizalizer/palette"
 14 | )
 15 | 
 16 | type State int
 17 | 
 18 | const (
 19 | 	CountForm State = iota
 20 | 	IterForm
 21 | 	Generate
 22 | 	Save
 23 | )
 24 | 
 25 | type Model struct {
 26 | 	focus  State
 27 | 	active State
 28 | 
 29 | 	palette palette.Model
 30 | 
 31 | 	countInput textinput.Model
 32 | 	iterInput  textinput.Model
 33 | 
 34 | 	width, height int
 35 | 
 36 | 	ShouldClose   bool
 37 | 	ShouldUnfocus bool
 38 | 	IsActive      bool
 39 | 	IsSelected    bool // true if we've selected something (ie. render w/ adaptive)
 40 | }
 41 | 
 42 | func New(w int) Model {
 43 | 	return Model{
 44 | 		focus: CountForm,
 45 | 
 46 | 		countInput: newInput(CountForm),
 47 | 		iterInput:  newInput(IterForm),
 48 | 
 49 | 		ShouldUnfocus: false,
 50 | 		IsActive:      false,
 51 | 		IsSelected:    false,
 52 | 
 53 | 		width: w,
 54 | 	}
 55 | }
 56 | 
 57 | func (m Model) Init() tea.Cmd {
 58 | 	return nil
 59 | }
 60 | 
 61 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
 62 | 	switch m.active {
 63 | 	case CountForm:
 64 | 		if m.countInput.Focused() {
 65 | 			return m.handleCountUpdate(msg)
 66 | 		}
 67 | 	case IterForm:
 68 | 		if m.iterInput.Focused() {
 69 | 			return m.handleIterUpdate(msg)
 70 | 		}
 71 | 	}
 72 | 
 73 | 	switch msg := msg.(type) {
 74 | 	case tea.KeyMsg:
 75 | 		switch {
 76 | 		case key.Matches(msg, event.KeyMap.Enter):
 77 | 			return m.handleEnter()
 78 | 		case key.Matches(msg, event.KeyMap.Nav):
 79 | 			return m.handleNav(msg)
 80 | 		case key.Matches(msg, event.KeyMap.Esc):
 81 | 			return m.handleEsc()
 82 | 		}
 83 | 	}
 84 | 	return m, nil
 85 | }
 86 | 
 87 | func (m Model) View() string {
 88 | 	title := m.drawTitle()
 89 | 	inputs := m.drawInputs()
 90 | 	generate := m.drawGenerateButton()
 91 | 	if len(m.palette.Colors()) == 0 {
 92 | 		return lipgloss.JoinVertical(lipgloss.Top, title, inputs, generate)
 93 | 	}
 94 | 
 95 | 	palette := lipgloss.NewStyle().Padding(0, 1, 0, 1).Render(m.palette.View())
 96 | 	saveButton := m.drawSaveButton()
 97 | 	content := lipgloss.JoinVertical(lipgloss.Top, title, inputs, generate, palette, saveButton)
 98 | 	return content
 99 | }
100 | 
101 | func (m Model) Info() (int, int) {
102 | 	var count, iterations int
103 | 	count, _ = strconv.Atoi(m.countInput.Value())
104 | 	iterations, _ = strconv.Atoi(m.iterInput.Value())
105 | 	return count, iterations
106 | }
107 | 
108 | func (m Model) GetCurrent() palette.Model {
109 | 	return m.palette
110 | }
111 | 
112 | func (m Model) SetPalette(colors color.Palette, name string) Model {
113 | 	m.palette = palette.New(name, colors, m.width-4, 3)
114 | 	return m
115 | }
116 | 
```

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

```go
  1 | package advanced
  2 | 
  3 | import (
  4 | 	"github.com/charmbracelet/bubbles/key"
  5 | 	tea "github.com/charmbracelet/bubbletea"
  6 | 
  7 | 	"github.com/Zebbeni/ansizalizer/event"
  8 | )
  9 | 
 10 | type Direction int
 11 | 
 12 | const (
 13 | 	Left Direction = iota
 14 | 	Right
 15 | 	Up
 16 | 	Down
 17 | )
 18 | 
 19 | var navMap = map[Direction]map[State]State{
 20 | 	Right: {
 21 | 		Sampling: Dithering,
 22 | 	},
 23 | 	Left: {
 24 | 		Dithering: Sampling,
 25 | 	},
 26 | 	Down: {
 27 | 		Sampling:  SamplingControls,
 28 | 		Dithering: DitheringControls,
 29 | 	},
 30 | 	Up: {
 31 | 		SamplingControls:  Sampling,
 32 | 		DitheringControls: Dithering,
 33 | 	},
 34 | }
 35 | 
 36 | func (m Model) handleSamplingUpdate(msg tea.Msg) (Model, tea.Cmd) {
 37 | 	var cmd tea.Cmd
 38 | 	m.sampling, cmd = m.sampling.Update(msg)
 39 | 
 40 | 	if m.sampling.ShouldClose {
 41 | 		m.active = Menu
 42 | 		m.focus = Sampling
 43 | 		m.sampling.ShouldClose = false
 44 | 		m.sampling.IsActive = false
 45 | 	}
 46 | 	return m, cmd
 47 | }
 48 | 
 49 | func (m Model) handleDitheringUpdate(msg tea.Msg) (Model, tea.Cmd) {
 50 | 	var cmd tea.Cmd
 51 | 	m.dithering, cmd = m.dithering.Update(msg)
 52 | 
 53 | 	if m.dithering.ShouldClose {
 54 | 		m.active = Menu
 55 | 		m.focus = Dithering
 56 | 		m.dithering.ShouldClose = false
 57 | 		m.dithering.IsActive = false
 58 | 	}
 59 | 	return m, cmd
 60 | }
 61 | 
 62 | func (m Model) handleEsc() (Model, tea.Cmd) {
 63 | 	m.ShouldClose = true
 64 | 	return m, nil
 65 | }
 66 | 
 67 | func (m Model) handleEnter() (Model, tea.Cmd) {
 68 | 	m.active = m.focus
 69 | 	return m, nil
 70 | }
 71 | 
 72 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
 73 | 	var cmd tea.Cmd
 74 | 	switch {
 75 | 	case key.Matches(msg, event.KeyMap.Right):
 76 | 		if next, hasNext := navMap[Right][m.focus]; hasNext {
 77 | 			return m.setFocus(next)
 78 | 		}
 79 | 	case key.Matches(msg, event.KeyMap.Left):
 80 | 		if next, hasNext := navMap[Left][m.focus]; hasNext {
 81 | 			return m.setFocus(next)
 82 | 		}
 83 | 	case key.Matches(msg, event.KeyMap.Up):
 84 | 		if next, hasNext := navMap[Up][m.focus]; hasNext {
 85 | 			return m.setFocus(next)
 86 | 		} else {
 87 | 			m.IsActive = false
 88 | 			m.ShouldClose = true
 89 | 		}
 90 | 	case key.Matches(msg, event.KeyMap.Down):
 91 | 		if next, hasNext := navMap[Down][m.focus]; hasNext {
 92 | 			return m.setFocus(next)
 93 | 		} else {
 94 | 			m.IsActive = false
 95 | 			m.ShouldClose = true
 96 | 		}
 97 | 	}
 98 | 	return m, cmd
 99 | }
100 | 
101 | func (m Model) setFocus(focus State) (Model, tea.Cmd) {
102 | 	m.focus = focus
103 | 	switch m.focus {
104 | 	case Sampling:
105 | 		m.activeTab = Sampling
106 | 	case Dithering:
107 | 		m.activeTab = Dithering
108 | 	case SamplingControls:
109 | 		m.active = SamplingControls
110 | 		m.sampling.IsActive = true
111 | 	case DitheringControls:
112 | 		m.active = DitheringControls
113 | 		m.dithering.IsActive = true
114 | 	}
115 | 	return m, nil
116 | }
117 | 
```

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

```go
  1 | package size
  2 | 
  3 | import (
  4 | 	"strconv"
  5 | 
  6 | 	"github.com/charmbracelet/bubbles/key"
  7 | 	"github.com/charmbracelet/bubbles/textinput"
  8 | 	tea "github.com/charmbracelet/bubbletea"
  9 | 	"github.com/charmbracelet/lipgloss"
 10 | 
 11 | 	"github.com/Zebbeni/ansizalizer/event"
 12 | )
 13 | 
 14 | const DEFAULT_CHAR_W_TO_H_RATIO = 0.5
 15 | 
 16 | type State int
 17 | type Mode int
 18 | 
 19 | const (
 20 | 	Fit Mode = iota
 21 | 	Stretch
 22 | )
 23 | 
 24 | const (
 25 | 	FitButton State = iota
 26 | 	StretchButton
 27 | 	WidthForm
 28 | 	HeightForm
 29 | 	CharRatioForm
 30 | 	None
 31 | )
 32 | 
 33 | type Model struct {
 34 | 	focus  State
 35 | 	active State
 36 | 	mode   Mode
 37 | 
 38 | 	widthInput     textinput.Model
 39 | 	heightInput    textinput.Model
 40 | 	charRatioInput textinput.Model
 41 | 
 42 | 	ShouldUnfocus bool
 43 | 	ShouldClose   bool
 44 | 	IsActive      bool
 45 | }
 46 | 
 47 | func New() Model {
 48 | 	return Model{
 49 | 		focus:          FitButton,
 50 | 		active:         None,
 51 | 		mode:           Fit,
 52 | 		widthInput:     newInput(WidthForm, 50),
 53 | 		heightInput:    newInput(HeightForm, 40),
 54 | 		charRatioInput: newFloatInput(CharRatioForm, DEFAULT_CHAR_W_TO_H_RATIO),
 55 | 
 56 | 		ShouldUnfocus: false,
 57 | 		ShouldClose:   false,
 58 | 		IsActive:      false,
 59 | 	}
 60 | }
 61 | 
 62 | func (m Model) Init() tea.Cmd {
 63 | 	return nil
 64 | }
 65 | 
 66 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
 67 | 	var cmd1, cmd2 tea.Cmd
 68 | 	newM := m
 69 | 
 70 | 	switch m.active {
 71 | 	case WidthForm:
 72 | 		if m.widthInput.Focused() {
 73 | 			newM, cmd1 = newM.handleWidthUpdate(msg)
 74 | 		}
 75 | 	case HeightForm:
 76 | 		if m.heightInput.Focused() {
 77 | 			newM, cmd1 = newM.handleHeightUpdate(msg)
 78 | 		}
 79 | 	case CharRatioForm:
 80 | 		if m.charRatioInput.Focused() {
 81 | 			newM, cmd1 = newM.handleCharRatioUpdate(msg)
 82 | 		}
 83 | 	}
 84 | 
 85 | 	switch msg := msg.(type) {
 86 | 	case tea.KeyMsg:
 87 | 		switch {
 88 | 		case key.Matches(msg, event.KeyMap.Enter):
 89 | 			newM, cmd2 = newM.handleEnter()
 90 | 		case key.Matches(msg, event.KeyMap.Nav):
 91 | 			newM, cmd2 = newM.handleNav(msg)
 92 | 		case key.Matches(msg, event.KeyMap.Esc):
 93 | 			newM, cmd2 = newM.handleEsc()
 94 | 		}
 95 | 	}
 96 | 	return newM, tea.Batch(cmd1, cmd2)
 97 | }
 98 | 
 99 | func (m Model) View() string {
100 | 	buttonRow := m.drawButtons()
101 | 	forms := m.drawSizeForms()
102 | 	ratioForm := m.drawCharRatioForm()
103 | 	return lipgloss.JoinVertical(lipgloss.Left, buttonRow, forms, ratioForm)
104 | }
105 | 
106 | func (m Model) Info() (Mode, int, int, float64) {
107 | 	var width, height int
108 | 	width, _ = strconv.Atoi(m.widthInput.Value())
109 | 	height, _ = strconv.Atoi(m.heightInput.Value())
110 | 	charRatio, err := strconv.ParseFloat(m.charRatioInput.Value(), 64)
111 | 	if err != nil {
112 | 		charRatio = DEFAULT_CHAR_W_TO_H_RATIO
113 | 	}
114 | 	return m.mode, width, height, charRatio
115 | }
116 | 
```

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

```go
  1 | package browser
  2 | 
  3 | import (
  4 | 	"github.com/charmbracelet/bubbles/key"
  5 | 	tea "github.com/charmbracelet/bubbletea"
  6 | 
  7 | 	"github.com/Zebbeni/ansizalizer/controls/menu"
  8 | 	"github.com/Zebbeni/ansizalizer/event"
  9 | )
 10 | 
 11 | func (m Model) handleEnter() (Model, tea.Cmd) {
 12 | 	return m.updateSelected()
 13 | }
 14 | 
 15 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
 16 | 	if m.currentList().Index() == 0 && key.Matches(msg, event.KeyMap.Up) {
 17 | 		m.ShouldClose = true
 18 | 		return m, nil
 19 | 	}
 20 | 
 21 | 	cmds := make([]tea.Cmd, 2)
 22 | 	m.lists[m.listIndex()], cmds[0] = m.currentList().Update(msg)
 23 | 	m, cmds[1] = m.updateActive()
 24 | 	return m, tea.Batch(cmds...)
 25 | }
 26 | 
 27 | func (m Model) handleEsc() (Model, tea.Cmd) {
 28 | 	// remove last list if possible (go back to previous)
 29 | 	if len(m.lists) > 1 {
 30 | 		m.lists = m.lists[:m.listIndex()]
 31 | 		return m, nil
 32 | 	}
 33 | 
 34 | 	m.ShouldClose = true
 35 | 	return m, nil
 36 | }
 37 | 
 38 | func (m Model) updateActive() (Model, tea.Cmd) {
 39 | 	itm, ok := m.currentList().SelectedItem().(item)
 40 | 	if !ok {
 41 | 		panic("Unexpected list item type")
 42 | 	}
 43 | 
 44 | 	if itm.isDir && m.ActiveDir != itm.path {
 45 | 		m.ActiveDir = itm.path
 46 | 		return m, nil
 47 | 	}
 48 | 
 49 | 	if itm.isDir == false && m.ActiveFile != itm.path {
 50 | 		m.ActiveFile = itm.path
 51 | 		return m, event.StartRenderToViewCmd
 52 | 	}
 53 | 
 54 | 	return m, nil
 55 | }
 56 | 
 57 | func (m Model) updateSelected() (Model, tea.Cmd) {
 58 | 	itm, ok := m.currentList().SelectedItem().(item)
 59 | 	if !ok {
 60 | 		panic("Unexpected list item type")
 61 | 	}
 62 | 
 63 | 	if itm.isDir {
 64 | 		m.SelectedDir = itm.path
 65 | 		m = m.addListForDirectory(itm.path)
 66 | 	} else {
 67 | 		m.SelectedFile = itm.path
 68 | 		m.ShouldClose = true
 69 | 	}
 70 | 
 71 | 	return m, nil
 72 | }
 73 | 
 74 | func (m Model) addListForDirectory(dir string) Model {
 75 | 	newList := menu.New(getItems(m.fileExtensions, dir), m.width)
 76 | 
 77 | 	newList.SetShowTitle(false)
 78 | 
 79 | 	//title := filepath.Join(filepath.Base(filepath.Dir(dir)), filepath.Base(dir))
 80 | 
 81 | 	//newList.Title = fitString(title, m.width-10)
 82 | 	//newList.Styles.Title = newList.Styles.Title.Copy().Foreground(style.DimmedColor2).UnsetBackground()
 83 | 	//newList.Styles.TitleBar = newList.Styles.TitleBar.Copy().Padding(0).Height(2)
 84 | 	newList.SetShowStatusBar(false)
 85 | 	newList.SetFilteringEnabled(false)
 86 | 	newList.SetShowFilter(false)
 87 | 	newList.SetWidth(m.width)
 88 | 
 89 | 	m.lists = append(m.lists, newList)
 90 | 	m.SelectedDir = dir
 91 | 
 92 | 	return m
 93 | }
 94 | 
 95 | func fitString(value string, width int) string {
 96 | 	valueRunes := []rune(value)
 97 | 
 98 | 	start := len(valueRunes) - width - 2
 99 | 	if start < 0 {
100 | 		start = 0
101 | 	}
102 | 
103 | 	if len(valueRunes) > width {
104 | 		value = "\n.." + string(valueRunes[start:])
105 | 	}
106 | 
107 | 	return value
108 | }
109 | 
```

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

```go
  1 | package export
  2 | 
  3 | import (
  4 | 	"github.com/charmbracelet/bubbles/key"
  5 | 	tea "github.com/charmbracelet/bubbletea"
  6 | 
  7 | 	"github.com/Zebbeni/ansizalizer/event"
  8 | )
  9 | 
 10 | type Direction int
 11 | 
 12 | const (
 13 | 	Down Direction = iota
 14 | 	Up
 15 | )
 16 | 
 17 | var navMap = map[Direction]map[State]State{
 18 | 	Down: {Source: Destination, Destination: Process},
 19 | 	Up:   {Destination: Source, Process: Destination},
 20 | }
 21 | 
 22 | func (m Model) handleSourceUpdate(msg tea.Msg) (Model, tea.Cmd) {
 23 | 	var cmd tea.Cmd
 24 | 	m.Source, cmd = m.Source.Update(msg)
 25 | 
 26 | 	if m.Source.ShouldClose {
 27 | 		m.active = None
 28 | 		m.Source.ShouldClose = false
 29 | 	}
 30 | 	if m.Source.ShouldUnfocus {
 31 | 		return m.handleMenuUpdate(msg)
 32 | 	}
 33 | 	return m, cmd
 34 | }
 35 | 
 36 | func (m Model) handleDestinationUpdate(msg tea.Msg) (Model, tea.Cmd) {
 37 | 	var cmd tea.Cmd
 38 | 	m.Destination, cmd = m.Destination.Update(msg)
 39 | 
 40 | 	if m.Destination.ShouldClose {
 41 | 		m.active = None
 42 | 		m.Destination.ShouldClose = false
 43 | 	}
 44 | 	return m, cmd
 45 | }
 46 | 
 47 | func (m Model) handleEnter() (Model, tea.Cmd) {
 48 | 	m.active = m.focus
 49 | 	switch m.active {
 50 | 	case Source:
 51 | 		m.Source.IsActive = true
 52 | 	case Destination:
 53 | 		m.Destination.IsActive = true
 54 | 	case Process:
 55 | 		return m.handleProcess()
 56 | 	}
 57 | 	return m, nil
 58 | }
 59 | 
 60 | func (m Model) handleEsc() (Model, tea.Cmd) {
 61 | 	m.ShouldClose = true
 62 | 	return m, nil
 63 | }
 64 | 
 65 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
 66 | 	switch {
 67 | 	case key.Matches(msg, event.KeyMap.Down):
 68 | 		if next, hasNext := navMap[Down][m.focus]; hasNext {
 69 | 			m.focus = next
 70 | 		} else {
 71 | 			m.ShouldClose = true
 72 | 		}
 73 | 	case key.Matches(msg, event.KeyMap.Up):
 74 | 		if next, hasNext := navMap[Up][m.focus]; hasNext {
 75 | 			m.focus = next
 76 | 		} else {
 77 | 			m.ShouldClose = true
 78 | 		}
 79 | 	}
 80 | 	return m, nil
 81 | }
 82 | 
 83 | func (m Model) handleMenuUpdate(msg tea.Msg) (Model, tea.Cmd) {
 84 | 	if keyMsg, ok := msg.(tea.KeyMsg); ok {
 85 | 		return m.handleKeyMsg(keyMsg)
 86 | 	}
 87 | 	return m, nil
 88 | }
 89 | 
 90 | func (m Model) handleKeyMsg(msg tea.KeyMsg) (Model, tea.Cmd) {
 91 | 	var cmd tea.Cmd
 92 | 	switch {
 93 | 	case key.Matches(msg, event.KeyMap.Enter):
 94 | 		return m.handleEnter()
 95 | 	case key.Matches(msg, event.KeyMap.Nav):
 96 | 		return m.handleNav(msg)
 97 | 	case key.Matches(msg, event.KeyMap.Esc):
 98 | 		return m.handleEsc()
 99 | 	}
100 | 	return m, cmd
101 | }
102 | 
103 | func (m Model) handleProcess() (Model, tea.Cmd) {
104 | 	sourcePath, isDir, useSubDirs := m.Source.GetSelected()
105 | 	destinationPath := m.Destination.GetSelected()
106 | 	return m, event.BuildStartExportCmd(event.StartExportMsg{
107 | 		SourcePath:      sourcePath,
108 | 		DestinationPath: destinationPath,
109 | 		IsDir:           isDir,
110 | 		UseSubDirs:      useSubDirs,
111 | 	})
112 | }
113 | 
114 | func (m Model) GetDestination() (path string) {
115 | 	return m.Destination.GetSelected()
116 | }
117 | 
```

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

```go
  1 | package loader
  2 | 
  3 | import (
  4 | 	"bufio"
  5 | 	"fmt"
  6 | 	"image/color"
  7 | 	"os"
  8 | 	"path/filepath"
  9 | 	"strings"
 10 | 
 11 | 	tea "github.com/charmbracelet/bubbletea"
 12 | 	"github.com/charmbracelet/lipgloss"
 13 | 	"github.com/lucasb-eyer/go-colorful"
 14 | 
 15 | 	"github.com/Zebbeni/ansizalizer/controls/browser"
 16 | 	"github.com/Zebbeni/ansizalizer/event"
 17 | 	"github.com/Zebbeni/ansizalizer/palette"
 18 | 	"github.com/Zebbeni/ansizalizer/style"
 19 | )
 20 | 
 21 | var (
 22 | 	paletteExtensions = map[string]bool{".hex": true}
 23 | )
 24 | 
 25 | type Model struct {
 26 | 	FileBrowser browser.Model
 27 | 
 28 | 	paletteFilepath string
 29 | 	palette         palette.Model
 30 | 
 31 | 	IsSelected    bool // true if we've selected something (ie. render w/ loader)
 32 | 	ShouldUnfocus bool
 33 | 
 34 | 	width int
 35 | }
 36 | 
 37 | func New(w int) Model {
 38 | 	fileBrowser := browser.New(paletteExtensions, w-2)
 39 | 
 40 | 	return Model{
 41 | 		FileBrowser:   fileBrowser,
 42 | 		IsSelected:    false,
 43 | 		ShouldUnfocus: false,
 44 | 		width:         w,
 45 | 	}
 46 | }
 47 | 
 48 | func (m Model) Init() tea.Cmd {
 49 | 	return nil
 50 | }
 51 | 
 52 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
 53 | 	var cmd tea.Cmd
 54 | 
 55 | 	m.FileBrowser, cmd = m.FileBrowser.Update(msg)
 56 | 
 57 | 	if m.FileBrowser.ActiveFile != m.paletteFilepath {
 58 | 		m.paletteFilepath = m.FileBrowser.ActiveFile
 59 | 
 60 | 		name := strings.Split(filepath.Base(m.paletteFilepath), ".hex")[0]
 61 | 		colors, err := parsePaletteFile(m.paletteFilepath)
 62 | 		if err != nil {
 63 | 			return m, tea.Batch(cmd, event.BuildDisplayCmd("error parsing paletteFilepath file"))
 64 | 		}
 65 | 		m.palette = palette.New(name, colors, m.width-5, 3)
 66 | 
 67 | 		m.IsSelected = true
 68 | 		return m, tea.Batch(cmd, event.StartRenderToViewCmd)
 69 | 	}
 70 | 
 71 | 	if m.FileBrowser.ShouldClose {
 72 | 		m.IsSelected = false
 73 | 		m.FileBrowser.ShouldClose = false
 74 | 		m.ShouldUnfocus = true
 75 | 	}
 76 | 
 77 | 	return m, cmd
 78 | }
 79 | 
 80 | func (m Model) View() string {
 81 | 	activePreview := style.DimmedTitle.Render("No palette selected")
 82 | 	if len(m.palette.Colors()) != 0 {
 83 | 		activePreview = m.palette.View()
 84 | 	}
 85 | 	activePreview = lipgloss.NewStyle().Padding(0, 0, 1, 2).Render(activePreview)
 86 | 
 87 | 	title := m.drawTitle()
 88 | 	browser := m.FileBrowser.View()
 89 | 	return lipgloss.JoinVertical(lipgloss.Top, title, browser, activePreview)
 90 | }
 91 | 
 92 | func (m Model) GetCurrent() palette.Model {
 93 | 	return m.palette
 94 | }
 95 | 
 96 | func parsePaletteFile(filepath string) (color.Palette, error) {
 97 | 	readFile, err := os.Open(filepath)
 98 | 	if err != nil {
 99 | 		return nil, err
100 | 	}
101 | 
102 | 	fileScanner := bufio.NewScanner(readFile)
103 | 	fileScanner.Split(bufio.ScanLines)
104 | 
105 | 	var col colorful.Color
106 | 	p := make(color.Palette, 0, 256)
107 | 
108 | 	for fileScanner.Scan() {
109 | 		col, err = colorful.Hex(fmt.Sprintf("#%s", fileScanner.Text()))
110 | 		if err != nil {
111 | 			return nil, err
112 | 		}
113 | 		p = append(p, col)
114 | 	}
115 | 
116 | 	return p, nil
117 | }
118 | 
```

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

```go
  1 | package palettes
  2 | 
  3 | import (
  4 | 	tea "github.com/charmbracelet/bubbletea"
  5 | 	"github.com/charmbracelet/lipgloss"
  6 | 
  7 | 	"github.com/Zebbeni/ansizalizer/controls/settings/palettes/adaptive"
  8 | 	"github.com/Zebbeni/ansizalizer/controls/settings/palettes/loader"
  9 | 	"github.com/Zebbeni/ansizalizer/controls/settings/palettes/lospec"
 10 | 	"github.com/Zebbeni/ansizalizer/palette"
 11 | )
 12 | 
 13 | type State int
 14 | 
 15 | // None consists of a few different components that are shown or hidden
 16 | // depending on which toggles have been set on / off. The Model state indicates
 17 | // which component is currently focused. From top to bottom the components are:
 18 | 
 19 | // 1) Limited (on/off)
 20 | // 2) Loader (Name) (if Limited) -> [Enter] displays Loader menu
 21 | // 3) Dithering (on/off) (if Limited)
 22 | // 4) Serpentine (on/off) (if Dithering)
 23 | // 5) Matrix (Name) (if Dithering) -> [Enter] displays to Matrix menu
 24 | 
 25 | // These can all be part of a single list, but we need to onSelect the list items
 26 | 
 27 | const (
 28 | 	Adapt State = iota
 29 | 	Load
 30 | 	Lospec
 31 | 	AdaptiveControls
 32 | 	LoadControls
 33 | 	LospecControls
 34 | )
 35 | 
 36 | type Model struct {
 37 | 	selected State
 38 | 	focus    State // the component taking input
 39 | 	controls State
 40 | 
 41 | 	Adapter adaptive.Model
 42 | 	Loader  loader.Model
 43 | 	Lospec  lospec.Model
 44 | 
 45 | 	ShouldClose bool
 46 | 
 47 | 	IsActive bool
 48 | 
 49 | 	width int
 50 | }
 51 | 
 52 | func New(w int) Model {
 53 | 	m := Model{
 54 | 		selected:    Load,
 55 | 		focus:       Load,
 56 | 		controls:    Load,
 57 | 		Adapter:     adaptive.New(w),
 58 | 		Loader:      loader.New(w),
 59 | 		Lospec:      lospec.New(w),
 60 | 		ShouldClose: false,
 61 | 		IsActive:    false,
 62 | 		width:       w,
 63 | 	}
 64 | 	return m
 65 | }
 66 | 
 67 | func (m Model) Init() tea.Cmd {
 68 | 	return nil
 69 | }
 70 | 
 71 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
 72 | 	switch m.focus {
 73 | 	case AdaptiveControls:
 74 | 		return m.handleAdaptiveUpdate(msg)
 75 | 	case LoadControls:
 76 | 		return m.handleLoaderUpdate(msg)
 77 | 	case LospecControls:
 78 | 		return m.handleLospecUpdate(msg)
 79 | 	}
 80 | 	return m.handleMenuUpdate(msg)
 81 | }
 82 | 
 83 | func (m Model) View() string {
 84 | 	buttons := m.drawButtons()
 85 | 	if m.IsActive == false {
 86 | 		return buttons
 87 | 	}
 88 | 
 89 | 	var controls string
 90 | 	switch m.controls {
 91 | 	case Adapt:
 92 | 		controls = m.Adapter.View()
 93 | 	case Load:
 94 | 		controls = m.Loader.View()
 95 | 	case Lospec:
 96 | 		controls = m.Lospec.View()
 97 | 	}
 98 | 	if len(controls) == 0 {
 99 | 		return buttons
100 | 	}
101 | 
102 | 	return lipgloss.JoinVertical(lipgloss.Top, buttons, controls)
103 | }
104 | 
105 | func (m Model) IsAdaptive() bool {
106 | 	return m.selected == Adapt
107 | }
108 | 
109 | func (m Model) IsPaletted() bool {
110 | 	return m.selected == Load
111 | }
112 | 
113 | func (m Model) GetCurrentPalette() palette.Model {
114 | 	switch m.selected {
115 | 	case Load:
116 | 		return m.Loader.GetCurrent()
117 | 	case Adapt:
118 | 		return m.Adapter.GetCurrent()
119 | 	case Lospec:
120 | 		return m.Lospec.GetCurrent()
121 | 	}
122 | 	return palette.Model{}
123 | }
124 | 
```

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

```go
  1 | package settings
  2 | 
  3 | import (
  4 | 	"github.com/charmbracelet/bubbles/key"
  5 | 	tea "github.com/charmbracelet/bubbletea"
  6 | 
  7 | 	"github.com/Zebbeni/ansizalizer/event"
  8 | )
  9 | 
 10 | type Direction int
 11 | 
 12 | const (
 13 | 	Down Direction = iota
 14 | 	Up
 15 | )
 16 | 
 17 | var navMap = map[Direction]map[State]State{
 18 | 	Down: {Colors: Characters, Characters: Size, Size: Advanced},
 19 | 	Up:   {Advanced: Size, Size: Characters, Characters: Colors},
 20 | }
 21 | 
 22 | func (m Model) handleSettingsUpdate(msg tea.Msg) (Model, tea.Cmd) {
 23 | 	if keyMsg, ok := msg.(tea.KeyMsg); ok {
 24 | 		return m.handleKeyMsg(keyMsg)
 25 | 	}
 26 | 	return m, nil
 27 | }
 28 | 
 29 | func (m Model) handleColorsUpdate(msg tea.Msg) (Model, tea.Cmd) {
 30 | 	var cmd tea.Cmd
 31 | 	m.Colors, cmd = m.Colors.Update(msg)
 32 | 
 33 | 	if m.Colors.ShouldClose {
 34 | 		m.active = None
 35 | 		m.Colors.IsActive = false
 36 | 		m.Colors.ShouldClose = false
 37 | 	}
 38 | 	return m, cmd
 39 | }
 40 | 
 41 | func (m Model) handleCharactersUpdate(msg tea.Msg) (Model, tea.Cmd) {
 42 | 	var cmd tea.Cmd
 43 | 	m.Characters, cmd = m.Characters.Update(msg)
 44 | 
 45 | 	if m.Characters.ShouldClose {
 46 | 		m.active = None
 47 | 		m.Characters.IsActive = false
 48 | 		m.Characters.ShouldClose = false
 49 | 	}
 50 | 	return m, cmd
 51 | }
 52 | 
 53 | func (m Model) handleSizeUpdate(msg tea.Msg) (Model, tea.Cmd) {
 54 | 	var cmd tea.Cmd
 55 | 	m.Size, cmd = m.Size.Update(msg)
 56 | 	if m.Size.ShouldClose {
 57 | 		m.active = None
 58 | 		m.Size.IsActive = false
 59 | 		m.Size.ShouldClose = false
 60 | 	}
 61 | 	if m.Size.ShouldUnfocus {
 62 | 		return m.handleSettingsUpdate(msg)
 63 | 	}
 64 | 	return m, cmd
 65 | }
 66 | 
 67 | func (m Model) handleAdvancedUpdate(msg tea.Msg) (Model, tea.Cmd) {
 68 | 	var cmd tea.Cmd
 69 | 	m.Advanced, cmd = m.Advanced.Update(msg)
 70 | 
 71 | 	if m.Advanced.ShouldClose {
 72 | 		m.active = None
 73 | 		m.Advanced.ShouldClose = false
 74 | 	}
 75 | 	return m, cmd
 76 | }
 77 | 
 78 | func (m Model) handleEnter() (Model, tea.Cmd) {
 79 | 	m.active = m.focus
 80 | 	switch m.active {
 81 | 	case Colors:
 82 | 		m.Colors.IsActive = true
 83 | 	case Characters:
 84 | 		m.Characters.IsActive = true
 85 | 	case Size:
 86 | 		m.Size.IsActive = true
 87 | 	case Advanced:
 88 | 		m.Advanced.IsActive = true
 89 | 	}
 90 | 	return m, nil
 91 | }
 92 | 
 93 | func (m Model) handleEsc() (Model, tea.Cmd) {
 94 | 	m.ShouldClose = true
 95 | 	return m, nil
 96 | }
 97 | 
 98 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
 99 | 	switch {
100 | 	case key.Matches(msg, event.KeyMap.Down):
101 | 		if next, hasNext := navMap[Down][m.focus]; hasNext {
102 | 			m.focus = next
103 | 		}
104 | 	case key.Matches(msg, event.KeyMap.Up):
105 | 		if next, hasNext := navMap[Up][m.focus]; hasNext {
106 | 			m.focus = next
107 | 		} else {
108 | 			m.ShouldClose = true
109 | 		}
110 | 	}
111 | 	return m, nil
112 | }
113 | 
114 | func (m Model) handleKeyMsg(msg tea.KeyMsg) (Model, tea.Cmd) {
115 | 	var cmd tea.Cmd
116 | 	switch {
117 | 	case key.Matches(msg, event.KeyMap.Enter):
118 | 		return m.handleEnter()
119 | 	case key.Matches(msg, event.KeyMap.Nav):
120 | 		return m.handleNav(msg)
121 | 	case key.Matches(msg, event.KeyMap.Esc):
122 | 		return m.handleEsc()
123 | 	}
124 | 	return m, cmd
125 | }
126 | 
```

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

```go
 1 | package style
 2 | 
 3 | import (
 4 | 	"strings"
 5 | 
 6 | 	"github.com/charmbracelet/lipgloss"
 7 | )
 8 | 
 9 | type BoxWithLabel struct {
10 | 	BoxStyle   lipgloss.Style
11 | 	LabelStyle lipgloss.Style
12 | }
13 | 
14 | func NewDefaultBoxWithLabel() BoxWithLabel {
15 | 	return BoxWithLabel{
16 | 		BoxStyle: lipgloss.NewStyle().
17 | 			Border(lipgloss.RoundedBorder()).
18 | 			BorderForeground(lipgloss.Color("63")),
19 | 
20 | 		// You could, of course, also set background and foreground colors here
21 | 		// as well.
22 | 		LabelStyle: lipgloss.NewStyle().
23 | 			AlignHorizontal(lipgloss.Center).
24 | 			PaddingTop(0).
25 | 			PaddingBottom(0),
26 | 	}
27 | }
28 | 
29 | func (b BoxWithLabel) Render(label, content string, width int) string {
30 | 	var (
31 | 		// Query the box style for some of its border properties so we can
32 | 		// essentially take the top border apart and put it around the label.
33 | 		border             lipgloss.Border     = b.BoxStyle.GetBorderStyle()
34 | 		topBorderStyler    func(string) string = lipgloss.NewStyle().Foreground(b.BoxStyle.GetBorderTopForeground()).Render
35 | 		bottomBorderStyler func(string) string = lipgloss.NewStyle().Foreground(b.BoxStyle.GetBorderBottomForeground()).Render
36 | 		topLeft            string              = topBorderStyler(border.TopLeft)
37 | 		topRight           string              = topBorderStyler(border.TopRight)
38 | 		botLeft            string              = bottomBorderStyler(border.BottomLeft)
39 | 		botRight           string              = bottomBorderStyler(border.BottomRight)
40 | 
41 | 		renderedLabel string = b.LabelStyle.Render(label)
42 | 	)
43 | 
44 | 	// Render top row with the label
45 | 	borderWidth := b.BoxStyle.GetHorizontalBorderSize()
46 | 	cellsShort := max(0, width+borderWidth-lipgloss.Width(topLeft+topRight+renderedLabel))
47 | 
48 | 	gap := strings.Repeat(border.Top, cellsShort)
49 | 	var gapLeft, gapRight string
50 | 	switch b.LabelStyle.GetAlignHorizontal() {
51 | 	case lipgloss.Left:
52 | 		gapRight = gap
53 | 	case lipgloss.Right:
54 | 		gapLeft = gap
55 | 	case lipgloss.Center:
56 | 		gapLeft = strings.Repeat(border.Top, cellsShort/2)
57 | 		gapRight = strings.Repeat(border.Top, cellsShort-(cellsShort/2))
58 | 	}
59 | 
60 | 	var top, bottom string
61 | 
62 | 	switch b.LabelStyle.GetAlignVertical() {
63 | 	case lipgloss.Top:
64 | 		strings.Repeat(border.Top, cellsShort)
65 | 		top = topLeft + topBorderStyler(gapLeft) + renderedLabel + topBorderStyler(gapRight) + topRight
66 | 		bottom = b.BoxStyle.Copy().
67 | 			BorderTop(false).
68 | 			Width(width).
69 | 			Render(content)
70 | 	case lipgloss.Bottom:
71 | 		strings.Repeat(border.Bottom, cellsShort)
72 | 		bottom = botLeft + bottomBorderStyler(gapLeft) + renderedLabel + bottomBorderStyler(gapRight) + botRight
73 | 		top = b.BoxStyle.Copy().
74 | 			BorderBottom(false).
75 | 			Width(width).
76 | 			Render(content)
77 | 	}
78 | 
79 | 	// Stack the pieces
80 | 	return top + "\n" + bottom
81 | }
82 | 
83 | func max(a, b int) int {
84 | 	if a > b {
85 | 		return a
86 | 	}
87 | 	return b
88 | }
89 | 
```

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

```go
  1 | package dithering
  2 | 
  3 | import (
  4 | 	"github.com/charmbracelet/bubbles/key"
  5 | 	tea "github.com/charmbracelet/bubbletea"
  6 | 
  7 | 	"github.com/Zebbeni/ansizalizer/event"
  8 | )
  9 | 
 10 | type Direction int
 11 | 
 12 | const (
 13 | 	Left Direction = iota
 14 | 	Right
 15 | 	Up
 16 | 	Down
 17 | )
 18 | 
 19 | var navMap = map[Direction]map[State]State{
 20 | 	Right: {
 21 | 		DitherOn:     DitherOff,
 22 | 		SerpentineOn: SerpentineOff,
 23 | 	},
 24 | 	Left: {
 25 | 		DitherOff:     DitherOn,
 26 | 		SerpentineOff: SerpentineOn,
 27 | 	},
 28 | 	Down: {
 29 | 		DitherOn:      SerpentineOn,
 30 | 		DitherOff:     SerpentineOff,
 31 | 		SerpentineOn:  Matrix,
 32 | 		SerpentineOff: Matrix,
 33 | 	},
 34 | 	Up: {
 35 | 		SerpentineOn:  DitherOn,
 36 | 		SerpentineOff: DitherOff,
 37 | 		Matrix:        SerpentineOn,
 38 | 	},
 39 | }
 40 | 
 41 | func (m Model) handleMatrixListUpdate(msg tea.Msg) (Model, tea.Cmd) {
 42 | 	if keyMsg, ok := msg.(tea.KeyMsg); ok {
 43 | 		switch {
 44 | 		case key.Matches(keyMsg, event.KeyMap.Up) && m.list.Index() == 0:
 45 | 			return m.handleNav(keyMsg)
 46 | 		case key.Matches(keyMsg, event.KeyMap.Esc):
 47 | 		case key.Matches(keyMsg, event.KeyMap.Enter):
 48 | 			var cmd tea.Cmd
 49 | 			m, cmd = m.setFocus(navMap[Up][Matrix])
 50 | 			return m, tea.Batch(cmd, event.StartRenderToViewCmd)
 51 | 		}
 52 | 	}
 53 | 
 54 | 	var cmd tea.Cmd
 55 | 	m.list, cmd = m.list.Update(msg)
 56 | 	return m, cmd
 57 | }
 58 | 
 59 | func (m Model) handleEsc() (Model, tea.Cmd) {
 60 | 	m.ShouldClose = true
 61 | 	return m, nil
 62 | }
 63 | 
 64 | func (m Model) handleEnter() (Model, tea.Cmd) {
 65 | 	switch m.focus {
 66 | 	case DitherOn:
 67 | 		m.doDithering = true
 68 | 	case DitherOff:
 69 | 		m.doDithering = false
 70 | 	case SerpentineOn:
 71 | 		m.doSerpentine = true
 72 | 	case SerpentineOff:
 73 | 		m.doSerpentine = false
 74 | 	}
 75 | 	return m, event.StartRenderToViewCmd
 76 | }
 77 | 
 78 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
 79 | 	var cmd tea.Cmd
 80 | 	switch {
 81 | 	case key.Matches(msg, event.KeyMap.Right):
 82 | 		if next, hasNext := navMap[Right][m.focus]; hasNext {
 83 | 			return m.setFocus(next)
 84 | 		}
 85 | 	case key.Matches(msg, event.KeyMap.Left):
 86 | 		if next, hasNext := navMap[Left][m.focus]; hasNext {
 87 | 			return m.setFocus(next)
 88 | 		}
 89 | 	case key.Matches(msg, event.KeyMap.Up):
 90 | 		if next, hasNext := navMap[Up][m.focus]; hasNext {
 91 | 			return m.setFocus(next)
 92 | 		} else {
 93 | 			m.ShouldClose = true
 94 | 		}
 95 | 	case key.Matches(msg, event.KeyMap.Down):
 96 | 		if next, hasNext := navMap[Down][m.focus]; hasNext {
 97 | 			return m.setFocus(next)
 98 | 		} else {
 99 | 			m.ShouldClose = true
100 | 		}
101 | 	}
102 | 	return m, cmd
103 | }
104 | 
105 | func (m Model) handleKeyMsg(msg tea.KeyMsg) (Model, tea.Cmd) {
106 | 	var cmd tea.Cmd
107 | 	switch {
108 | 	case key.Matches(msg, event.KeyMap.Enter):
109 | 		return m.handleEnter()
110 | 	case key.Matches(msg, event.KeyMap.Nav):
111 | 		return m.handleNav(msg)
112 | 	case key.Matches(msg, event.KeyMap.Esc):
113 | 		return m.handleEsc()
114 | 	}
115 | 	return m, cmd
116 | }
117 | 
118 | func (m Model) setFocus(focus State) (Model, tea.Cmd) {
119 | 	m.focus = focus
120 | 	if focus != Matrix {
121 | 		m.list.SetDelegate(NewDelegate(false))
122 | 	} else {
123 | 		m.list.SetDelegate(NewDelegate(true))
124 | 	}
125 | 
126 | 	return m, nil
127 | }
128 | 
```

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

```go
  1 | package app
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"os"
  6 | 	"path/filepath"
  7 | 	"strings"
  8 | 
  9 | 	"github.com/Zebbeni/ansizalizer/global"
 10 | )
 11 | 
 12 | const (
 13 | 	maxExportJobs = 1000
 14 | )
 15 | 
 16 | type exportJob struct {
 17 | 	sourcePath      string
 18 | 	destinationPath string
 19 | }
 20 | 
 21 | type MaxExportQueueError struct {
 22 | 	count int
 23 | }
 24 | 
 25 | func (r *MaxExportQueueError) Error() string {
 26 | 	return fmt.Sprintf("%d+ export jobs exceed %d max", r.count, maxExportJobs)
 27 | }
 28 | 
 29 | // this process may get more complicated if we want to do animated gifs,
 30 | // since each gif  will require multiple image exports.
 31 | func buildExportQueue(dirPath, destPath string, useSubDirs bool) ([]exportJob, error) {
 32 | 	// for each image file found in the dirPath, append an exportJob object
 33 | 	// with the source filepath and its corresponding .ansi destination filepath
 34 | 	entries, err := os.ReadDir(dirPath)
 35 | 	if err != nil {
 36 | 		return nil, err
 37 | 	}
 38 | 
 39 | 	exportJobs := make([]exportJob, 0, len(entries))
 40 | 	subDirs := make([]string, 0, len(entries))
 41 | 
 42 | 	for _, e := range entries {
 43 | 		sourcePath := filepath.Join(dirPath, e.Name())
 44 | 
 45 | 		if e.IsDir() {
 46 | 			subDirs = append(subDirs, sourcePath)
 47 | 			continue
 48 | 		}
 49 | 
 50 | 		ext := filepath.Ext(e.Name())
 51 | 		if _, ok := global.ImgExtensions[ext]; ok {
 52 | 			nameWithoutExt := strings.Split(filepath.Base(sourcePath), ".")[0]
 53 | 			nameWithExt := fmt.Sprintf("%s.ansi", nameWithoutExt)
 54 | 			destFilePath := filepath.Join(destPath, nameWithExt)
 55 | 			exportJobs = append(exportJobs, exportJob{
 56 | 				sourcePath:      sourcePath,
 57 | 				destinationPath: destFilePath,
 58 | 			})
 59 | 		}
 60 | 	}
 61 | 
 62 | 	if useSubDirs {
 63 | 		// call buildExportQueue on each subdirectory in dirPath, creating
 64 | 		// subdirectories in the destination path to mimic the source directory
 65 | 		// structure, and providing these subdirectory paths to the build call as well
 66 | 		for _, subDir := range subDirs {
 67 | 
 68 | 			subDirName := filepath.Base(subDir)
 69 | 			subDestPath := filepath.Join(destPath, subDirName)
 70 | 
 71 | 			var subDirExportJobs []exportJob
 72 | 			subDirExportJobs, err = buildExportQueue(subDir, subDestPath, true)
 73 | 			if err != nil {
 74 | 				return nil, err
 75 | 			}
 76 | 
 77 | 			// append resulting exportJob lists to the main list
 78 | 			exportJobs = append(exportJobs, subDirExportJobs...)
 79 | 			if len(exportJobs) > maxExportJobs {
 80 | 				return nil, &MaxExportQueueError{count: len(exportJobs)}
 81 | 			}
 82 | 
 83 | 			// skip creating mirrored subdirectories if no files found there
 84 | 			if len(subDirExportJobs) == 0 {
 85 | 				continue
 86 | 			}
 87 | 
 88 | 			// create the destination folder if it doesn't already exist
 89 | 			// do this after the recursive call to buildExportQueue. Otherwise,
 90 | 			// we can hit an infinite loop where our newly created directories
 91 | 			// get picked up by subsequent buildExportQueue calls, forever.
 92 | 			if _, err = os.Stat(subDestPath); os.IsNotExist(err) {
 93 | 				err = os.MkdirAll(subDestPath, os.ModeDir)
 94 | 				if err != nil {
 95 | 					return nil, err
 96 | 				}
 97 | 			}
 98 | 		}
 99 | 	}
100 | 
101 | 	return exportJobs, nil
102 | }
103 | 
```

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

```go
  1 | package process
  2 | 
  3 | import (
  4 | 	"image"
  5 | 	"math"
  6 | 
  7 | 	"github.com/charmbracelet/lipgloss"
  8 | 	"github.com/lucasb-eyer/go-colorful"
  9 | 	"github.com/makeworld-the-better-one/dither/v2"
 10 | 	"github.com/nfnt/resize"
 11 | 
 12 | 	"github.com/Zebbeni/ansizalizer/controls/settings/characters"
 13 | 	"github.com/Zebbeni/ansizalizer/controls/settings/size"
 14 | )
 15 | 
 16 | func (m Renderer) processCustom(input image.Image) string {
 17 | 	imgW, imgH := float32(input.Bounds().Dx()), float32(input.Bounds().Dy())
 18 | 
 19 | 	dimensionType, width, height, charRatio := m.Settings.Size.Info()
 20 | 	if dimensionType == size.Fit {
 21 | 		fitHeight := float32(width) * (imgH / imgW) * float32(charRatio)
 22 | 		fitWidth := (float32(height) * (imgW / imgH)) / float32(charRatio)
 23 | 		if fitHeight > float32(height) {
 24 | 			width = int(fitWidth)
 25 | 		} else {
 26 | 			height = int(fitHeight)
 27 | 		}
 28 | 	}
 29 | 
 30 | 	resizeFunc := m.Settings.Advanced.SamplingFunction()
 31 | 	refImg := resize.Resize(uint(width)*2, uint(height)*2, input, resizeFunc)
 32 | 
 33 | 	isTrueColor, _, palette := m.Settings.Colors.GetSelected()
 34 | 	isPaletted := !isTrueColor
 35 | 
 36 | 	doDither, doSerpentine, matrix := m.Settings.Advanced.Dithering()
 37 | 	if doDither && isPaletted {
 38 | 		ditherer := dither.NewDitherer(palette.Colors())
 39 | 		ditherer.Matrix = matrix
 40 | 		if doSerpentine {
 41 | 			ditherer.Serpentine = true
 42 | 		}
 43 | 		refImg = ditherer.Dither(refImg)
 44 | 	}
 45 | 
 46 | 	_, _, useFgBg, chars := m.Settings.Characters.Selected()
 47 | 	if len(chars) == 0 {
 48 | 		return "Enter at least one custom character"
 49 | 	}
 50 | 
 51 | 	content := ""
 52 | 	rows := make([]string, height)
 53 | 	row := make([]string, width)
 54 | 
 55 | 	for y := 0; y < height*2; y += 2 {
 56 | 		for x := 0; x < width*2; x += 2 {
 57 | 			r1, _ := colorful.MakeColor(refImg.At(x, y))
 58 | 			r2, _ := colorful.MakeColor(refImg.At(x+1, y))
 59 | 			r3, _ := colorful.MakeColor(refImg.At(x, y+1))
 60 | 			r4, _ := colorful.MakeColor(refImg.At(x+1, y+1))
 61 | 
 62 | 			if useFgBg == characters.TwoColor {
 63 | 				fg, bg, brightness := m.fgBgBrightness(r1, r2, r3, r4)
 64 | 
 65 | 				lipFg := lipgloss.Color(fg.Hex())
 66 | 				lipBg := lipgloss.Color(bg.Hex())
 67 | 				style := lipgloss.NewStyle().Foreground(lipFg).Background(lipBg).Bold(true)
 68 | 
 69 | 				index := min(int(brightness*float64(len(chars))), len(chars)-1)
 70 | 				char := chars[index]
 71 | 				charString := string(char)
 72 | 
 73 | 				row[x/2] = style.Render(charString)
 74 | 			} else {
 75 | 				fg := m.avgColTrue(r1, r2, r3, r4)
 76 | 				brightness := math.Min(1.0, math.Abs(fg.DistanceLuv(black)))
 77 | 				if isPaletted {
 78 | 					fg, _ = colorful.MakeColor(palette.Colors().Convert(fg))
 79 | 				}
 80 | 				lipFg := lipgloss.Color(fg.Hex())
 81 | 				style := lipgloss.NewStyle().Foreground(lipFg).Bold(true)
 82 | 				index := min(int(brightness*float64(len(chars))), len(chars)-1)
 83 | 				char := chars[index]
 84 | 				charString := string(char)
 85 | 				row[x/2] = style.Render(charString)
 86 | 			}
 87 | 		}
 88 | 		rows[y/2] = lipgloss.JoinHorizontal(lipgloss.Top, row...)
 89 | 	}
 90 | 	content += lipgloss.JoinVertical(lipgloss.Left, rows...)
 91 | 	return content
 92 | }
 93 | 
 94 | func min(a, b int) int {
 95 | 	if a < b {
 96 | 		return a
 97 | 	}
 98 | 	return b
 99 | }
100 | 
```

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

```go
  1 | package characters
  2 | 
  3 | import (
  4 | 	"strings"
  5 | 
  6 | 	"github.com/charmbracelet/lipgloss"
  7 | 
  8 | 	"github.com/Zebbeni/ansizalizer/style"
  9 | )
 10 | 
 11 | var (
 12 | 	inactiveTabBorder = tabBorderWithBottom("┴", "─", "┴")
 13 | 	activeTabBorder   = tabBorderWithBottom("┘", " ", "└")
 14 | 	docStyle          = lipgloss.NewStyle().Padding(0)
 15 | 	inactiveTabStyle  = lipgloss.NewStyle().Border(inactiveTabBorder, true)
 16 | 	activeTabStyle    = lipgloss.NewStyle().Border(activeTabBorder, true)
 17 | 	focusTabStyle     = activeTabStyle.Copy().BorderForeground(style.SelectedColor1)
 18 | 	windowStyle       = lipgloss.NewStyle().Align(lipgloss.Center).Border(lipgloss.NormalBorder()).UnsetBorderTop().Padding(1, 0)
 19 | )
 20 | 
 21 | func (m Model) drawCharTabs() string {
 22 | 	doc := strings.Builder{}
 23 | 	var renderedTabs []string
 24 | 	tabs := []State{Ascii, Unicode, Custom}
 25 | 
 26 | 	borderColor := style.DimmedColor2
 27 | 	if m.IsActive {
 28 | 		borderColor = style.NormalColor1
 29 | 	}
 30 | 
 31 | 	for i, t := range tabs {
 32 | 		var tabStyle lipgloss.Style
 33 | 
 34 | 		isFirst := i == 0
 35 | 		isLast := i == len(tabs)-1
 36 | 		isActive := m.focus == t
 37 | 		showControls := m.charControls == t
 38 | 
 39 | 		fgColor := style.DimmedColor2
 40 | 		if m.IsActive {
 41 | 			if isActive {
 42 | 				fgColor = style.SelectedColor1
 43 | 			} else {
 44 | 				fgColor = style.DimmedColor1
 45 | 			}
 46 | 		} else {
 47 | 			if isActive {
 48 | 				fgColor = style.NormalColor2
 49 | 			}
 50 | 		}
 51 | 
 52 | 		if showControls {
 53 | 			tabStyle = activeTabStyle.Copy()
 54 | 		} else {
 55 | 			tabStyle = inactiveTabStyle.Copy()
 56 | 		}
 57 | 
 58 | 		border, _, _, _, _ := tabStyle.GetBorder()
 59 | 		if isFirst && showControls {
 60 | 			border.BottomLeft = "│"
 61 | 		} else if isFirst && !showControls {
 62 | 			border.BottomLeft = "├"
 63 | 		} else if isLast && showControls {
 64 | 			border.BottomRight = "└"
 65 | 		} else if isLast && !showControls {
 66 | 			border.BottomRight = "┴"
 67 | 		}
 68 | 
 69 | 		tabStyle = tabStyle.Border(border).BorderForeground(borderColor).Foreground(fgColor)
 70 | 		renderedTabs = append(renderedTabs, tabStyle.Render(stateNames[t]))
 71 | 	}
 72 | 
 73 | 	tabBlock := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...)
 74 | 	extW, extH := max(m.width-lipgloss.Width(tabBlock)-2, 0), 1
 75 | 
 76 | 	border := lipgloss.Border{BottomLeft: "─", Bottom: "─", BottomRight: "┐"}
 77 | 
 78 | 	extendedStyle := windowStyle.Copy().Border(border).BorderForeground(borderColor).Padding(0)
 79 | 	extended := extendedStyle.Copy().Width(extW).Height(extH).Render("")
 80 | 	renderedTabs = append(renderedTabs, extended)
 81 | 
 82 | 	row := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...)
 83 | 	doc.WriteString(row)
 84 | 	doc.WriteString("\n")
 85 | 
 86 | 	charButtons := m.drawCharControls()
 87 | 	doc.WriteString(windowStyle.Copy().BorderForeground(borderColor).Width(lipgloss.Width(row) - windowStyle.GetHorizontalFrameSize()).Render(charButtons))
 88 | 	return docStyle.Render(doc.String())
 89 | }
 90 | 
 91 | func max(a, b int) int {
 92 | 	if a > b {
 93 | 		return a
 94 | 	}
 95 | 	return b
 96 | }
 97 | 
 98 | func min(a, b int) int {
 99 | 	if a < b {
100 | 		return a
101 | 	}
102 | 	return b
103 | }
104 | 
105 | func tabBorderWithBottom(left, middle, right string) lipgloss.Border {
106 | 	border := lipgloss.RoundedBorder()
107 | 	border.BottomLeft = left
108 | 	border.Bottom = middle
109 | 	border.BottomRight = right
110 | 	return border
111 | }
112 | 
```

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

```go
 1 | package size
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/bubbles/cursor"
 5 | 	"github.com/charmbracelet/lipgloss"
 6 | )
 7 | 
 8 | var (
 9 | 	stateOrder = []State{FitButton, StretchButton}
10 | 	stateNames = map[State]string{
11 | 		FitButton:     "Fit",
12 | 		StretchButton: "Stretch",
13 | 		WidthForm:     "Width",
14 | 		HeightForm:    "Height",
15 | 		CharRatioForm: "Char Size Ratio (Width/Height)",
16 | 	}
17 | 
18 | 	inputStyle = lipgloss.NewStyle().Width(14).AlignHorizontal(lipgloss.Left)
19 | 
20 | 	activeColor = lipgloss.Color("#aaaaaa")
21 | 	focusColor  = lipgloss.Color("#ffffff")
22 | 	normalColor = lipgloss.Color("#555555")
23 | 	titleStyle  = lipgloss.NewStyle().
24 | 			Foreground(lipgloss.Color("#888888"))
25 | )
26 | 
27 | func (m Model) drawButtons() string {
28 | 	buttons := make([]string, len(stateOrder))
29 | 	for i, state := range stateOrder {
30 | 		styleColor := normalColor
31 | 		if m.IsActive {
32 | 			if state == m.focus {
33 | 				styleColor = focusColor
34 | 			} else if state == m.active {
35 | 				styleColor = activeColor
36 | 			}
37 | 		}
38 | 		style := lipgloss.NewStyle().
39 | 			BorderStyle(lipgloss.RoundedBorder()).
40 | 			BorderForeground(styleColor).
41 | 			Foreground(styleColor)
42 | 		buttons[i] = style.Copy().Width(12).AlignHorizontal(lipgloss.Center).Render(stateNames[state])
43 | 	}
44 | 	return lipgloss.JoinHorizontal(lipgloss.Left, buttons...)
45 | }
46 | 
47 | func (m Model) drawSizeForms() string {
48 | 	prompt, text := m.getInputColors(WidthForm)
49 | 	m.widthInput.Width = 3
50 | 	m.widthInput.PromptStyle = m.widthInput.PromptStyle.Copy().Foreground(prompt)
51 | 	m.heightInput.TextStyle = m.heightInput.TextStyle.Copy().Foreground(text)
52 | 	if m.widthInput.Focused() {
53 | 		m.widthInput.Cursor.SetMode(cursor.CursorBlink)
54 | 	} else {
55 | 		m.widthInput.Cursor.SetMode(cursor.CursorHide)
56 | 	}
57 | 
58 | 	prompt, text = m.getInputColors(HeightForm)
59 | 	m.heightInput.PromptStyle = m.heightInput.PromptStyle.Copy().Foreground(prompt)
60 | 	m.heightInput.TextStyle = m.heightInput.TextStyle.Copy().Foreground(text)
61 | 	if m.heightInput.Focused() {
62 | 		m.heightInput.Cursor.SetMode(cursor.CursorBlink)
63 | 	} else {
64 | 		m.heightInput.Cursor.SetMode(cursor.CursorHide)
65 | 	}
66 | 
67 | 	width := inputStyle.Render(m.widthInput.View())
68 | 	height := inputStyle.Render(m.heightInput.View())
69 | 
70 | 	return lipgloss.JoinHorizontal(lipgloss.Top, width, height)
71 | }
72 | 
73 | func (m Model) drawCharRatioForm() string {
74 | 	prompt, text := m.getInputColors(CharRatioForm)
75 | 	m.charRatioInput.Width = 30
76 | 	m.charRatioInput.PromptStyle = m.charRatioInput.PromptStyle.Copy().Width(20).Foreground(prompt)
77 | 	m.charRatioInput.TextStyle = m.charRatioInput.TextStyle.Copy().Foreground(text)
78 | 	if m.charRatioInput.Focused() {
79 | 		m.charRatioInput.Cursor.SetMode(cursor.CursorBlink)
80 | 	} else {
81 | 		m.charRatioInput.Cursor.SetMode(cursor.CursorHide)
82 | 	}
83 | 
84 | 	return inputStyle.Copy().Width(28).AlignHorizontal(lipgloss.Left).PaddingTop(1).Render(m.charRatioInput.View())
85 | }
86 | 
87 | func (m Model) getInputColors(state State) (lipgloss.Color, lipgloss.Color) {
88 | 	if m.focus == state {
89 | 		if m.active == state {
90 | 			return activeColor, focusColor
91 | 		} else {
92 | 			return focusColor, activeColor
93 | 		}
94 | 	}
95 | 	return normalColor, normalColor
96 | }
97 | 
```

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

```go
  1 | package adaptive
  2 | 
  3 | import (
  4 | 	"github.com/charmbracelet/bubbles/cursor"
  5 | 	"github.com/charmbracelet/lipgloss"
  6 | 
  7 | 	"github.com/Zebbeni/ansizalizer/style"
  8 | )
  9 | 
 10 | var (
 11 | 	stateOrder = []State{CountForm, IterForm}
 12 | 	stateNames = map[State]string{
 13 | 		CountForm: "Colors",
 14 | 		IterForm:  "Passes",
 15 | 	}
 16 | 
 17 | 	inputStyle = lipgloss.NewStyle().Width(13).AlignHorizontal(lipgloss.Left)
 18 | 
 19 | 	activeColor = lipgloss.Color("#aaaaaa")
 20 | 	focusColor  = lipgloss.Color("#ffffff")
 21 | 	normalColor = lipgloss.Color("#555555")
 22 | 	titleStyle  = lipgloss.NewStyle().
 23 | 			Foreground(lipgloss.Color("#888888"))
 24 | )
 25 | 
 26 | func (m Model) drawTitle() string {
 27 | 	title := style.DimmedTitle.Copy().Italic(true).Render("Create palette From image")
 28 | 	return lipgloss.NewStyle().Width(m.width).PaddingBottom(1).AlignHorizontal(lipgloss.Center).Render(title)
 29 | }
 30 | 
 31 | func (m Model) drawInputs() string {
 32 | 	prompt, placeholder := m.getInputColors(CountForm)
 33 | 
 34 | 	m.countInput.PromptStyle = m.countInput.PromptStyle.Copy().Foreground(prompt)
 35 | 	m.countInput.PlaceholderStyle = m.countInput.PlaceholderStyle.Copy().Foreground(placeholder)
 36 | 	if m.countInput.Focused() {
 37 | 		m.countInput.Cursor.SetMode(cursor.CursorBlink)
 38 | 	} else {
 39 | 		m.countInput.Cursor.SetMode(cursor.CursorHide)
 40 | 	}
 41 | 
 42 | 	prompt, placeholder = m.getInputColors(IterForm)
 43 | 	m.iterInput.PromptStyle = m.countInput.PromptStyle.Copy().Foreground(prompt)
 44 | 	m.iterInput.PlaceholderStyle = m.countInput.PlaceholderStyle.Copy().Foreground(placeholder)
 45 | 	if m.iterInput.Focused() {
 46 | 		m.iterInput.Cursor.SetMode(cursor.CursorBlink)
 47 | 	} else {
 48 | 		m.iterInput.Cursor.SetMode(cursor.CursorHide)
 49 | 	}
 50 | 
 51 | 	countInput := inputStyle.Render(m.countInput.View())
 52 | 	iterInput := inputStyle.Render(m.iterInput.View())
 53 | 
 54 | 	return lipgloss.JoinHorizontal(lipgloss.Top, countInput, iterInput)
 55 | }
 56 | 
 57 | func (m Model) drawGenerateButton() string {
 58 | 	styleColor := normalColor
 59 | 	if m.IsActive && m.focus == Generate {
 60 | 		styleColor = focusColor
 61 | 	} else if m.active == Generate {
 62 | 		styleColor = activeColor
 63 | 	}
 64 | 
 65 | 	style := lipgloss.NewStyle().
 66 | 		Width(m.width - 4).
 67 | 		AlignHorizontal(lipgloss.Center).
 68 | 		BorderStyle(lipgloss.RoundedBorder()).
 69 | 		BorderForeground(styleColor).
 70 | 		Foreground(styleColor)
 71 | 
 72 | 	button := style.Render("Generate New")
 73 | 	return lipgloss.NewStyle().Width(m.width - 2).AlignHorizontal(lipgloss.Center).Render(button)
 74 | }
 75 | 
 76 | // TODO: This is almost the same as drawGenerateButton. See if we can generalize
 77 | func (m Model) drawSaveButton() string {
 78 | 	styleColor := normalColor
 79 | 	if m.IsActive && m.focus == Save {
 80 | 		styleColor = focusColor
 81 | 	} else if m.active == Save {
 82 | 		styleColor = activeColor
 83 | 	}
 84 | 
 85 | 	style := lipgloss.NewStyle().
 86 | 		Width(m.width - 4).
 87 | 		AlignHorizontal(lipgloss.Center).
 88 | 		PaddingTop(1).
 89 | 		Foreground(styleColor)
 90 | 
 91 | 	button := style.Render("Save to .hex File")
 92 | 	return lipgloss.NewStyle().Width(m.width - 2).AlignHorizontal(lipgloss.Center).Render(button)
 93 | }
 94 | 
 95 | func (m Model) getInputColors(state State) (lipgloss.Color, lipgloss.Color) {
 96 | 	if m.IsActive {
 97 | 		if m.focus == state {
 98 | 			return focusColor, focusColor
 99 | 		} else if m.active == state {
100 | 			return activeColor, activeColor
101 | 		}
102 | 	}
103 | 	return normalColor, normalColor
104 | }
105 | 
```

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

```go
  1 | package advanced
  2 | 
  3 | import (
  4 | 	"strings"
  5 | 
  6 | 	"github.com/charmbracelet/lipgloss"
  7 | 
  8 | 	"github.com/Zebbeni/ansizalizer/style"
  9 | )
 10 | 
 11 | var (
 12 | 	inactiveTabBorder = tabBorderWithBottom("┴", "─", "┴")
 13 | 	activeTabBorder   = tabBorderWithBottom("┘", " ", "└")
 14 | 	docStyle          = lipgloss.NewStyle().Padding(0)
 15 | 	inactiveTabStyle  = lipgloss.NewStyle().Border(inactiveTabBorder, true)
 16 | 	activeTabStyle    = lipgloss.NewStyle().Border(activeTabBorder, true)
 17 | 	focusTabStyle     = activeTabStyle.Copy().BorderForeground(style.SelectedColor1)
 18 | 	windowStyle       = lipgloss.NewStyle().Align(lipgloss.Left).Border(lipgloss.NormalBorder()).UnsetBorderTop().Padding(1, 0)
 19 | 	stateNames        = map[State]string{Sampling: "Sampling", Dithering: "Dithering"}
 20 | )
 21 | 
 22 | func (m Model) drawTabs() string {
 23 | 	doc := strings.Builder{}
 24 | 	var renderedTabs []string
 25 | 	tabs := []State{Sampling, Dithering}
 26 | 
 27 | 	borderColor := style.DimmedColor2
 28 | 	if m.IsActive {
 29 | 		borderColor = style.NormalColor1
 30 | 	}
 31 | 
 32 | 	for i, t := range tabs {
 33 | 		var tabStyle lipgloss.Style
 34 | 		isFirst, isLast, isActive, isActiveTab := i == 0, i == len(tabs)-1, m.focus == t, m.activeTab == t
 35 | 
 36 | 		fgColor := style.DimmedColor2
 37 | 		if m.IsActive {
 38 | 			if isActive {
 39 | 				fgColor = style.SelectedColor1
 40 | 			} else {
 41 | 				fgColor = style.DimmedColor1
 42 | 			}
 43 | 		} else {
 44 | 			if isActive {
 45 | 				fgColor = style.NormalColor2
 46 | 			}
 47 | 		}
 48 | 
 49 | 		if m.activeTab == t {
 50 | 			tabStyle = activeTabStyle.Copy()
 51 | 		} else {
 52 | 			tabStyle = inactiveTabStyle.Copy()
 53 | 		}
 54 | 
 55 | 		border, _, _, _, _ := tabStyle.GetBorder()
 56 | 		if isFirst && isActiveTab {
 57 | 			border.BottomLeft = "│"
 58 | 		} else if isFirst && !isActiveTab {
 59 | 			border.BottomLeft = "├"
 60 | 		} else if isLast && isActiveTab {
 61 | 			border.BottomRight = "└"
 62 | 		} else if isLast && !isActiveTab {
 63 | 			border.BottomRight = "┴"
 64 | 		}
 65 | 
 66 | 		tabStyle = tabStyle.Border(border).BorderForeground(borderColor).Foreground(fgColor)
 67 | 		renderedTabs = append(renderedTabs, tabStyle.Render(stateNames[t]))
 68 | 	}
 69 | 
 70 | 	tabBlock := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...)
 71 | 	extW, extH := max(m.width-lipgloss.Width(tabBlock)-2, 0), 1
 72 | 
 73 | 	border := lipgloss.Border{BottomLeft: "─", Bottom: "─", BottomRight: "┐"}
 74 | 
 75 | 	extendedStyle := windowStyle.Copy().Border(border).BorderForeground(borderColor).Padding(0)
 76 | 	extended := extendedStyle.Copy().Width(extW).Height(extH).Render("")
 77 | 	renderedTabs = append(renderedTabs, extended)
 78 | 
 79 | 	row := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...)
 80 | 	doc.WriteString(row)
 81 | 	doc.WriteString("\n")
 82 | 
 83 | 	content := m.drawTabContent()
 84 | 	doc.WriteString(windowStyle.Copy().BorderForeground(borderColor).Width(lipgloss.Width(row) - windowStyle.GetHorizontalFrameSize()).Render(content))
 85 | 	return docStyle.Render(doc.String())
 86 | }
 87 | 
 88 | func (m Model) drawTabContent() string {
 89 | 	switch m.activeTab {
 90 | 	case Sampling:
 91 | 		return m.sampling.View()
 92 | 	case Dithering:
 93 | 		return m.dithering.View()
 94 | 	}
 95 | 	return ""
 96 | }
 97 | 
 98 | func max(a, b int) int {
 99 | 	if a > b {
100 | 		return a
101 | 	}
102 | 	return b
103 | }
104 | 
105 | func min(a, b int) int {
106 | 	if a < b {
107 | 		return a
108 | 	}
109 | 	return b
110 | }
111 | 
112 | func tabBorderWithBottom(left, middle, right string) lipgloss.Border {
113 | 	border := lipgloss.RoundedBorder()
114 | 	border.BottomLeft = left
115 | 	border.Bottom = middle
116 | 	border.BottomRight = right
117 | 	return border
118 | }
119 | 
```

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

```go
  1 | package adaptive
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"image/color"
  6 | 	"os"
  7 | 	"path/filepath"
  8 | 
  9 | 	"github.com/charmbracelet/bubbles/key"
 10 | 	tea "github.com/charmbracelet/bubbletea"
 11 | 
 12 | 	"github.com/Zebbeni/ansizalizer/event"
 13 | )
 14 | 
 15 | type Direction int
 16 | 
 17 | const (
 18 | 	Left Direction = iota
 19 | 	Right
 20 | 	Up
 21 | 	Down
 22 | )
 23 | 
 24 | var navMap = map[Direction]map[State]State{
 25 | 	Right: {CountForm: IterForm},
 26 | 	Left:  {IterForm: CountForm},
 27 | 	Up:    {Generate: CountForm, Save: Generate},
 28 | 	Down:  {CountForm: Generate, IterForm: Generate, Generate: Save},
 29 | }
 30 | 
 31 | func (m Model) handleEsc() (Model, tea.Cmd) {
 32 | 	m.ShouldClose = true
 33 | 	m.IsSelected = false
 34 | 	return m, nil
 35 | }
 36 | 
 37 | func (m Model) handleEnter() (Model, tea.Cmd) {
 38 | 	m.active = m.focus
 39 | 	m.IsSelected = true
 40 | 	switch m.active {
 41 | 	case CountForm:
 42 | 		m.countInput.Focus()
 43 | 		return m, nil
 44 | 	case IterForm:
 45 | 		m.iterInput.Focus()
 46 | 		return m, nil
 47 | 	case Save:
 48 | 		return m.savePaletteFile()
 49 | 	}
 50 | 	return m, event.StartAdaptingCmd
 51 | }
 52 | 
 53 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
 54 | 	var cmd tea.Cmd
 55 | 	switch {
 56 | 	case key.Matches(msg, event.KeyMap.Right):
 57 | 		if next, hasNext := navMap[Right][m.focus]; hasNext {
 58 | 			m.focus = next
 59 | 		}
 60 | 	case key.Matches(msg, event.KeyMap.Left):
 61 | 		if next, hasNext := navMap[Left][m.focus]; hasNext {
 62 | 			m.focus = next
 63 | 		}
 64 | 	case key.Matches(msg, event.KeyMap.Down):
 65 | 		if next, hasNext := navMap[Down][m.focus]; hasNext {
 66 | 			m.focus = next
 67 | 		} else {
 68 | 			m.IsSelected = false
 69 | 			m.ShouldUnfocus = true
 70 | 		}
 71 | 	case key.Matches(msg, event.KeyMap.Up):
 72 | 		if next, hasNext := navMap[Up][m.focus]; hasNext {
 73 | 			m.focus = next
 74 | 		} else {
 75 | 			m.IsSelected = false
 76 | 			m.ShouldUnfocus = true
 77 | 		}
 78 | 	}
 79 | 
 80 | 	return m, cmd
 81 | }
 82 | 
 83 | func (m Model) handleCountUpdate(msg tea.Msg) (Model, tea.Cmd) {
 84 | 	if keyMsg, ok := msg.(tea.KeyMsg); ok {
 85 | 		switch {
 86 | 		case key.Matches(keyMsg, event.KeyMap.Enter):
 87 | 			m.IsSelected = true
 88 | 			m.countInput.Blur()
 89 | 			return m, event.StartAdaptingCmd
 90 | 		case key.Matches(keyMsg, event.KeyMap.Esc):
 91 | 			m.countInput.Blur()
 92 | 		}
 93 | 	}
 94 | 	var cmd tea.Cmd
 95 | 	m.countInput, cmd = m.countInput.Update(msg)
 96 | 	return m, cmd
 97 | }
 98 | 
 99 | func (m Model) handleIterUpdate(msg tea.Msg) (Model, tea.Cmd) {
100 | 	if keyMsg, ok := msg.(tea.KeyMsg); ok {
101 | 		switch {
102 | 		case key.Matches(keyMsg, event.KeyMap.Enter):
103 | 			m.IsSelected = true
104 | 			m.iterInput.Blur()
105 | 			return m, event.StartAdaptingCmd
106 | 		case key.Matches(keyMsg, event.KeyMap.Esc):
107 | 			m.iterInput.Blur()
108 | 		}
109 | 	}
110 | 	var cmd tea.Cmd
111 | 	m.iterInput, cmd = m.iterInput.Update(msg)
112 | 	return m, cmd
113 | }
114 | 
115 | func (m Model) savePaletteFile() (Model, tea.Cmd) {
116 | 	filename := fmt.Sprintf("%s.hex", m.palette.Name())
117 | 
118 | 	f, err := os.Create(filename)
119 | 
120 | 	if err != nil {
121 | 		return m, event.BuildDisplayCmd("error saving palette file")
122 | 	}
123 | 
124 | 	defer f.Close()
125 | 
126 | 	var hexStrings string
127 | 
128 | 	for _, c := range m.palette.Colors() {
129 | 		hexStrings += hexColor(c) + "\n"
130 | 
131 | 		if err != nil {
132 | 			return m, event.BuildDisplayCmd("error writing to palette file")
133 | 		}
134 | 	}
135 | 
136 | 	_, err = f.WriteString(hexStrings)
137 | 
138 | 	dir, _ := os.Getwd()
139 | 	msg := fmt.Sprintf("saved %s in /%s", filename, filepath.Base(dir))
140 | 	return m, event.BuildDisplayCmd(msg)
141 | }
142 | 
143 | func hexColor(c color.Color) string {
144 | 	rgba := color.RGBAModel.Convert(c).(color.RGBA)
145 | 	return fmt.Sprintf("%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B)
146 | }
147 | 
```
Page 1/2FirstPrevNextLast