#
tokens: 28030/50000 14/88 files (page 2/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 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

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

```go
  1 | package size
  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: {FitButton: StretchButton, WidthForm: HeightForm},
 21 | 	Left:  {StretchButton: FitButton, HeightForm: WidthForm},
 22 | 	Up:    {WidthForm: FitButton, HeightForm: StretchButton, CharRatioForm: HeightForm},
 23 | 	Down:  {FitButton: WidthForm, StretchButton: HeightForm, WidthForm: CharRatioForm, HeightForm: CharRatioForm},
 24 | }
 25 | 
 26 | func (m Model) handleEsc() (Model, tea.Cmd) {
 27 | 	m.ShouldClose = true
 28 | 	return m, nil
 29 | }
 30 | 
 31 | func (m Model) handleEnter() (Model, tea.Cmd) {
 32 | 	if m.active == m.focus {
 33 | 		if m.active == FitButton || m.active == StretchButton {
 34 | 			m.ShouldClose = true
 35 | 			return m, nil
 36 | 		} else {
 37 | 			switch m.active {
 38 | 			case WidthForm:
 39 | 				m.widthInput.Blur()
 40 | 				m.active = None
 41 | 			case HeightForm:
 42 | 				m.heightInput.Blur()
 43 | 				m.active = None
 44 | 			case CharRatioForm:
 45 | 				m.charRatioInput.Blur()
 46 | 				m.active = None
 47 | 			}
 48 | 			return m, event.StartRenderToViewCmd
 49 | 		}
 50 | 	}
 51 | 
 52 | 	m.active = m.focus
 53 | 	switch m.active {
 54 | 	case FitButton:
 55 | 		m.mode = Fit
 56 | 	case StretchButton:
 57 | 		m.mode = Stretch
 58 | 	case WidthForm:
 59 | 		m.widthInput.Focus()
 60 | 	case HeightForm:
 61 | 		m.heightInput.Focus()
 62 | 	case CharRatioForm:
 63 | 		m.charRatioInput.Focus()
 64 | 	}
 65 | 	return m, event.StartRenderToViewCmd
 66 | }
 67 | 
 68 | func (m Model) handleWidthUpdate(msg tea.Msg) (Model, tea.Cmd) {
 69 | 	if keyMsg, ok := msg.(tea.KeyMsg); ok {
 70 | 		switch {
 71 | 		case key.Matches(keyMsg, event.KeyMap.Enter):
 72 | 			m.widthInput.Blur()
 73 | 			return m, event.StartRenderToViewCmd
 74 | 		case key.Matches(keyMsg, event.KeyMap.Esc):
 75 | 			m.widthInput.Blur()
 76 | 		}
 77 | 	}
 78 | 	var cmd tea.Cmd
 79 | 	m.widthInput, cmd = m.widthInput.Update(msg)
 80 | 	return m, cmd
 81 | }
 82 | 
 83 | func (m Model) handleHeightUpdate(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.heightInput.Blur()
 88 | 			return m, event.StartRenderToViewCmd
 89 | 		case key.Matches(keyMsg, event.KeyMap.Esc):
 90 | 			m.heightInput.Blur()
 91 | 		}
 92 | 	}
 93 | 	var cmd tea.Cmd
 94 | 	m.heightInput, cmd = m.heightInput.Update(msg)
 95 | 	return m, cmd
 96 | }
 97 | 
 98 | func (m Model) handleCharRatioUpdate(msg tea.Msg) (Model, tea.Cmd) {
 99 | 	if keyMsg, ok := msg.(tea.KeyMsg); ok {
100 | 		switch {
101 | 		case key.Matches(keyMsg, event.KeyMap.Enter):
102 | 			m.charRatioInput.Blur()
103 | 			return m, event.StartRenderToViewCmd
104 | 		case key.Matches(keyMsg, event.KeyMap.Esc):
105 | 			m.charRatioInput.Blur()
106 | 		}
107 | 	}
108 | 	var cmd tea.Cmd
109 | 	m.charRatioInput, cmd = m.charRatioInput.Update(msg)
110 | 	return m, cmd
111 | }
112 | 
113 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
114 | 	var cmd tea.Cmd
115 | 	switch {
116 | 	case key.Matches(msg, event.KeyMap.Right):
117 | 		if next, hasNext := navMap[Right][m.focus]; hasNext {
118 | 			m.focus = next
119 | 		}
120 | 	case key.Matches(msg, event.KeyMap.Left):
121 | 		if next, hasNext := navMap[Left][m.focus]; hasNext {
122 | 			m.focus = next
123 | 		}
124 | 	case key.Matches(msg, event.KeyMap.Up):
125 | 		if next, hasNext := navMap[Up][m.focus]; hasNext {
126 | 			m.focus = next
127 | 		} else {
128 | 			m.ShouldClose = true
129 | 		}
130 | 	case key.Matches(msg, event.KeyMap.Down):
131 | 		if next, hasNext := navMap[Down][m.focus]; hasNext {
132 | 			m.focus = next
133 | 		} else {
134 | 			m.ShouldClose = true
135 | 		}
136 | 	}
137 | 
138 | 	return m, cmd
139 | }
140 | 
```

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

```go
  1 | package lospec
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 
  6 | 	"github.com/charmbracelet/bubbles/key"
  7 | 	"github.com/charmbracelet/bubbles/list"
  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 | 	"github.com/Zebbeni/ansizalizer/style"
 15 | )
 16 | 
 17 | type State int
 18 | 
 19 | const (
 20 | 	CountForm State = iota
 21 | 	TagForm
 22 | 	FilterExact
 23 | 	FilterMax
 24 | 	FilterMin
 25 | 	SortAlphabetical
 26 | 	SortDownloads
 27 | 	SortNewest
 28 | 	List
 29 | )
 30 | 
 31 | type Model struct {
 32 | 	focus  State
 33 | 	active State
 34 | 
 35 | 	countInput textinput.Model
 36 | 	tagInput   textinput.Model
 37 | 	filterType State
 38 | 	sortType   State
 39 | 
 40 | 	paletteList            list.Model
 41 | 	palettes               []list.Item
 42 | 	palette                palette.Model
 43 | 	isPaletteListAllocated bool
 44 | 	highestPageRequested   int
 45 | 	requestID              int
 46 | 
 47 | 	ShouldClose   bool
 48 | 	ShouldUnfocus bool
 49 | 	IsActive      bool
 50 | 	IsSelected    bool // true if we've selected something (ie. render w/ lospec)
 51 | 
 52 | 	width             int
 53 | 	didInitializeList bool
 54 | }
 55 | 
 56 | func New(w int) Model {
 57 | 	return Model{
 58 | 		focus: CountForm,
 59 | 
 60 | 		countInput: newInput(CountForm, "16"),
 61 | 		tagInput:   newInput(TagForm, ""),
 62 | 		filterType: FilterMin,
 63 | 		sortType:   SortDownloads,
 64 | 
 65 | 		isPaletteListAllocated: false,
 66 | 		highestPageRequested:   0,
 67 | 		requestID:              0,
 68 | 
 69 | 		ShouldClose:   false,
 70 | 		ShouldUnfocus: false,
 71 | 		IsActive:      false,
 72 | 		IsSelected:    false,
 73 | 
 74 | 		width: w,
 75 | 	}
 76 | }
 77 | 
 78 | func (m Model) Init() tea.Cmd {
 79 | 	return nil
 80 | }
 81 | 
 82 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
 83 | 	switch m.active {
 84 | 	case CountForm:
 85 | 		if m.countInput.Focused() {
 86 | 			return m.handleCountFormUpdate(msg)
 87 | 		}
 88 | 	case TagForm:
 89 | 		if m.tagInput.Focused() {
 90 | 			return m.handleTagFormUpdate(msg)
 91 | 		}
 92 | 	}
 93 | 
 94 | 	switch msg := msg.(type) {
 95 | 	case event.LospecResponseMsg:
 96 | 		return m.handleLospecResponse(msg)
 97 | 	case tea.KeyMsg:
 98 | 		if m.focus == List {
 99 | 			return m.handleListUpdate(msg)
100 | 		}
101 | 		switch {
102 | 		case key.Matches(msg, event.KeyMap.Enter):
103 | 			return m.handleEnter()
104 | 		case key.Matches(msg, event.KeyMap.Nav):
105 | 			return m.handleNav(msg)
106 | 		case key.Matches(msg, event.KeyMap.Esc):
107 | 			return m.handleEsc()
108 | 		}
109 | 	}
110 | 
111 | 	return m, nil
112 | }
113 | 
114 | // View draws a control panel like this:
115 | //
116 | // Colors ___ |Exact Max Min
117 | // Tag _____________________
118 | // Sort By |A-Z Downloads New
119 | //
120 | // (Palette List)
121 | // <palette name>
122 | // <preview>
123 | // <...>
124 | // <...>
125 | // ..
126 | func (m Model) View() string {
127 | 	title := m.drawTitle()
128 | 	colorsInput := m.drawColorsInput()
129 | 	filters := m.drawFilterButtons()
130 | 	colorFilters := lipgloss.JoinHorizontal(lipgloss.Left, colorsInput, filters)
131 | 	tagInput := m.drawTagInput()
132 | 	sortButtons := m.drawSortButtons()
133 | 
134 | 	results := fmt.Sprintf("%d results found\npage %d of %d", len(m.paletteList.Items()), m.paletteList.Paginator.Page, m.paletteList.Paginator.TotalPages)
135 | 	results = style.DimmedTitle.Copy().Width(m.width).Height(2).AlignHorizontal(lipgloss.Center).Padding(1, 0, 1, 0).Render(results)
136 | 	paletteList := m.paletteList.View()
137 | 	if len(m.paletteList.Items()) == 0 {
138 | 		paletteList = ""
139 | 	}
140 | 	return lipgloss.JoinVertical(lipgloss.Top, title, colorFilters, tagInput, sortButtons, results, paletteList)
141 | }
142 | 
143 | func (m Model) LoadInitial() (Model, tea.Cmd) {
144 | 	return m.searchLospec(0)
145 | }
146 | 
147 | func (m Model) GetCurrent() palette.Model {
148 | 	return m.palette
149 | }
150 | 
```

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

```go
  1 | package characters
  2 | 
  3 | import (
  4 | 	"github.com/charmbracelet/lipgloss"
  5 | 
  6 | 	"github.com/Zebbeni/ansizalizer/style"
  7 | )
  8 | 
  9 | var (
 10 | 	stateOrder         = []State{Ascii, Unicode, Custom}
 11 | 	asciiButtonOrder   = []State{AsciiAz, AsciiNums, AsciiSpec, AsciiAll}
 12 | 	unicodeButtonOrder = []State{UnicodeFull, UnicodeHalf, UnicodeQuart, UnicodeShadeLight, UnicodeShadeMed, UnicodeShadeHeavy}
 13 | 
 14 | 	stateNames = map[State]string{
 15 | 		Ascii:             "Ascii",
 16 | 		Unicode:           "Unicode",
 17 | 		Custom:            "Custom",
 18 | 		AsciiAz:           "AZ",
 19 | 		AsciiNums:         "0-9",
 20 | 		AsciiSpec:         "!$",
 21 | 		AsciiAll:          "All",
 22 | 		UnicodeFull:       "█",
 23 | 		UnicodeHalf:       "▀▄",
 24 | 		UnicodeQuart:      "▞▟",
 25 | 		UnicodeShadeLight: "░",
 26 | 		UnicodeShadeMed:   "▒",
 27 | 		UnicodeShadeHeavy: "▓",
 28 | 		OneColor:          "1 Color",
 29 | 		TwoColor:          "2 Colors",
 30 | 	}
 31 | 
 32 | 	activeColor = lipgloss.Color("#aaaaaa")
 33 | 	focusColor  = lipgloss.Color("#ffffff")
 34 | 	normalColor = lipgloss.Color("#555555")
 35 | 	titleStyle  = lipgloss.NewStyle().
 36 | 			Foreground(lipgloss.Color("#888888"))
 37 | )
 38 | 
 39 | func (m Model) drawCharControls() string {
 40 | 	if m.charControls == Custom {
 41 | 		content := m.drawCustomControls()
 42 | 		return lipgloss.NewStyle().Width(m.width).AlignHorizontal(lipgloss.Left).Render(content)
 43 | 	}
 44 | 
 45 | 	whitespace := 0
 46 | 
 47 | 	var buttonOrder []State
 48 | 	switch m.charControls {
 49 | 	case Ascii:
 50 | 		buttonOrder = asciiButtonOrder
 51 | 	case Unicode:
 52 | 		buttonOrder = unicodeButtonOrder
 53 | 	}
 54 | 
 55 | 	buttons := make([]string, len(buttonOrder))
 56 | 	for i, state := range buttonOrder {
 57 | 		buttonStyle := style.NormalButtonNode
 58 | 		if m.IsActive && state == m.focus {
 59 | 			buttonStyle = style.FocusButtonNode
 60 | 		} else if state == m.asciiMode || state == m.unicodeMode {
 61 | 			buttonStyle = style.ActiveButtonNode
 62 | 		}
 63 | 
 64 | 		buttons[i] = buttonStyle.Copy().Render(stateNames[state])
 65 | 
 66 | 		whitespace += lipgloss.Width(buttons[i])
 67 | 	}
 68 | 
 69 | 	gapSpace := whitespace / (len(buttons))
 70 | 	for i, button := range buttons {
 71 | 		buttons[i] = lipgloss.NewStyle().PaddingRight(gapSpace).Render(button)
 72 | 	}
 73 | 	content := lipgloss.JoinHorizontal(lipgloss.Left, buttons...)
 74 | 
 75 | 	return lipgloss.NewStyle().Width(m.width).AlignHorizontal(lipgloss.Left).Render(content)
 76 | }
 77 | 
 78 | func (m Model) drawCustomControls() string {
 79 | 	nodeStyle := style.NormalButtonNode.Copy().PaddingRight(1)
 80 | 	if m.customInput.Focused() {
 81 | 		nodeStyle = style.ActiveButtonNode.Copy().PaddingRight(1)
 82 | 	} else if m.focus == SymbolsForm {
 83 | 		nodeStyle = style.FocusButtonNode.Copy().PaddingRight(1)
 84 | 	}
 85 | 	m.customInput.PromptStyle = nodeStyle.Copy()
 86 | 	return m.customInput.View()
 87 | }
 88 | 
 89 | func (m Model) drawColorsButtons() string {
 90 | 	title := style.DimmedTitle.Copy().PaddingLeft(1).Render("Colors per Char:")
 91 | 
 92 | 	oneStyle := style.NormalButtonNode
 93 | 	if m.IsActive && OneColor == m.focus {
 94 | 		oneStyle = style.FocusButtonNode
 95 | 	} else if m.useFgBg == OneColor {
 96 | 		oneStyle = style.ActiveButtonNode
 97 | 	}
 98 | 	oneButton := oneStyle.Render("1")
 99 | 	oneButton = lipgloss.NewStyle().Width(5).AlignHorizontal(lipgloss.Center).Render(oneButton)
100 | 
101 | 	twoStyle := style.NormalButtonNode
102 | 	if m.IsActive && TwoColor == m.focus {
103 | 		twoStyle = style.FocusButtonNode
104 | 	} else if m.useFgBg == TwoColor {
105 | 		twoStyle = style.ActiveButtonNode
106 | 	}
107 | 	twoButton := twoStyle.Render("2")
108 | 	twoButton = lipgloss.NewStyle().Width(5).AlignHorizontal(lipgloss.Center).Render(twoButton)
109 | 
110 | 	return lipgloss.JoinHorizontal(lipgloss.Left, title, oneButton, twoButton)
111 | }
112 | 
```

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

```go
  1 | package source
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"path/filepath"
  6 | 
  7 | 	"github.com/charmbracelet/lipgloss"
  8 | 
  9 | 	"github.com/Zebbeni/ansizalizer/style"
 10 | )
 11 | 
 12 | var (
 13 | 	stateNames = map[State]string{
 14 | 		ExpFile:      "Single File",
 15 | 		ExpDirectory: "Directory",
 16 | 	}
 17 | )
 18 | 
 19 | func (m Model) drawExportTypeOptions() string {
 20 | 	widthStyle := lipgloss.NewStyle().Width((m.width / 2) - 2).AlignHorizontal(lipgloss.Center)
 21 | 	optionStyle := style.NormalButton
 22 | 	if ExpFile == m.focus && m.IsActive {
 23 | 		optionStyle = style.FocusButton
 24 | 	} else if m.doExportDirectory == false {
 25 | 		optionStyle = style.ActiveButton
 26 | 	}
 27 | 	singleFileButtonText := widthStyle.Render(stateNames[ExpFile])
 28 | 	singleFileButton := optionStyle.Render(singleFileButtonText)
 29 | 
 30 | 	optionStyle = style.NormalButton
 31 | 	if ExpDirectory == m.focus && m.IsActive {
 32 | 		optionStyle = style.FocusButton
 33 | 	} else if m.doExportDirectory {
 34 | 		optionStyle = style.ActiveButton
 35 | 	}
 36 | 	directoryButtonText := widthStyle.Render(stateNames[ExpDirectory])
 37 | 	directoryButton := optionStyle.Render(directoryButtonText)
 38 | 
 39 | 	return lipgloss.JoinHorizontal(lipgloss.Center, singleFileButton, directoryButton)
 40 | }
 41 | 
 42 | func (m Model) drawSubDirOptions() string {
 43 | 	title := style.DimmedTitle.Copy().Render("Include Subdirectories")
 44 | 
 45 | 	nodeWidthStyle := lipgloss.NewStyle().Width(m.width / 2).AlignHorizontal(lipgloss.Center)
 46 | 
 47 | 	yesStyle := style.NormalButtonNode.Copy()
 48 | 	if m.includeSubdirectories {
 49 | 		yesStyle = style.ActiveButtonNode.Copy()
 50 | 	}
 51 | 	if m.focus == SubDirsYes {
 52 | 		yesStyle = style.FocusButtonNode.Copy()
 53 | 	}
 54 | 	yesNode := nodeWidthStyle.Render(yesStyle.Render("Yes"))
 55 | 
 56 | 	noStyle := style.NormalButtonNode.Copy()
 57 | 	if !m.includeSubdirectories {
 58 | 		noStyle = style.ActiveButtonNode.Copy()
 59 | 	}
 60 | 	if m.focus == SubDirsNo {
 61 | 		noStyle = style.FocusButtonNode.Copy()
 62 | 	}
 63 | 
 64 | 	noStyle.Padding(0)
 65 | 	noNode := nodeWidthStyle.Render(noStyle.Render("No"))
 66 | 
 67 | 	options := lipgloss.JoinHorizontal(lipgloss.Center, yesNode, noNode)
 68 | 
 69 | 	widthStyle := lipgloss.NewStyle().Width(m.width).AlignHorizontal(lipgloss.Left).PaddingBottom(1)
 70 | 	content := lipgloss.JoinVertical(lipgloss.Center, title, options)
 71 | 
 72 | 	return widthStyle.Render(content)
 73 | }
 74 | 
 75 | func (m Model) drawPrompt() string {
 76 | 	return style.DimmedTitle.Copy().AlignHorizontal(lipgloss.Center).Padding(0).Render("Select")
 77 | }
 78 | 
 79 | func (m Model) drawSelected() string {
 80 | 	title := style.DimmedTitle.Copy().Render("Selected")
 81 | 
 82 | 	valueStyle := style.DimmedTitle.Copy()
 83 | 	if Input == m.focus {
 84 | 		if m.IsActive {
 85 | 			valueStyle = style.SelectedTitle.Copy()
 86 | 		} else {
 87 | 			valueStyle = style.NormalTitle.Copy()
 88 | 		}
 89 | 	}
 90 | 	valueStyle.Padding(0, 0, 1, 0)
 91 | 
 92 | 	path := m.Browser.SelectedFile
 93 | 	if m.doExportDirectory {
 94 | 		path = m.Browser.SelectedDir
 95 | 	}
 96 | 
 97 | 	parent := filepath.Base(filepath.Dir(path))
 98 | 	selected := filepath.Base(path)
 99 | 	value := fmt.Sprintf("%s/%s", parent, selected)
100 | 
101 | 	valueRunes := []rune(value)
102 | 	if len(valueRunes) > m.width {
103 | 		value = string(valueRunes[len(valueRunes)-m.width:])
104 | 	}
105 | 
106 | 	valueContent := valueStyle.Render(value)
107 | 
108 | 	widthStyle := lipgloss.NewStyle().Width(m.width).AlignHorizontal(lipgloss.Center)
109 | 	content := lipgloss.JoinVertical(lipgloss.Center, title, valueContent)
110 | 
111 | 	return widthStyle.Render(content)
112 | }
113 | 
114 | func (m Model) drawBrowserTitle() string {
115 | 	if m.doExportDirectory {
116 | 		return style.DimmedTitle.Copy().Padding(0, 2, 1, 2).Render("Select a directory")
117 | 	}
118 | 	return style.DimmedTitle.Copy().Padding(0, 2, 1, 2).Render("Select a .png or .jpg file")
119 | }
120 | 
```

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

```go
  1 | package dithering
  2 | 
  3 | import (
  4 | 	"github.com/charmbracelet/bubbles/list"
  5 | 	"github.com/charmbracelet/lipgloss"
  6 | 	"github.com/makeworld-the-better-one/dither/v2"
  7 | 
  8 | 	"github.com/Zebbeni/ansizalizer/style"
  9 | )
 10 | 
 11 | type MatrixType int
 12 | 
 13 | const (
 14 | 	Atkinson MatrixType = iota
 15 | 	Burkes
 16 | 	FloydSteinberg
 17 | 	FalseFloydSteinberg
 18 | 	JarvisJudiceNinke
 19 | 	Sierra
 20 | 	Sierra2
 21 | 	Sierra3
 22 | 	SierraLite
 23 | 	TwoRowSierra
 24 | 	Sierra2_4A
 25 | 	Simple2D
 26 | 	StevenPigeon
 27 | 	Stucki
 28 | )
 29 | 
 30 | var Matrices = []MatrixType{
 31 | 	Atkinson,
 32 | 	Burkes,
 33 | 	FloydSteinberg,
 34 | 	FalseFloydSteinberg,
 35 | 	JarvisJudiceNinke,
 36 | 	Sierra,
 37 | 	Sierra2,
 38 | 	Sierra3,
 39 | 	SierraLite,
 40 | 	TwoRowSierra,
 41 | 	Sierra2_4A,
 42 | 	Simple2D,
 43 | 	Stucki,
 44 | 	StevenPigeon,
 45 | }
 46 | 
 47 | var nameMap = map[MatrixType]string{
 48 | 	Atkinson:            "Atkinson",
 49 | 	Burkes:              "Burkes",
 50 | 	FloydSteinberg:      "FloydSteinberg",
 51 | 	FalseFloydSteinberg: "FalseFloydSteinberg",
 52 | 	JarvisJudiceNinke:   "JarvisJudiceNinke",
 53 | 	Sierra:              "Sierra",
 54 | 	Sierra2:             "Sierra2",
 55 | 	Sierra3:             "Sierra3",
 56 | 	SierraLite:          "SierraLite",
 57 | 	TwoRowSierra:        "TwoRowSierra",
 58 | 	Sierra2_4A:          "Sierra2_4A",
 59 | 	Simple2D:            "Simple2D",
 60 | 	Stucki:              "Stucki",
 61 | 	StevenPigeon:        "StevenPigeon",
 62 | }
 63 | 
 64 | var errorDiffMatrixMap = map[MatrixType]dither.ErrorDiffusionMatrix{
 65 | 	Atkinson:            dither.Atkinson,
 66 | 	Burkes:              dither.Burkes,
 67 | 	FloydSteinberg:      dither.FloydSteinberg,
 68 | 	FalseFloydSteinberg: dither.FalseFloydSteinberg,
 69 | 	JarvisJudiceNinke:   dither.JarvisJudiceNinke,
 70 | 	Sierra:              dither.Sierra,
 71 | 	Sierra2:             dither.Sierra2,
 72 | 	Sierra3:             dither.Sierra3,
 73 | 	SierraLite:          dither.SierraLite,
 74 | 	TwoRowSierra:        dither.TwoRowSierra,
 75 | 	Sierra2_4A:          dither.Sierra2_4A,
 76 | 	Simple2D:            dither.Simple2D,
 77 | 	Stucki:              dither.Stucki,
 78 | 	StevenPigeon:        dither.StevenPigeon,
 79 | }
 80 | 
 81 | func newMatrixMenu(width int) list.Model {
 82 | 	items := menuItems()
 83 | 	return newMenu(items, width, len(items))
 84 | }
 85 | 
 86 | type item struct {
 87 | 	Type MatrixType
 88 | }
 89 | 
 90 | func (i item) FilterValue() string {
 91 | 	return nameMap[i.Type]
 92 | }
 93 | 
 94 | func (i item) Title() string {
 95 | 	return nameMap[i.Type]
 96 | }
 97 | 
 98 | func (i item) Description() string {
 99 | 	return ""
100 | }
101 | 
102 | func menuItems() []list.Item {
103 | 	items := make([]list.Item, len(Matrices))
104 | 	for i, matrix := range Matrices {
105 | 		items[i] = item{Type: matrix}
106 | 	}
107 | 	return items
108 | }
109 | 
110 | func newMenu(items []list.Item, width, height int) list.Model {
111 | 	l := list.New(items, NewDelegate(false), width, height/2)
112 | 	l.SetShowHelp(false)
113 | 	l.SetFilteringEnabled(false)
114 | 	l.SetShowTitle(false)
115 | 	l.SetShowPagination(true)
116 | 	l.SetShowStatusBar(false)
117 | 
118 | 	l.KeyMap.ForceQuit.Unbind()
119 | 	l.KeyMap.Quit.Unbind()
120 | 
121 | 	return l
122 | }
123 | 
124 | func NewDelegate(isActive bool) list.DefaultDelegate {
125 | 	delegate := list.NewDefaultDelegate()
126 | 	delegate.SetSpacing(0)
127 | 	delegate.ShowDescription = false
128 | 	if isActive {
129 | 		delegate.Styles = ItemStylesActive()
130 | 	} else {
131 | 		delegate.Styles = ItemStylesInactive()
132 | 	}
133 | 	return delegate
134 | }
135 | 
136 | func ItemStylesActive() (s list.DefaultItemStyles) {
137 | 	s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2)
138 | 	s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1).
139 | 		Border(lipgloss.NormalBorder(), false, false, false, true).
140 | 		BorderForeground(style.SelectedColor1)
141 | 	s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0)
142 | 	return s
143 | }
144 | 
145 | func ItemStylesInactive() (s list.DefaultItemStyles) {
146 | 	s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2)
147 | 	s.SelectedTitle = style.NormalTitle.Copy().Padding(0, 1, 0, 2)
148 | 	s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0)
149 | 	return s
150 | }
151 | 
```

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

```go
  1 | package lospec
  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 | 	stateNames = map[State]string{
 12 | 		CountForm:        "Colors",
 13 | 		TagForm:          "Tag",
 14 | 		FilterExact:      "Exact",
 15 | 		FilterMax:        "Max",
 16 | 		FilterMin:        "Min",
 17 | 		SortAlphabetical: "A-Z",
 18 | 		SortDownloads:    "Downloads",
 19 | 		SortNewest:       "Newest",
 20 | 	}
 21 | 
 22 | 	filterOrder = []State{FilterExact, FilterMax, FilterMin}
 23 | 	sortOrder   = []State{SortAlphabetical, SortDownloads, SortNewest}
 24 | 
 25 | 	activeColor = lipgloss.Color("#aaaaaa")
 26 | 	focusColor  = lipgloss.Color("#ffffff")
 27 | 	normalColor = lipgloss.Color("#555555")
 28 | 	titleStyle  = lipgloss.NewStyle().
 29 | 			Foreground(lipgloss.Color("#888888"))
 30 | )
 31 | 
 32 | func (m Model) drawInputs() string {
 33 | 	colorsInput := m.drawColorsInput()
 34 | 	tagInput := m.drawTagInput()
 35 | 
 36 | 	return lipgloss.JoinHorizontal(lipgloss.Left, colorsInput, tagInput)
 37 | }
 38 | 
 39 | func (m Model) drawTitle() string {
 40 | 	title := style.DimmedTitle.Copy().Italic(true).Render("Browse Lospec.com")
 41 | 	return lipgloss.NewStyle().Width(m.width).PaddingBottom(1).AlignHorizontal(lipgloss.Center).Render(title)
 42 | }
 43 | 
 44 | func (m Model) drawColorsInput() string {
 45 | 	prompt, placeholder := m.getInputColors(CountForm)
 46 | 
 47 | 	m.countInput.CharLimit = 3
 48 | 	m.countInput.Width = 3
 49 | 	m.countInput.PromptStyle = m.countInput.PromptStyle.Copy().Foreground(prompt)
 50 | 	m.countInput.TextStyle = m.countInput.TextStyle.Copy().Foreground(prompt).MaxWidth(3)
 51 | 	m.countInput.PlaceholderStyle = m.countInput.PlaceholderStyle.Copy().Foreground(placeholder)
 52 | 	if m.countInput.Focused() {
 53 | 		m.countInput.Cursor.SetMode(cursor.CursorBlink)
 54 | 	} else {
 55 | 		m.countInput.Cursor.SetMode(cursor.CursorHide)
 56 | 	}
 57 | 	return lipgloss.NewStyle().Width(13).Render(m.countInput.View())
 58 | }
 59 | 
 60 | func (m Model) drawTagInput() string {
 61 | 	prompt, placeholder := m.getInputColors(TagForm)
 62 | 
 63 | 	m.tagInput.Width = m.width - 5
 64 | 	m.tagInput.PromptStyle = m.countInput.PromptStyle.Copy().Foreground(prompt)
 65 | 	m.tagInput.PlaceholderStyle = m.countInput.PlaceholderStyle.Copy().Foreground(placeholder)
 66 | 	if m.tagInput.Focused() {
 67 | 		m.tagInput.Cursor.SetMode(cursor.CursorBlink)
 68 | 	} else {
 69 | 		m.tagInput.Cursor.SetMode(cursor.CursorHide)
 70 | 	}
 71 | 	return m.tagInput.View()
 72 | }
 73 | 
 74 | func (m Model) drawFilterButtons() string {
 75 | 	buttons := make([]string, len(filterOrder))
 76 | 	for i, filter := range filterOrder {
 77 | 		buttonStyle := style.NormalButtonNode
 78 | 		if filter == m.focus {
 79 | 			buttonStyle = style.FocusButtonNode
 80 | 		} else if filter == m.filterType {
 81 | 			buttonStyle = style.ActiveButtonNode
 82 | 		}
 83 | 		buttons[i] = buttonStyle.Render(stateNames[filter])
 84 | 	}
 85 | 
 86 | 	return lipgloss.JoinHorizontal(lipgloss.Left, buttons...)
 87 | }
 88 | 
 89 | func (m Model) drawSortButtons() string {
 90 | 	title := style.DimmedTitle.Copy().PaddingLeft(1).Render("Sort:")
 91 | 	buttons := make([]string, len(sortOrder))
 92 | 	for i, sort := range sortOrder {
 93 | 		buttonStyle := style.NormalButtonNode
 94 | 		if sort == m.focus {
 95 | 			buttonStyle = style.FocusButtonNode
 96 | 		} else if sort == m.sortType {
 97 | 			buttonStyle = style.ActiveButtonNode
 98 | 		}
 99 | 		buttons[i] = buttonStyle.Render(stateNames[sort])
100 | 	}
101 | 	buttonContent := lipgloss.JoinHorizontal(lipgloss.Left, buttons...)
102 | 	return lipgloss.JoinHorizontal(lipgloss.Left, title, buttonContent)
103 | }
104 | 
105 | func (m Model) drawPaletteList() string {
106 | 	if len(m.paletteList.Items()) == 0 {
107 | 		return ""
108 | 	}
109 | 
110 | 	return m.paletteList.View()
111 | }
112 | 
113 | func (m Model) getInputColors(state State) (lipgloss.Color, lipgloss.Color) {
114 | 	if m.IsActive {
115 | 		if m.focus == state {
116 | 			return focusColor, focusColor
117 | 		} else if m.active == state {
118 | 			return activeColor, activeColor
119 | 		}
120 | 	}
121 | 	return normalColor, normalColor
122 | }
123 | 
```

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

```go
  1 | package palettes
  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 | 	Down
 16 | 	Up
 17 | )
 18 | 
 19 | var navMap = map[Direction]map[State]State{
 20 | 	Right: {Load: Adapt, Adapt: Lospec},
 21 | 	Left:  {Lospec: Adapt, Adapt: Load},
 22 | 	Down:  {Adapt: AdaptiveControls, Load: LoadControls, Lospec: LospecControls},
 23 | 	Up:    {AdaptiveControls: Adapt, LoadControls: Load, LospecControls: Lospec},
 24 | }
 25 | 
 26 | func (m Model) handleMenuUpdate(msg tea.Msg) (Model, tea.Cmd) {
 27 | 	switch msg := msg.(type) {
 28 | 	case tea.KeyMsg:
 29 | 		switch {
 30 | 		case key.Matches(msg, event.KeyMap.Esc):
 31 | 			return m.handleEsc()
 32 | 		case key.Matches(msg, event.KeyMap.Enter):
 33 | 			return m.handleEnter()
 34 | 		case key.Matches(msg, event.KeyMap.Nav):
 35 | 			return m.handleNav(msg)
 36 | 		}
 37 | 	}
 38 | 	return m, nil
 39 | }
 40 | 
 41 | func (m Model) handleAdaptiveUpdate(msg tea.Msg) (Model, tea.Cmd) {
 42 | 	var cmd tea.Cmd
 43 | 	m.Adapter, cmd = m.Adapter.Update(msg)
 44 | 	if m.Adapter.IsSelected {
 45 | 		m.selected = Adapt
 46 | 	} else if m.Adapter.ShouldUnfocus {
 47 | 		m.Adapter.IsActive = true
 48 | 		m.Adapter.ShouldUnfocus = false
 49 | 		m.focus = Adapt
 50 | 	} else if m.Adapter.ShouldClose {
 51 | 		m.Adapter.IsActive = true
 52 | 		m.Adapter.ShouldClose = false
 53 | 		m.ShouldClose = true
 54 | 	}
 55 | 	return m, cmd
 56 | }
 57 | 
 58 | func (m Model) handleLoaderUpdate(msg tea.Msg) (Model, tea.Cmd) {
 59 | 	var cmd tea.Cmd
 60 | 	m.Loader, cmd = m.Loader.Update(msg)
 61 | 	if m.Loader.IsSelected {
 62 | 		m.selected = Load
 63 | 	}
 64 | 	if m.Loader.ShouldUnfocus {
 65 | 		m.Loader.ShouldUnfocus = false
 66 | 		m.focus = Load
 67 | 	}
 68 | 	return m, cmd
 69 | }
 70 | 
 71 | func (m Model) handleLospecUpdate(msg tea.Msg) (Model, tea.Cmd) {
 72 | 	var cmd tea.Cmd
 73 | 	m.Lospec, cmd = m.Lospec.Update(msg)
 74 | 	if m.Lospec.IsSelected {
 75 | 		m.selected = Lospec
 76 | 	} else if m.Lospec.ShouldUnfocus {
 77 | 		m.Lospec.IsActive = true
 78 | 		m.Lospec.ShouldUnfocus = false
 79 | 		m.focus = Lospec
 80 | 	} else if m.Lospec.ShouldClose {
 81 | 		m.Lospec.IsActive = true
 82 | 		m.Lospec.ShouldClose = false
 83 | 		m.ShouldClose = true
 84 | 	}
 85 | 	return m, cmd
 86 | }
 87 | 
 88 | func (m Model) handleEsc() (Model, tea.Cmd) {
 89 | 	m.ShouldClose = true
 90 | 	return m, nil
 91 | }
 92 | 
 93 | func (m Model) handleEnter() (Model, tea.Cmd) {
 94 | 	m.selected = m.focus
 95 | 	// Kick off a new palette generation before rendering if not done yet.
 96 | 	// Allow the app to trigger a render when the generation is complete.
 97 | 	if m.IsAdaptive() && len(m.Adapter.GetCurrent().Colors()) == 0 {
 98 | 		return m, event.StartAdaptingCmd
 99 | 	}
100 | 	return m, event.StartRenderToViewCmd
101 | }
102 | 
103 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
104 | 	var cmd tea.Cmd
105 | 	switch {
106 | 	case key.Matches(msg, event.KeyMap.Right):
107 | 		if next, hasNext := navMap[Right][m.focus]; hasNext {
108 | 			return m.setFocus(next)
109 | 		}
110 | 	case key.Matches(msg, event.KeyMap.Left):
111 | 		if next, hasNext := navMap[Left][m.focus]; hasNext {
112 | 			return m.setFocus(next)
113 | 		}
114 | 	case key.Matches(msg, event.KeyMap.Down):
115 | 		if next, hasNext := navMap[Down][m.focus]; hasNext {
116 | 			return m.setFocus(next)
117 | 		} else {
118 | 			m.IsActive = false
119 | 			m.ShouldClose = true
120 | 		}
121 | 	case key.Matches(msg, event.KeyMap.Up):
122 | 		if next, hasNext := navMap[Up][m.focus]; hasNext {
123 | 			return m.setFocus(next)
124 | 		} else {
125 | 			m.IsActive = false
126 | 			m.ShouldClose = true
127 | 		}
128 | 	}
129 | 
130 | 	return m, cmd
131 | }
132 | 
133 | func (m Model) setFocus(focus State) (Model, tea.Cmd) {
134 | 	var cmd tea.Cmd
135 | 	m.focus = focus
136 | 
137 | 	switch m.focus {
138 | 	case Adapt:
139 | 		m.controls = Adapt
140 | 	case Load:
141 | 		m.controls = Load
142 | 	case Lospec:
143 | 		m.controls = Lospec
144 | 	case AdaptiveControls:
145 | 		m.Adapter.IsActive = true
146 | 	case LoadControls:
147 | 		m.controls = Load
148 | 	case LospecControls:
149 | 		m.Lospec.IsActive = true
150 | 	}
151 | 
152 | 	if m.controls == Lospec && !m.Lospec.DidInitializeList() {
153 | 		m.Lospec, cmd = m.Lospec.InitializeList()
154 | 	}
155 | 
156 | 	return m, cmd
157 | }
158 | 
```

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

```go
  1 | package characters
  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 | 		Ascii:             Unicode,
 22 | 		Unicode:           Custom,
 23 | 		AsciiAz:           AsciiNums,
 24 | 		AsciiNums:         AsciiSpec,
 25 | 		AsciiSpec:         AsciiAll,
 26 | 		UnicodeFull:       UnicodeHalf,
 27 | 		UnicodeHalf:       UnicodeQuart,
 28 | 		UnicodeQuart:      UnicodeShadeLight,
 29 | 		UnicodeShadeLight: UnicodeShadeMed,
 30 | 		UnicodeShadeMed:   UnicodeShadeHeavy,
 31 | 		OneColor:          TwoColor,
 32 | 	},
 33 | 	Left: {
 34 | 		Unicode:           Ascii,
 35 | 		Custom:            Unicode,
 36 | 		AsciiAll:          AsciiSpec,
 37 | 		AsciiSpec:         AsciiNums,
 38 | 		AsciiNums:         AsciiAz,
 39 | 		UnicodeShadeHeavy: UnicodeShadeMed,
 40 | 		UnicodeShadeMed:   UnicodeShadeLight,
 41 | 		UnicodeShadeLight: UnicodeQuart,
 42 | 		UnicodeQuart:      UnicodeHalf,
 43 | 		UnicodeHalf:       UnicodeFull,
 44 | 		TwoColor:          OneColor,
 45 | 	},
 46 | 	Up: {
 47 | 		Ascii:             OneColor,
 48 | 		Unicode:           OneColor,
 49 | 		Custom:            OneColor,
 50 | 		AsciiAz:           Ascii,
 51 | 		AsciiNums:         Ascii,
 52 | 		AsciiSpec:         Ascii,
 53 | 		AsciiAll:          Ascii,
 54 | 		UnicodeFull:       Unicode,
 55 | 		UnicodeHalf:       Unicode,
 56 | 		UnicodeQuart:      Unicode,
 57 | 		UnicodeShadeLight: Unicode,
 58 | 		UnicodeShadeMed:   Unicode,
 59 | 		UnicodeShadeHeavy: Unicode,
 60 | 		SymbolsForm:       Custom,
 61 | 	},
 62 | 	Down: {
 63 | 		OneColor: Custom,
 64 | 		TwoColor: Custom,
 65 | 		Ascii:    AsciiAz,
 66 | 		Unicode:  UnicodeShadeMed,
 67 | 		Custom:   SymbolsForm,
 68 | 	},
 69 | }
 70 | 
 71 | var (
 72 | 	asciiCharModeMap   = map[State]bool{AsciiAz: true, AsciiNums: true, AsciiSpec: true, AsciiAll: true}
 73 | 	unicodeCharModeMap = map[State]bool{UnicodeFull: true, UnicodeHalf: true, UnicodeQuart: true, UnicodeShadeLight: true, UnicodeShadeMed: true, UnicodeShadeHeavy: true}
 74 | )
 75 | 
 76 | func (m Model) handleSymbolsFormUpdate(msg tea.Msg) (Model, tea.Cmd) {
 77 | 	if keyMsg, ok := msg.(tea.KeyMsg); ok {
 78 | 		switch {
 79 | 		case key.Matches(keyMsg, event.KeyMap.Enter):
 80 | 			m.customInput.Blur()
 81 | 			return m, event.StartRenderToViewCmd
 82 | 		case key.Matches(keyMsg, event.KeyMap.Esc):
 83 | 			m.customInput.Blur()
 84 | 		}
 85 | 	}
 86 | 
 87 | 	var cmd tea.Cmd
 88 | 	m.customInput, cmd = m.customInput.Update(msg)
 89 | 	return m, cmd
 90 | }
 91 | 
 92 | func (m Model) handleEsc() (Model, tea.Cmd) {
 93 | 	m.ShouldClose = true
 94 | 	return m, nil
 95 | }
 96 | 
 97 | func (m Model) handleEnter() (Model, tea.Cmd) {
 98 | 	m.active = m.focus
 99 | 
100 | 	switch m.active {
101 | 	case Ascii:
102 | 		m.mode = Ascii
103 | 	case Unicode:
104 | 		m.mode = Unicode
105 | 	case Custom:
106 | 		m.mode = Custom
107 | 	case SymbolsForm:
108 | 		m.mode = Custom
109 | 		m.customInput.Focus()
110 | 	case OneColor, TwoColor:
111 | 		m.useFgBg = m.active
112 | 	default:
113 | 		switch m.charControls {
114 | 		case Ascii:
115 | 			if _, ok := asciiCharModeMap[m.active]; ok {
116 | 				m.asciiMode = m.active
117 | 				m.mode = Ascii
118 | 			}
119 | 		case Unicode:
120 | 			if _, ok := unicodeCharModeMap[m.active]; ok {
121 | 				m.unicodeMode = m.active
122 | 				m.mode = Unicode
123 | 			}
124 | 		}
125 | 	}
126 | 	return m, event.StartRenderToViewCmd
127 | }
128 | 
129 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
130 | 
131 | 	var cmd tea.Cmd
132 | 	switch {
133 | 	case key.Matches(msg, event.KeyMap.Right):
134 | 		if next, hasNext := navMap[Right][m.focus]; hasNext {
135 | 			return m.setFocus(next)
136 | 		}
137 | 	case key.Matches(msg, event.KeyMap.Left):
138 | 		if next, hasNext := navMap[Left][m.focus]; hasNext {
139 | 			return m.setFocus(next)
140 | 		}
141 | 	case key.Matches(msg, event.KeyMap.Up):
142 | 		if next, hasNext := navMap[Up][m.focus]; hasNext {
143 | 			return m.setFocus(next)
144 | 		} else {
145 | 			m.IsActive = false
146 | 			m.ShouldClose = true
147 | 		}
148 | 	case key.Matches(msg, event.KeyMap.Down):
149 | 		if next, hasNext := navMap[Down][m.focus]; hasNext {
150 | 			return m.setFocus(next)
151 | 		} else {
152 | 			m.IsActive = false
153 | 			m.ShouldClose = true
154 | 		}
155 | 	}
156 | 	return m, cmd
157 | }
158 | 
159 | func (m Model) setFocus(focus State) (Model, tea.Cmd) {
160 | 	m.focus = focus
161 | 	switch m.focus {
162 | 	case Ascii:
163 | 		m.charControls = Ascii
164 | 	case Unicode:
165 | 		m.charControls = Unicode
166 | 	case Custom:
167 | 		m.charControls = Custom
168 | 	}
169 | 	return m, nil
170 | }
171 | 
```

--------------------------------------------------------------------------------
/app/process/unicode.go:
--------------------------------------------------------------------------------

```go
  1 | package process
  2 | 
  3 | import (
  4 | 	"image"
  5 | 	_ "image/gif"
  6 | 	_ "image/jpeg"
  7 | 	_ "image/png"
  8 | 	"math"
  9 | 
 10 | 	"github.com/charmbracelet/lipgloss"
 11 | 	"github.com/lucasb-eyer/go-colorful"
 12 | 	"github.com/makeworld-the-better-one/dither/v2"
 13 | 	"github.com/nfnt/resize"
 14 | 
 15 | 	"github.com/Zebbeni/ansizalizer/controls/settings/characters"
 16 | 	"github.com/Zebbeni/ansizalizer/controls/settings/size"
 17 | )
 18 | 
 19 | var unicodeShadeChars = []rune{' ', '░', '▒', '▓'}
 20 | 
 21 | func (m Renderer) processUnicode(input image.Image) string {
 22 | 	imgW, imgH := float32(input.Bounds().Dx()), float32(input.Bounds().Dy())
 23 | 
 24 | 	dimensionType, width, height, charRatio := m.Settings.Size.Info()
 25 | 	if dimensionType == size.Fit {
 26 | 		fitHeight := float32(width) * (imgH / imgW) * float32(charRatio)
 27 | 		fitWidth := (float32(height) * (imgW / imgH)) / float32(charRatio)
 28 | 		if fitHeight > float32(height) {
 29 | 			width = int(fitWidth)
 30 | 		} else {
 31 | 			height = int(fitHeight)
 32 | 		}
 33 | 	}
 34 | 
 35 | 	resizeFunc := m.Settings.Advanced.SamplingFunction()
 36 | 	refImg := resize.Resize(uint(width)*2, uint(height)*2, input, resizeFunc)
 37 | 
 38 | 	isTrueColor, _, palette := m.Settings.Colors.GetSelected()
 39 | 	isPaletted := !isTrueColor
 40 | 
 41 | 	doDither, doSerpentine, matrix := m.Settings.Advanced.Dithering()
 42 | 	if doDither && isPaletted {
 43 | 		ditherer := dither.NewDitherer(palette.Colors())
 44 | 		ditherer.Matrix = matrix
 45 | 		if doSerpentine {
 46 | 			ditherer.Serpentine = true
 47 | 		}
 48 | 		refImg = ditherer.Dither(refImg)
 49 | 	}
 50 | 
 51 | 	content := ""
 52 | 	rows := make([]string, height)
 53 | 	row := make([]string, width)
 54 | 	for y := 0; y < height*2; y += 2 {
 55 | 		for x := 0; x < width*2; x += 2 {
 56 | 			// r1 r2
 57 | 			// r3 r4
 58 | 			r1, _ := colorful.MakeColor(refImg.At(x, y))
 59 | 			r2, _ := colorful.MakeColor(refImg.At(x+1, y))
 60 | 			r3, _ := colorful.MakeColor(refImg.At(x, y+1))
 61 | 			r4, _ := colorful.MakeColor(refImg.At(x+1, y+1))
 62 | 
 63 | 			// pick the block, fg and bg color with the lowest total difference
 64 | 			// convert the colors to ansi, render the block and add it at row[x]
 65 | 			r, fg, bg := m.getBlock(r1, r2, r3, r4)
 66 | 
 67 | 			pFg, _ := colorful.MakeColor(fg)
 68 | 			pBg, _ := colorful.MakeColor(bg)
 69 | 
 70 | 			lipFg := lipgloss.Color(pFg.Hex())
 71 | 			lipBg := lipgloss.Color(pBg.Hex())
 72 | 
 73 | 			style := lipgloss.NewStyle().Foreground(lipFg)
 74 | 			if _, _, mode, _ := m.Settings.Characters.Selected(); mode == characters.TwoColor {
 75 | 				style = style.Copy().Background(lipBg)
 76 | 			}
 77 | 
 78 | 			row[x/2] = style.Render(string(r))
 79 | 		}
 80 | 		rows[y/2] = lipgloss.JoinHorizontal(lipgloss.Top, row...)
 81 | 	}
 82 | 	content += lipgloss.JoinVertical(lipgloss.Left, rows...)
 83 | 	return content
 84 | }
 85 | 
 86 | // find the best block character and foreground and background colors to match
 87 | // a set of 4 pixels. return
 88 | func (m Renderer) getBlock(r1, r2, r3, r4 colorful.Color) (r rune, fg, bg colorful.Color) {
 89 | 	var blockFuncs map[rune]blockFunc
 90 | 	switch _, charSet, _, _ := m.Settings.Characters.Selected(); charSet {
 91 | 	case characters.UnicodeFull:
 92 | 		blockFuncs = m.fullBlockFuncs
 93 | 	case characters.UnicodeHalf:
 94 | 		blockFuncs = m.halfBlockFuncs
 95 | 	case characters.UnicodeQuart:
 96 | 		blockFuncs = m.quarterBlockFuncs
 97 | 	case characters.UnicodeShadeLight:
 98 | 		blockFuncs = m.shadeLightBlockFuncs
 99 | 	case characters.UnicodeShadeMed:
100 | 		blockFuncs = m.shadeMedBlockFuncs
101 | 	case characters.UnicodeShadeHeavy:
102 | 		blockFuncs = m.shadeHeavyBlockFuncs
103 | 	}
104 | 
105 | 	minDist := 100.0
106 | 	for bRune, bFunc := range blockFuncs {
107 | 		f, b, dist := bFunc(r1, r2, r3, r4)
108 | 		if dist < minDist {
109 | 			minDist = dist
110 | 			r, fg, bg = bRune, f, b
111 | 		}
112 | 	}
113 | 	return
114 | }
115 | 
116 | func (m Renderer) avgCol(colors ...colorful.Color) (colorful.Color, float64) {
117 | 	rSum, gSum, bSum := 0.0, 0.0, 0.0
118 | 	for _, col := range colors {
119 | 		rSum += col.R
120 | 		gSum += col.G
121 | 		bSum += col.B
122 | 	}
123 | 	count := float64(len(colors))
124 | 	avg := colorful.Color{R: rSum / count, G: gSum / count, B: bSum / count}
125 | 
126 | 	if m.Settings.Colors.IsLimited() {
127 | 		_, _, palette := m.Settings.Colors.GetSelected()
128 | 
129 | 		paletteAvg := palette.Colors().Convert(avg)
130 | 		avg, _ = colorful.MakeColor(paletteAvg)
131 | 	}
132 | 
133 | 	// compute sum of squares
134 | 	totalDist := 0.0
135 | 	for _, col := range colors {
136 | 		totalDist += math.Pow(col.DistanceCIEDE2000(avg), 2)
137 | 	}
138 | 	return avg, totalDist
139 | }
140 | 
```

--------------------------------------------------------------------------------
/app/process/ascii.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 | // A list of Ascii characters by ascending brightness
 17 | var asciiChars = []rune(" `.-':_,^=;><+!rc*/z?sLTv)J7(|Fi{C}fI31tlu[neoZ5Yxjya]2ESwqkP6h9d4VpOGbUAKXHm8RD#$Bg0MNWQ%&@")
 18 | var asciiAZChars = []rune(" rczsLTvJFiCfItluneoZYxjyaESwqkPhdVpOGbUAKXHmRDBgMNWQ")
 19 | var asciiNumChars = []rune(" 7315269480")
 20 | var asciiSpecChars = []rune(" `.-':_,^=;><+!*/?)(|{}[]#$%&@")
 21 | 
 22 | func (m Renderer) processAscii(input image.Image) string {
 23 | 	imgW, imgH := float32(input.Bounds().Dx()), float32(input.Bounds().Dy())
 24 | 
 25 | 	dimensionType, width, height, charRatio := m.Settings.Size.Info()
 26 | 	if dimensionType == size.Fit {
 27 | 		fitHeight := float32(width) * (imgH / imgW) * float32(charRatio)
 28 | 		fitWidth := (float32(height) * (imgW / imgH)) / float32(charRatio)
 29 | 		if fitHeight > float32(height) {
 30 | 			width = int(fitWidth)
 31 | 		} else {
 32 | 			height = int(fitHeight)
 33 | 		}
 34 | 	}
 35 | 
 36 | 	resizeFunc := m.Settings.Advanced.SamplingFunction()
 37 | 	refImg := resize.Resize(uint(width)*2, uint(height)*2, input, resizeFunc)
 38 | 
 39 | 	isTrueColor, _, palette := m.Settings.Colors.GetSelected()
 40 | 	isPaletted := !isTrueColor
 41 | 
 42 | 	doDither, doSerpentine, matrix := m.Settings.Advanced.Dithering()
 43 | 	if doDither && isPaletted {
 44 | 		ditherer := dither.NewDitherer(palette.Colors())
 45 | 		ditherer.Matrix = matrix
 46 | 		if doSerpentine {
 47 | 			ditherer.Serpentine = true
 48 | 		}
 49 | 		refImg = ditherer.Dither(refImg)
 50 | 	}
 51 | 
 52 | 	var chars []rune
 53 | 	_, charMode, useFgBg, _ := m.Settings.Characters.Selected()
 54 | 	switch charMode {
 55 | 	case characters.AsciiAz:
 56 | 		chars = asciiAZChars
 57 | 	case characters.AsciiNums:
 58 | 		chars = asciiNumChars
 59 | 	case characters.AsciiSpec:
 60 | 		chars = asciiSpecChars
 61 | 	case characters.AsciiAll:
 62 | 		chars = asciiChars
 63 | 	}
 64 | 
 65 | 	content := ""
 66 | 	rows := make([]string, height)
 67 | 	row := make([]string, width)
 68 | 
 69 | 	for y := 0; y < height*2; y += 2 {
 70 | 		for x := 0; x < width*2; x += 2 {
 71 | 			r1, isTrans1 := colorful.MakeColor(refImg.At(x, y))
 72 | 			r2, isTrans2 := colorful.MakeColor(refImg.At(x+1, y))
 73 | 			r3, isTrans3 := colorful.MakeColor(refImg.At(x, y+1))
 74 | 			r4, isTrans4 := colorful.MakeColor(refImg.At(x+1, y+1))
 75 | 
 76 | 			if isTrans1 || isTrans2 || isTrans3 || isTrans4 {
 77 | 				isTrans2 = !isTrans2 == false
 78 | 			}
 79 | 
 80 | 			if useFgBg == characters.TwoColor {
 81 | 				fg, bg, brightness := m.fgBgBrightness(r1, r2, r3, r4)
 82 | 
 83 | 				lipFg := lipgloss.Color(fg.Hex())
 84 | 				lipBg := lipgloss.Color(bg.Hex())
 85 | 				style := lipgloss.NewStyle().Foreground(lipFg).Background(lipBg).Bold(true)
 86 | 
 87 | 				index := min(int(brightness*float64(len(chars))), len(chars)-1)
 88 | 				char := chars[index]
 89 | 				charString := string(char)
 90 | 
 91 | 				row[x/2] = style.Render(charString)
 92 | 			} else {
 93 | 				fg := m.avgColTrue(r1, r2, r3, r4)
 94 | 				brightness := math.Min(1.0, math.Abs(fg.DistanceLuv(black)))
 95 | 				if !isTrueColor {
 96 | 					fg, _ = colorful.MakeColor(palette.Colors().Convert(fg))
 97 | 				}
 98 | 				lipFg := lipgloss.Color(fg.Hex())
 99 | 				style := lipgloss.NewStyle().Foreground(lipFg).Bold(true)
100 | 
101 | 				index := min(int(brightness*float64(len(chars))), len(chars)-1)
102 | 				char := chars[index]
103 | 				charString := string(char)
104 | 				row[x/2] = style.Render(charString)
105 | 			}
106 | 		}
107 | 		rows[y/2] = lipgloss.JoinHorizontal(lipgloss.Top, row...)
108 | 	}
109 | 	content += lipgloss.JoinVertical(lipgloss.Left, rows...)
110 | 	return content
111 | }
112 | 
113 | func (m Renderer) fgBgBrightness(c ...colorful.Color) (fg, bg colorful.Color, b float64) {
114 | 	// find the darkest and lightest among given colors
115 | 	light, dark := lightDark(c...)
116 | 
117 | 	avg := m.avgColTrue(c...)
118 | 	avgCol, _ := colorful.MakeColor(avg)
119 | 
120 | 	//distLight := avgCol.DistanceLuv(light)
121 | 	distDark := avgCol.DistanceLuv(dark)
122 | 	distTotal := light.DistanceLuv(dark)
123 | 	var brightness float64
124 | 	if distTotal == 0 {
125 | 		brightness = 0
126 | 	} else {
127 | 		brightness = math.Min(1.0, math.Abs(distDark/distTotal))
128 | 	}
129 | 
130 | 	// if paletted:
131 | 	//   convert the darkest to its closest paletted color
132 | 	//   convert the lightest to its closest paletted color (excluding the previously found color)
133 | 	if m.Settings.Colors.IsLimited() {
134 | 		light, dark = m.getLightDarkPaletted(light, dark)
135 | 	}
136 | 
137 | 	return light, dark, brightness
138 | }
139 | 
140 | func (m Renderer) avgColTrue(colors ...colorful.Color) colorful.Color {
141 | 	rSum, gSum, bSum := 0.0, 0.0, 0.0
142 | 	for _, col := range colors {
143 | 		rSum += col.R
144 | 		gSum += col.G
145 | 		bSum += col.B
146 | 	}
147 | 	count := float64(len(colors))
148 | 	avg := colorful.Color{R: rSum / count, G: gSum / count, B: bSum / count}
149 | 
150 | 	return avg
151 | }
152 | 
153 | func lightDark(c ...colorful.Color) (light, dark colorful.Color) {
154 | 	mostLight, mostDark := 0.0, 1.0
155 | 	for _, col := range c {
156 | 		_, _, l := col.Hsl()
157 | 		if l < mostDark {
158 | 			mostDark = l
159 | 			dark = col
160 | 		}
161 | 		if l > mostLight {
162 | 			mostLight = l
163 | 			light = col
164 | 		}
165 | 	}
166 | 	return
167 | }
168 | 
```

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

```go
  1 | package loader
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"image/color"
  6 | 
  7 | 	"github.com/lucasb-eyer/go-colorful"
  8 | 	"github.com/muesli/termenv"
  9 | )
 10 | 
 11 | func BlackAndWhite() color.Palette {
 12 | 	return color.Palette{
 13 | 		color.RGBA{R: 0, G: 0, B: 0, A: 255},
 14 | 		color.RGBA{R: 255, G: 255, B: 255, A: 255},
 15 | 	}
 16 | }
 17 | 
 18 | func AnsiVga16() color.Palette {
 19 | 	return color.Palette{
 20 | 		color.RGBA{R: 0, G: 0, B: 0, A: 255},
 21 | 		color.RGBA{R: 170, G: 0, B: 0, A: 255},
 22 | 		color.RGBA{R: 0, G: 170, B: 0, A: 255},
 23 | 		color.RGBA{R: 170, G: 85, B: 0, A: 255},
 24 | 		color.RGBA{R: 0, G: 0, B: 170, A: 255},
 25 | 		color.RGBA{R: 170, G: 0, B: 170, A: 255},
 26 | 		color.RGBA{R: 0, G: 170, B: 170, A: 255},
 27 | 		color.RGBA{R: 170, G: 170, B: 170, A: 255},
 28 | 		color.RGBA{R: 85, G: 85, B: 85, A: 255},
 29 | 		color.RGBA{R: 255, G: 85, B: 85, A: 255},
 30 | 		color.RGBA{R: 85, G: 255, B: 85, A: 255},
 31 | 		color.RGBA{R: 255, G: 255, B: 85, A: 255},
 32 | 		color.RGBA{R: 85, G: 85, B: 255, A: 255},
 33 | 		color.RGBA{R: 255, G: 85, B: 255, A: 255},
 34 | 		color.RGBA{R: 85, G: 255, B: 255, A: 255},
 35 | 		color.RGBA{R: 255, G: 255, B: 255, A: 255},
 36 | 	}
 37 | }
 38 | 
 39 | func AnsiWinConsole16() color.Palette {
 40 | 	return color.Palette{
 41 | 		color.RGBA{R: 0, G: 0, B: 0, A: 255},
 42 | 		color.RGBA{R: 128, G: 0, B: 0, A: 255},
 43 | 		color.RGBA{R: 0, G: 128, B: 0, A: 255},
 44 | 		color.RGBA{R: 128, G: 128, B: 0, A: 255},
 45 | 		color.RGBA{R: 0, G: 0, B: 128, A: 255},
 46 | 		color.RGBA{R: 128, G: 0, B: 128, A: 255},
 47 | 		color.RGBA{R: 0, G: 128, B: 128, A: 255},
 48 | 		color.RGBA{R: 192, G: 192, B: 192, A: 255},
 49 | 		color.RGBA{R: 128, G: 128, B: 128, A: 255},
 50 | 		color.RGBA{R: 255, G: 0, B: 0, A: 255},
 51 | 		color.RGBA{R: 0, G: 255, B: 0, A: 255},
 52 | 		color.RGBA{R: 255, G: 255, B: 0, A: 255},
 53 | 		color.RGBA{R: 0, G: 0, B: 255, A: 255},
 54 | 		color.RGBA{R: 255, G: 0, B: 255, A: 255},
 55 | 		color.RGBA{R: 0, G: 255, B: 255, A: 255},
 56 | 		color.RGBA{R: 255, G: 255, B: 255, A: 255},
 57 | 	}
 58 | }
 59 | 
 60 | func AnsiWinPowershell16() color.Palette {
 61 | 	return color.Palette{
 62 | 		color.RGBA{R: 12, G: 12, B: 12, A: 255},
 63 | 		color.RGBA{R: 197, G: 15, B: 31, A: 255},
 64 | 		color.RGBA{R: 19, G: 161, B: 14, A: 255},
 65 | 		color.RGBA{R: 193, G: 156, B: 0, A: 255},
 66 | 		color.RGBA{R: 0, G: 55, B: 218, A: 255},
 67 | 		color.RGBA{R: 136, G: 23, B: 152, A: 255},
 68 | 		color.RGBA{R: 58, G: 150, B: 221, A: 255},
 69 | 		color.RGBA{R: 204, G: 204, B: 204, A: 255},
 70 | 		color.RGBA{R: 118, G: 118, B: 118, A: 255},
 71 | 		color.RGBA{R: 231, G: 72, B: 86, A: 255},
 72 | 		color.RGBA{R: 22, G: 198, B: 12, A: 255},
 73 | 		color.RGBA{R: 249, G: 241, B: 165, A: 255},
 74 | 		color.RGBA{R: 59, G: 120, B: 255, A: 255},
 75 | 		color.RGBA{R: 180, G: 0, B: 158, A: 255},
 76 | 		color.RGBA{R: 97, G: 214, B: 214, A: 255},
 77 | 		color.RGBA{R: 242, G: 242, B: 242, A: 255},
 78 | 	}
 79 | }
 80 | 
 81 | func Ansi16() color.Palette {
 82 | 	p := make(color.Palette, 0, 16)
 83 | 	for i := 0; i < 16; i++ {
 84 | 		ansi := termenv.ANSI.Color(fmt.Sprintf("%d", i))
 85 | 		col := termenv.ConvertToRGB(ansi)
 86 | 		p = append(p, col)
 87 | 	}
 88 | 	return p
 89 | }
 90 | 
 91 | func Ansi256() color.Palette {
 92 | 	p := make(color.Palette, 0, 256)
 93 | 	for i := 0; i < 256; i++ {
 94 | 		ansi := termenv.ANSI256.Color(fmt.Sprintf("%d", i))
 95 | 		col := termenv.ConvertToRGB(ansi)
 96 | 		p = append(p, col)
 97 | 	}
 98 | 	return p
 99 | }
100 | 
101 | func KlarikFilmic() color.Palette {
102 | 	hexes := []string{
103 | 		"#ffffff",
104 | 		"#d6dfdf",
105 | 		"#b5c4c1",
106 | 		"#8fa6a0",
107 | 		"#6f837e",
108 | 		"#536a66",
109 | 		"#2b3b3e",
110 | 		"#162424",
111 | 		"#000000",
112 | 		"#250a1d",
113 | 		"#3f1526",
114 | 		"#5a2535",
115 | 		"#82363f",
116 | 		"#a64e54",
117 | 		"#b66868",
118 | 		"#c08780",
119 | 		"#ceaea4",
120 | 		"#b2897c",
121 | 		"#9a6a5d",
122 | 		"#7c4d3f",
123 | 		"#5b2e2b",
124 | 		"#3d181b",
125 | 		"#280b15",
126 | 		"#895938",
127 | 		"#b1834e",
128 | 		"#bb995f",
129 | 		"#caac7a",
130 | 		"#d3c59f",
131 | 		"#a8ad80",
132 | 		"#84935a",
133 | 		"#5a7645",
134 | 		"#305630",
135 | 		"#1a3725",
136 | 		"#0e2724",
137 | 		"#152f3c",
138 | 		"#2d4e59",
139 | 		"#4b7674",
140 | 		"#628e87",
141 | 		"#7ca294",
142 | 		"#a5bbae",
143 | 		"#bacbc9",
144 | 		"#a1b7bf",
145 | 		"#778faa",
146 | 		"#5e6d92",
147 | 		"#424372",
148 | 		"#352959",
149 | 		"#2c173d",
150 | 		"#492854",
151 | 		"#6e3f72",
152 | 		"#935c8d",
153 | 		"#ae7d9e",
154 | 		"#c6a7b5",
155 | 		"#ac7b90",
156 | 		"#8f516c",
157 | 		"#73415a",
158 | 		"#542846",
159 | 		"#3f1831",
160 | 	}
161 | 	return hexesToColorPalette(hexes)
162 | }
163 | 
164 | func Mudstone() color.Palette {
165 | 	hexes := []string{
166 | 		"#1b1611",
167 | 		"#1f253c",
168 | 		"#423c32",
169 | 		"#465d32",
170 | 		"#6e3f24",
171 | 		"#6b624e",
172 | 		"#90752e",
173 | 		"#cda465",
174 | 	}
175 | 	return hexesToColorPalette(hexes)
176 | }
177 | 
178 | func IsleOfTheDead() color.Palette {
179 | 	hexes := []string{
180 | 		"#0b0b0b",
181 | 		"#454848",
182 | 		"#4f514f",
183 | 		"#5a5a5a",
184 | 		"#666666",
185 | 		"#3e3f3f",
186 | 		"#373838",
187 | 		"#242421",
188 | 		"#2c2d25",
189 | 		"#36382a",
190 | 		"#1b1b17",
191 | 		"#313333",
192 | 		"#858585",
193 | 		"#a0a0a0",
194 | 		"#717171",
195 | 		"#2c2d2d",
196 | 		"#121210",
197 | 		"#3f4132",
198 | 		"#aeaeae",
199 | 		"#575a4a",
200 | 		"#737359",
201 | 		"#858562",
202 | 		"#93906c",
203 | 		"#686652",
204 | 		"#a9a681",
205 | 		"#48534d",
206 | 		"#252928",
207 | 		"#857d62",
208 | 		"#aea282",
209 | 		"#d0cec1",
210 | 		"#c0b9a5",
211 | 		"#58503b",
212 | 		"#7a6b54",
213 | 		"#413a28",
214 | 		"#53493a",
215 | 		"#685a44",
216 | 		"#443b2e",
217 | 		"#1a201e",
218 | 		"#362e23",
219 | 		"#7a704d",
220 | 		"#222b31",
221 | 		"#364550",
222 | 	}
223 | 	return hexesToColorPalette(hexes)
224 | }
225 | 
226 | func hexesToColorPalette(hexes []string) color.Palette {
227 | 	var colorPalette color.Palette
228 | 	for _, h := range hexes {
229 | 		c, _ := colorful.Hex(h)
230 | 		colorPalette = append(colorPalette, c)
231 | 	}
232 | 	return colorPalette
233 | }
234 | 
```

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

```go
  1 | package lospec
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"image/color"
  6 | 	"strconv"
  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 | 	"github.com/lucasb-eyer/go-colorful"
 13 | 
 14 | 	"github.com/Zebbeni/ansizalizer/event"
 15 | 	"github.com/Zebbeni/ansizalizer/palette"
 16 | 	"github.com/Zebbeni/ansizalizer/style"
 17 | )
 18 | 
 19 | // TODO: Direction is redefined in multiple places
 20 | 
 21 | type Direction int
 22 | 
 23 | type Param int
 24 | 
 25 | const (
 26 | 	Left Direction = iota
 27 | 	Right
 28 | 	Up
 29 | 	Down
 30 | )
 31 | 
 32 | var (
 33 | 	navMap = map[Direction]map[State]State{
 34 | 		Right: {CountForm: FilterExact, FilterExact: FilterMax, FilterMax: FilterMin, SortAlphabetical: SortDownloads, SortDownloads: SortNewest},
 35 | 		Left:  {TagForm: CountForm, FilterMin: FilterMax, FilterMax: FilterExact, FilterExact: CountForm, SortNewest: SortDownloads, SortDownloads: SortAlphabetical},
 36 | 		Up:    {TagForm: CountForm, SortAlphabetical: TagForm, SortDownloads: TagForm, SortNewest: TagForm, List: SortAlphabetical},
 37 | 		Down:  {CountForm: TagForm, FilterExact: TagForm, FilterMax: TagForm, FilterMin: TagForm, TagForm: SortAlphabetical, SortAlphabetical: List, SortDownloads: List, SortNewest: List},
 38 | 	}
 39 | 	filterParams = map[State]string{
 40 | 		FilterExact: "exact",
 41 | 		FilterMax:   "max",
 42 | 		FilterMin:   "min",
 43 | 	}
 44 | 	sortParams = map[State]string{
 45 | 		SortAlphabetical: "alphabetical",
 46 | 		SortDownloads:    "downloads",
 47 | 		SortNewest:       "newest",
 48 | 	}
 49 | )
 50 | 
 51 | func (m Model) handleEsc() (Model, tea.Cmd) {
 52 | 	m.ShouldClose = true
 53 | 	m.IsSelected = false
 54 | 	m.ShouldUnfocus = true
 55 | 	return m, nil
 56 | }
 57 | 
 58 | func (m Model) handleEnter() (Model, tea.Cmd) {
 59 | 	m.active = m.focus
 60 | 	switch m.focus {
 61 | 	case CountForm:
 62 | 		m.countInput.Focus()
 63 | 		return m, nil
 64 | 	case TagForm:
 65 | 		m.tagInput.Focus()
 66 | 		return m, nil
 67 | 	case FilterExact, FilterMax, FilterMin:
 68 | 		m.filterType = m.focus
 69 | 		return m.searchLospec(0)
 70 | 	case SortAlphabetical, SortDownloads, SortNewest:
 71 | 		m.sortType = m.focus
 72 | 		return m.searchLospec(0)
 73 | 	case List:
 74 | 		m.palette, _ = m.paletteList.SelectedItem().(palette.Model)
 75 | 		m.IsSelected = true
 76 | 		return m, event.StartRenderToViewCmd
 77 | 	}
 78 | 	return m, nil
 79 | }
 80 | 
 81 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) {
 82 | 	switch {
 83 | 	case key.Matches(msg, event.KeyMap.Right):
 84 | 		if next, hasNext := navMap[Right][m.focus]; hasNext {
 85 | 			m.focus = next
 86 | 		}
 87 | 	case key.Matches(msg, event.KeyMap.Left):
 88 | 		if next, hasNext := navMap[Left][m.focus]; hasNext {
 89 | 			m.focus = next
 90 | 		}
 91 | 	case key.Matches(msg, event.KeyMap.Down):
 92 | 		if next, hasNext := navMap[Down][m.focus]; hasNext {
 93 | 			m.focus = next
 94 | 		}
 95 | 	case key.Matches(msg, event.KeyMap.Up):
 96 | 		if next, hasNext := navMap[Up][m.focus]; hasNext {
 97 | 			m.focus = next
 98 | 		} else {
 99 | 			m.IsSelected = false
100 | 			m.ShouldUnfocus = true
101 | 		}
102 | 	}
103 | 	return m, nil
104 | }
105 | 
106 | func (m Model) handleLospecResponse(msg event.LospecResponseMsg) (Model, tea.Cmd) {
107 | 	var cmd tea.Cmd
108 | 	// return early if response no longer matches current requestID
109 | 	if msg.ID != m.requestID {
110 | 		return m, cmd
111 | 	}
112 | 
113 | 	// if we haven't initialized and allocated an array of palettes for the current request series, do that first
114 | 	if !m.isPaletteListAllocated {
115 | 		m.palettes = make([]list.Item, msg.Data.TotalCount)
116 | 		m.paletteList = CreateList(m.palettes, m.width-2)
117 | 		m.paletteList.Styles.Title = style.DimmedTitle
118 | 		m.paletteList.Styles.TitleBar = m.paletteList.Styles.TitleBar.Padding(0).Width(m.width).AlignHorizontal(lipgloss.Center)
119 | 		m.isPaletteListAllocated = true
120 | 	}
121 | 
122 | 	// use the page number*10 (assumes 10 palettes per page) to populate palettes
123 | 	for i, p := range msg.Data.Palettes {
124 | 		colors := make([]color.Color, len(p.Colors))
125 | 		var err error
126 | 
127 | 		for colorIndex, c := range p.Colors {
128 | 			colors[colorIndex], err = colorful.Hex(fmt.Sprintf("#%s", c))
129 | 			if err != nil {
130 | 				return m, event.BuildDisplayCmd("error converting hex value")
131 | 			}
132 | 		}
133 | 
134 | 		idx := (msg.Page * 10) + i
135 | 		m.palettes[idx] = palette.New(p.Title, colors, m.width-4, 2)
136 | 	}
137 | 
138 | 	m.paletteList.SetItems(m.palettes)
139 | 
140 | 	return m, cmd
141 | }
142 | 
143 | func (m Model) handleCountFormUpdate(msg tea.Msg) (Model, tea.Cmd) {
144 | 	if keyMsg, ok := msg.(tea.KeyMsg); ok {
145 | 		switch {
146 | 		case key.Matches(keyMsg, event.KeyMap.Enter):
147 | 			m.countInput.Blur()
148 | 			return m.searchLospec(0)
149 | 		case key.Matches(keyMsg, event.KeyMap.Esc):
150 | 			m.countInput.Blur()
151 | 		}
152 | 	}
153 | 	var cmd tea.Cmd
154 | 	m.countInput, cmd = m.countInput.Update(msg)
155 | 	return m, cmd
156 | }
157 | 
158 | func (m Model) handleTagFormUpdate(msg tea.Msg) (Model, tea.Cmd) {
159 | 	if keyMsg, ok := msg.(tea.KeyMsg); ok {
160 | 		switch {
161 | 		case key.Matches(keyMsg, event.KeyMap.Enter):
162 | 			m.tagInput.Blur()
163 | 			return m.searchLospec(0)
164 | 		case key.Matches(keyMsg, event.KeyMap.Esc):
165 | 			m.tagInput.Blur()
166 | 		}
167 | 	}
168 | 	var cmd tea.Cmd
169 | 	m.tagInput, cmd = m.tagInput.Update(msg)
170 | 	return m, cmd
171 | }
172 | 
173 | func (m Model) handleListUpdate(msg tea.Msg) (Model, tea.Cmd) {
174 | 	keyMsg, ok := msg.(tea.KeyMsg)
175 | 	if !ok {
176 | 		return m, nil
177 | 	}
178 | 
179 | 	switch {
180 | 	case key.Matches(keyMsg, event.KeyMap.Enter):
181 | 		return m.handleEnter()
182 | 	case key.Matches(keyMsg, event.KeyMap.Up) && m.paletteList.Index() == 0:
183 | 		return m.handleNav(keyMsg)
184 | 	case key.Matches(keyMsg, event.KeyMap.Esc):
185 | 		m.focus = TagForm
186 | 	}
187 | 
188 | 	var cmd tea.Cmd
189 | 	if len(m.paletteList.Items()) > 0 {
190 | 		m.paletteList, cmd = m.paletteList.Update(msg)
191 | 	}
192 | 
193 | 	if m.paletteList.Index() < (m.highestPageRequested-1)*10 {
194 | 		return m, cmd
195 | 	}
196 | 
197 | 	m.highestPageRequested += 1
198 | 	return m.searchLospec(m.highestPageRequested)
199 | }
200 | 
201 | func (m Model) searchLospec(page int) (Model, tea.Cmd) {
202 | 	if page == 0 {
203 | 		m.requestID += 1
204 | 		m.highestPageRequested = 0
205 | 		m.isPaletteListAllocated = false
206 | 	}
207 | 
208 | 	colors, _ := strconv.Atoi(m.countInput.Value())
209 | 	tag := m.tagInput.Value()
210 | 	filterType := filterParams[m.filterType]
211 | 	sortingType := sortParams[m.sortType]
212 | 
213 | 	urlString := "https://lospec.com/palette-list/load?colorNumber=%d&tag=%s&colorNumberFilterType=%s&sortingType=%s&page=%d"
214 | 	url := fmt.Sprintf(urlString, colors, tag, filterType, sortingType, page)
215 | 	return m, event.BuildLospecRequestCmd(event.LospecRequestMsg{
216 | 		URL:  url,
217 | 		ID:   m.requestID,
218 | 		Page: page,
219 | 	})
220 | }
221 | 
```

--------------------------------------------------------------------------------
/app/update.go:
--------------------------------------------------------------------------------

```go
  1 | package app
  2 | 
  3 | import (
  4 | 	"bufio"
  5 | 	"encoding/json"
  6 | 	"fmt"
  7 | 	"io"
  8 | 	"net/http"
  9 | 	"os"
 10 | 	"strings"
 11 | 
 12 | 	"github.com/atotto/clipboard"
 13 | 	tea "github.com/charmbracelet/bubbletea"
 14 | 
 15 | 	"github.com/Zebbeni/ansizalizer/app/adapt"
 16 | 	"github.com/Zebbeni/ansizalizer/app/process"
 17 | 	"github.com/Zebbeni/ansizalizer/event"
 18 | )
 19 | 
 20 | func (m Model) handleStartRenderToViewCmd() (Model, tea.Cmd) {
 21 | 	m.viewer.WaitingOnRender = true
 22 | 	return m, m.processRenderToViewCmd
 23 | }
 24 | 
 25 | func (m Model) handleFinishRenderToViewMsg(msg event.FinishRenderToViewMsg) (Model, tea.Cmd) {
 26 | 	// cut out early if the finished render is for a previously selected image
 27 | 	if msg.FilePath != m.controls.FileBrowser.ActiveFile {
 28 | 		return m, nil
 29 | 	}
 30 | 
 31 | 	var cmd tea.Cmd
 32 | 	m.viewer, cmd = m.viewer.Update(msg)
 33 | 	return m, cmd
 34 | }
 35 | 
 36 | func (m Model) processRenderToViewCmd() tea.Msg {
 37 | 	imgString := process.RenderImageFile(m.controls.Settings, m.controls.FileBrowser.ActiveFile)
 38 | 	colorsString := "true color"
 39 | 	if m.controls.Settings.Colors.IsLimited() {
 40 | 		palette := m.controls.Settings.Colors.GetCurrentPalette()
 41 | 		colorsString = palette.Title()
 42 | 	}
 43 | 	return event.FinishRenderToViewMsg{FilePath: m.controls.FileBrowser.ActiveFile, ImgString: imgString, ColorsString: colorsString}
 44 | }
 45 | 
 46 | func (m Model) handleStartExportMsg(msg event.StartExportMsg) (Model, tea.Cmd) {
 47 | 	if m.waitingOnExport {
 48 | 		return m, nil
 49 | 	}
 50 | 
 51 | 	var exportQueue []exportJob
 52 | 	var err error
 53 | 
 54 | 	// build export queue
 55 | 	if msg.IsDir {
 56 | 		exportQueue, err = buildExportQueue(msg.SourcePath, msg.DestinationPath, msg.UseSubDirs)
 57 | 		if err != nil {
 58 | 			return m, event.BuildDisplayCmd(fmt.Sprintf("error exporting: %s", err))
 59 | 		}
 60 | 	} else {
 61 | 		exportQueue = []exportJob{
 62 | 			{
 63 | 				sourcePath:      msg.SourcePath,
 64 | 				destinationPath: msg.DestinationPath,
 65 | 			},
 66 | 		}
 67 | 	}
 68 | 
 69 | 	m.exportIndex = 0
 70 | 	m.exportQueue = exportQueue
 71 | 	m.waitingOnExport = true
 72 | 
 73 | 	return m, tea.Batch(event.StartRenderToExportCmd, event.BuildDisplayCmd(fmt.Sprintf("%d jobs queued", len(exportQueue))))
 74 | }
 75 | 
 76 | func (m Model) handleRenderToExportMsg() (Model, tea.Cmd) {
 77 | 
 78 | 	currentJob := m.exportQueue[m.exportIndex]
 79 | 
 80 | 	// render image
 81 | 	imgString := process.RenderImageFile(m.controls.Settings, currentJob.sourcePath)
 82 | 
 83 | 	// save file
 84 | 	file, err := os.Create(currentJob.destinationPath)
 85 | 	if err != nil {
 86 | 		return m, event.BuildDisplayCmd("error creating save file")
 87 | 	}
 88 | 
 89 | 	w := bufio.NewWriter(file)
 90 | 	_, err = w.WriteString(imgString)
 91 | 	if err != nil {
 92 | 		return m, event.BuildDisplayCmd("error writing to save file")
 93 | 	}
 94 | 
 95 | 	m.exportIndex += 1
 96 | 	displayMsg := fmt.Sprintf("%d/%d exports completed", m.exportIndex, len(m.exportQueue))
 97 | 	displayCmd := event.BuildDisplayCmd(displayMsg)
 98 | 
 99 | 	if m.exportIndex >= len(m.exportQueue) {
100 | 		m.waitingOnExport = false
101 | 		return m, displayCmd
102 | 	}
103 | 
104 | 	return m, tea.Batch(event.StartRenderToExportCmd, displayCmd)
105 | }
106 | 
107 | func (m Model) startExportingDir(msg event.StartExportMsg) (Model, tea.Cmd) {
108 | 	return m, event.BuildDisplayCmd(fmt.Sprintf("exporting %s", msg.SourcePath))
109 | }
110 | 
111 | func (m Model) startExportingFile(msg event.StartExportMsg) (Model, tea.Cmd) {
112 | 	return m, event.BuildDisplayCmd(fmt.Sprintf("exporting %s", msg.SourcePath))
113 | }
114 | 
115 | func (m Model) handleStartAdaptingMsg() (Model, tea.Cmd) {
116 | 	filename := m.controls.FileBrowser.ActiveFilename()
117 | 	message := fmt.Sprintf("generating palette from %s...", filename)
118 | 	return m, tea.Batch(event.BuildDisplayCmd(message), m.processAdaptingCmd)
119 | }
120 | 
121 | func (m Model) handleFinishAdaptingMsg(msg event.FinishAdaptingMsg) (Model, tea.Cmd) {
122 | 	m.controls.Settings.Colors.PaletteControls.Adapter = m.controls.Settings.Colors.PaletteControls.Adapter.SetPalette(msg.Colors, msg.Name)
123 | 	return m, tea.Batch(event.StartRenderToViewCmd, event.BuildDisplayCmd("rendering..."))
124 | }
125 | 
126 | type Foo struct {
127 | 	Bar string
128 | }
129 | 
130 | func (m Model) handleLospecRequestMsg(msg event.LospecRequestMsg) (Model, tea.Cmd) {
131 | 	// make url request
132 | 	r, err := http.Get(msg.URL)
133 | 	if err != nil {
134 | 		return m, event.BuildDisplayCmd("error making lospec request")
135 | 	}
136 | 	defer r.Body.Close()
137 | 
138 | 	body, err := io.ReadAll(r.Body)
139 | 	if err != nil {
140 | 		return m, event.BuildDisplayCmd("error reading lospec response")
141 | 	}
142 | 
143 | 	// parse json and populate LospecResponseMsg
144 | 	data := new(event.LospecData)
145 | 	err = json.Unmarshal(body, &data)
146 | 	if err != nil {
147 | 		return m, event.BuildDisplayCmd("error decoding lospec request")
148 | 	}
149 | 
150 | 	// build Data Cmd
151 | 	return m, event.BuildLospecResponseCmd(event.LospecResponseMsg{
152 | 		ID:   msg.ID,
153 | 		Page: msg.Page,
154 | 		Data: *data,
155 | 	})
156 | }
157 | 
158 | func (m Model) handleLospecResponseMsg(msg event.LospecResponseMsg) (Model, tea.Cmd) {
159 | 	var cmd tea.Cmd
160 | 	m.controls.Settings.Colors.PaletteControls.Lospec, cmd = m.controls.Settings.Colors.PaletteControls.Lospec.Update(msg)
161 | 	return m, cmd
162 | }
163 | 
164 | func (m Model) processAdaptingCmd() tea.Msg {
165 | 	colors, name := adapt.GeneratePalette(m.controls.Settings.Colors.PaletteControls.Adapter, m.controls.FileBrowser.ActiveFile)
166 | 	return event.FinishAdaptingMsg{
167 | 		Name:   name,
168 | 		Colors: colors,
169 | 	}
170 | }
171 | 
172 | func (m Model) handleControlsUpdate(msg tea.Msg) (Model, tea.Cmd) {
173 | 	var cmd tea.Cmd
174 | 	m.controls, cmd = m.controls.Update(msg)
175 | 	return m, cmd
176 | }
177 | 
178 | func (m Model) handleDisplayMsg(msg tea.Msg) (Model, tea.Cmd) {
179 | 	var cmd tea.Cmd
180 | 	m.display, cmd = m.display.Update(msg)
181 | 	return m, cmd
182 | }
183 | 
184 | func (m Model) handleCopy() (Model, tea.Cmd) {
185 | 	if err := clipboard.WriteAll(m.viewer.View()); err != nil {
186 | 		return m, event.BuildDisplayCmd("Error copying to clipboard")
187 | 		// we should have a place in the UI where we display errors or processing messages,
188 | 		// and package our desired event to the user in a specific command
189 | 	}
190 | 	filename := m.controls.FileBrowser.ActiveFilename()
191 | 	name := strings.Split(filename, ".")[0] // strip extension
192 | 	return m, event.BuildDisplayCmd(fmt.Sprintf("copied %s to clipboard", name))
193 | }
194 | 
195 | func (m Model) handleSave() (Model, tea.Cmd) {
196 | 	name := strings.Split(m.controls.FileBrowser.ActiveFilename(), ".")[0]
197 | 	filename := fmt.Sprintf("%s.ansi", name)
198 | 	file, err := os.Create(filename)
199 | 	if err != nil {
200 | 		return m, event.BuildDisplayCmd("error creating save file")
201 | 	}
202 | 
203 | 	w := bufio.NewWriter(file)
204 | 	_, err = w.WriteString(m.viewer.View())
205 | 	if err != nil {
206 | 		return m, event.BuildDisplayCmd("error writing to save file")
207 | 	}
208 | 
209 | 	return m, event.BuildDisplayCmd(fmt.Sprintf("saved to %s", filename))
210 | }
211 | 
```

--------------------------------------------------------------------------------
/app/process/renderer.go:
--------------------------------------------------------------------------------

```go
  1 | package process
  2 | 
  3 | import (
  4 | 	"image/color"
  5 | 	"math"
  6 | 
  7 | 	"github.com/lucasb-eyer/go-colorful"
  8 | 
  9 | 	"github.com/Zebbeni/ansizalizer/controls/settings"
 10 | 	"github.com/Zebbeni/ansizalizer/controls/settings/characters"
 11 | )
 12 | 
 13 | type Renderer struct {
 14 | 	Settings             settings.Model
 15 | 	shadeLightBlockFuncs map[rune]blockFunc
 16 | 	shadeMedBlockFuncs   map[rune]blockFunc
 17 | 	shadeHeavyBlockFuncs map[rune]blockFunc
 18 | 	quarterBlockFuncs    map[rune]blockFunc
 19 | 	halfBlockFuncs       map[rune]blockFunc
 20 | 	fullBlockFuncs       map[rune]blockFunc
 21 | }
 22 | 
 23 | func New(s settings.Model) Renderer {
 24 | 	m := Renderer{
 25 | 		Settings: s,
 26 | 	}
 27 | 	m.fullBlockFuncs = m.createFullBlockFuncs()
 28 | 	m.halfBlockFuncs = m.createHalfBlockFuncs()
 29 | 	m.quarterBlockFuncs = m.createQuarterBlockFuncs()
 30 | 	m.shadeLightBlockFuncs = m.createShadeLightFuncs()
 31 | 	m.shadeMedBlockFuncs = m.createShadeMedFuncs()
 32 | 	m.shadeHeavyBlockFuncs = m.createShadeHeavyFuncs()
 33 | 	return m
 34 | }
 35 | 
 36 | type blockFunc func(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64)
 37 | 
 38 | func (m Renderer) createQuarterBlockFuncs() map[rune]blockFunc {
 39 | 	return map[rune]blockFunc{
 40 | 		'▀': m.calcTop,
 41 | 		'▐': m.calcRight,
 42 | 		'▞': m.calcDiagonal,
 43 | 		'▖': m.calcBotLeft,
 44 | 		'▘': m.calcTopLeft,
 45 | 		'▝': m.calcTopRight,
 46 | 		'▗': m.calcBotRight,
 47 | 	}
 48 | }
 49 | func (m Renderer) createHalfBlockFuncs() map[rune]blockFunc {
 50 | 	return map[rune]blockFunc{
 51 | 		'▀': m.calcTop,
 52 | 	}
 53 | }
 54 | 
 55 | func (m Renderer) createFullBlockFuncs() map[rune]blockFunc {
 56 | 	return map[rune]blockFunc{
 57 | 		'█': m.calcFull,
 58 | 	}
 59 | }
 60 | 
 61 | func (m Renderer) createShadeLightFuncs() map[rune]blockFunc {
 62 | 	return map[rune]blockFunc{
 63 | 		'░': m.calcHeavy,
 64 | 	}
 65 | }
 66 | 
 67 | func (m Renderer) createShadeMedFuncs() map[rune]blockFunc {
 68 | 	return map[rune]blockFunc{
 69 | 		'▒': m.calcHeavy,
 70 | 	}
 71 | }
 72 | 
 73 | func (m Renderer) createShadeHeavyFuncs() map[rune]blockFunc {
 74 | 	return map[rune]blockFunc{
 75 | 		'▓': m.calcHeavy,
 76 | 	}
 77 | }
 78 | 
 79 | func (m Renderer) getLightDarkPaletted(light, dark colorful.Color) (colorful.Color, colorful.Color) {
 80 | 	_, _, p := m.Settings.Colors.GetSelected()
 81 | 	colors := p.Colors()
 82 | 
 83 | 	index := colors.Index(dark)
 84 | 	paletteDark := colors.Convert(dark)
 85 | 
 86 | 	palette := make([]color.Color, len(colors))
 87 | 	copy(palette, colors)
 88 | 
 89 | 	paletteMinusDarkest := append(colors[:index], colors[index+1:]...)
 90 | 	paletteLight := paletteMinusDarkest.Convert(light)
 91 | 
 92 | 	light, _ = colorful.MakeColor(paletteLight)
 93 | 	dark, _ = colorful.MakeColor(paletteDark)
 94 | 
 95 | 	// swap light / dark if light is darker than dark
 96 | 	lightBlackDist := light.DistanceLuv(black)
 97 | 	darkBlackDist := dark.DistanceLuv(black)
 98 | 	if darkBlackDist > lightBlackDist {
 99 | 		temp := light
100 | 		light = dark
101 | 		dark = temp
102 | 	}
103 | 
104 | 	return light, dark
105 | }
106 | 
107 | func (m Renderer) getDarkestPaletted() colorful.Color {
108 | 	if !m.Settings.Colors.IsLimited() {
109 | 		return black
110 | 	}
111 | 	_, _, p := m.Settings.Colors.GetSelected()
112 | 	colors := p.Colors()
113 | 	darkest := colors.Convert(black)
114 | 	darkestConverted, _ := colorful.MakeColor(darkest)
115 | 	return darkestConverted
116 | }
117 | 
118 | func (m Renderer) calcLight(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) {
119 | 	if _, _, fgBg, _ := m.Settings.Characters.Selected(); fgBg == characters.OneColor {
120 | 		avg, dist := m.avgCol(r1, r2, r3, r4)
121 | 		return avg, colorful.Color{}, math.Min(1.0, math.Abs(dist-1))
122 | 	} else {
123 | 		_, dark := lightDark(r1, r2, r3, r4)
124 | 		avg := m.avgColTrue(r1, r2, r3, r4)
125 | 
126 | 		if m.Settings.Colors.IsLimited() {
127 | 			avg, dark = m.getLightDarkPaletted(avg, dark)
128 | 		}
129 | 
130 | 		dist := avg.DistanceLuv(black)
131 | 		return avg, dark, math.Min(1.0, math.Abs(dist))
132 | 	}
133 | }
134 | 
135 | func (m Renderer) calcMed(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) {
136 | 	if _, _, fgBg, _ := m.Settings.Characters.Selected(); fgBg == characters.OneColor {
137 | 		avg, dist := m.avgCol(r1, r2, r3, r4)
138 | 		return avg, colorful.Color{}, math.Min(1.0, math.Abs(dist-1))
139 | 	} else {
140 | 		_, dark := lightDark(r1, r2, r3, r4)
141 | 		avg := m.avgColTrue(r1, r2, r3, r4)
142 | 
143 | 		if m.Settings.Colors.IsLimited() {
144 | 			avg, dark = m.getLightDarkPaletted(avg, dark)
145 | 		}
146 | 
147 | 		dist := avg.DistanceLuv(black)
148 | 		return avg, dark, math.Min(1.0, math.Abs(dist-0.5))
149 | 	}
150 | }
151 | 
152 | func (m Renderer) calcHeavy(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) {
153 | 	if _, _, fgBg, _ := m.Settings.Characters.Selected(); fgBg == characters.OneColor {
154 | 		avg, dist := m.avgCol(r1, r2, r3, r4)
155 | 		return avg, colorful.Color{}, math.Min(1.0, math.Abs(dist-1))
156 | 	} else {
157 | 		_, dark := lightDark(r1, r2, r3, r4)
158 | 		avg := m.avgColTrue(r1, r2, r3, r4)
159 | 
160 | 		if m.Settings.Colors.IsLimited() {
161 | 			avg, dark = m.getLightDarkPaletted(avg, dark)
162 | 		}
163 | 
164 | 		dist := avg.DistanceLuv(black)
165 | 		return avg, dark, math.Min(1.0, math.Abs(dist-1))
166 | 	}
167 | }
168 | 
169 | func (m Renderer) calcFull(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) {
170 | 	if _, _, fgBg, _ := m.Settings.Characters.Selected(); fgBg == characters.OneColor {
171 | 		avg, _ := m.avgCol(r1, r2, r3, r4)
172 | 		return avg, colorful.Color{}, 1.0
173 | 	} else {
174 | 		_, dark := lightDark(r1, r2, r3, r4)
175 | 		avg := m.avgColTrue(r1, r2, r3, r4)
176 | 
177 | 		if m.Settings.Colors.IsLimited() {
178 | 			avg, dark = m.getLightDarkPaletted(avg, dark)
179 | 		}
180 | 
181 | 		dist := avg.DistanceLuv(black)
182 | 		return avg, dark, math.Min(1.0, math.Abs(dist-1))
183 | 	}
184 | }
185 | 
186 | func (m Renderer) calcTop(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) {
187 | 	if r1.R == 0 && r1.G == 0 && r1.B == 0 && (r3.R != 0 || r3.G != 0 || r3.B != 0) {
188 | 		r1.R = r1.G
189 | 	}
190 | 	fg, fDist := m.avgCol(r1, r2)
191 | 	bg, bDist := m.avgCol(r3, r4)
192 | 	return fg, bg, fDist + bDist
193 | }
194 | 
195 | func (m Renderer) calcRight(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) {
196 | 	fg, fDist := m.avgCol(r2, r4)
197 | 	bg, bDist := m.avgCol(r1, r3)
198 | 	return fg, bg, fDist + bDist
199 | }
200 | 
201 | func (m Renderer) calcDiagonal(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) {
202 | 	fg, fDist := m.avgCol(r2, r3)
203 | 	bg, bDist := m.avgCol(r1, r4)
204 | 	return fg, bg, fDist + bDist
205 | }
206 | 
207 | func (m Renderer) calcBotLeft(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) {
208 | 	fg, fDist := m.avgCol(r3)
209 | 	bg, bDist := m.avgCol(r1, r2, r4)
210 | 	return fg, bg, fDist + bDist
211 | }
212 | 
213 | func (m Renderer) calcTopLeft(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) {
214 | 	fg, fDist := m.avgCol(r1)
215 | 	bg, bDist := m.avgCol(r2, r3, r4)
216 | 	return fg, bg, fDist + bDist
217 | }
218 | 
219 | func (m Renderer) calcTopRight(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) {
220 | 	fg, fDist := m.avgCol(r2)
221 | 	bg, bDist := m.avgCol(r1, r3, r4)
222 | 	return fg, bg, fDist + bDist
223 | }
224 | 
225 | func (m Renderer) calcBotRight(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) {
226 | 	fg, fDist := m.avgCol(r4)
227 | 	bg, bDist := m.avgCol(r1, r2, r3)
228 | 	return fg, bg, fDist + bDist
229 | }
230 | 
```
Page 2/2FirstPrevNextLast