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 | ```