This is page 1 of 2. Use http://codebase.md/Zebbeni/ansizalizer?page={x} to view the full context. # Directory Structure ``` ├── .gitignore ├── ansizalizer ├── app │ ├── adapt │ │ └── generate.go │ ├── export.go │ ├── item.go │ ├── model.go │ ├── process │ │ ├── ascii.go │ │ ├── custom.go │ │ ├── image.go │ │ ├── renderer.go │ │ └── unicode.go │ ├── resize.go │ ├── update.go │ └── view.go ├── assets │ └── palettes │ ├── android-screenshot-editor.hex │ ├── cascade-gb.hex │ ├── dull-aquatic.hex │ ├── florescence.hex │ ├── gb-blue-steel.hex │ ├── hama-beads-tub.hex │ ├── kiwami64-v1.hex │ └── yes.hex ├── controls │ ├── browser │ │ ├── item.go │ │ ├── model.go │ │ └── update.go │ ├── export │ │ ├── destination │ │ │ ├── model.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── model.go │ │ ├── source │ │ │ ├── model.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── update.go │ │ └── view.go │ ├── menu │ │ └── model.go │ ├── model.go │ ├── settings │ │ ├── advanced │ │ │ ├── dithering │ │ │ │ ├── list.go │ │ │ │ ├── model.go │ │ │ │ ├── update.go │ │ │ │ └── view.go │ │ │ ├── model.go │ │ │ ├── sampling │ │ │ │ ├── const.go │ │ │ │ ├── item.go │ │ │ │ ├── model.go │ │ │ │ └── update.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── characters │ │ │ ├── init.go │ │ │ ├── model.go │ │ │ ├── tabs.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── colors │ │ │ ├── model.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── item.go │ │ ├── model.go │ │ ├── palettes │ │ │ ├── adaptive │ │ │ │ ├── init.go │ │ │ │ ├── model.go │ │ │ │ ├── update.go │ │ │ │ └── view.go │ │ │ ├── loader │ │ │ │ ├── item.go │ │ │ │ ├── model.go │ │ │ │ ├── values.go │ │ │ │ └── view.go │ │ │ ├── lospec │ │ │ │ ├── init.go │ │ │ │ ├── list.go │ │ │ │ ├── model.go │ │ │ │ ├── update.go │ │ │ │ └── view.go │ │ │ ├── matrix.go │ │ │ ├── model.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── size │ │ │ ├── init.go │ │ │ ├── model.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── state.go │ │ ├── update.go │ │ └── view.go │ ├── update.go │ └── view.go ├── display │ └── model.go ├── env │ ├── os_darwin.go │ ├── os_linux.go │ └── os_windows.go ├── event │ ├── command.go │ └── keymap.go ├── global │ └── file.go ├── go.mod ├── go.sum ├── images │ └── characters │ ├── char_001.png │ ├── char_002.png │ ├── char_003.png │ ├── char_004.png │ ├── char_005.png │ ├── char_006.png │ ├── char_007.png │ ├── char_008.png │ ├── char_009.png │ ├── char_010.png │ ├── char_011.png │ ├── char_012.png │ ├── char_013.png │ ├── char_014.png │ ├── char_015.png │ ├── char_016.png │ ├── char_017.png │ ├── char_018.png │ ├── char_019.png │ ├── char_020.png │ ├── char_021.png │ ├── char_022.png │ ├── char_023.png │ ├── char_024.png │ ├── char_025.png │ ├── char_026.png │ ├── char_027.png │ └── char_028.png ├── LICENSE.md ├── main.go ├── palette │ ├── model.go │ └── view.go ├── README.md ├── style │ ├── box.go │ └── color.go ├── test_images │ ├── dock.png │ ├── mermaid.png │ ├── mona_lisa.jpg │ ├── planet.png │ ├── robots.png │ ├── sewer.png │ └── throne.png └── viewer ├── model.go └── update.go ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Images test directory images/* # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib *.idea/ *.hex *.ansi # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # ANSIZALIZER A TUI to convert Images to ANSI strings using bubbletea  ## Features - A keyboard-navigable Text-based UI - File browser: Search .png and .jpeg image files and preview in real-time - Export ANSI image strings to '.ansi' text files or copy directly to your Clipboard - Save files individually or Batch Process All Images in a chosen directory - Browse Lospec.com for cool color palettes ## Render Options - Set output Width and Height of rendered text images (in characters) - Choose character sets to use in output (ASCII, Unicode, or Custom) - Render images with "true" colors or convert using Limited Color Palettes - Generate new color palettes by sampling previewed image files - Use Advanced settings to tweak pixel Sampling mode and Dithering options  ## To Run **On Windows:** ```bash go install go build start ansizalizer.exe ``` **On Mac/Linux:** ```bash go install go build ./ansizalizer ```  ## FAQ / Troubleshooting **Q: The UI isn't rendering correctly** Check your default console appearance settings. Make sure your chosen font, font size, and line height aren't the cause of the problem. 'DejaVu Sans Mono' works well for me on Windows. **Q: My images look squashed / stretched** Try adjusting the value of Char Size Ratio under Settings > Size. Depending on what font your console uses, your characters may have a width-to-height ratio different than 0.5. **Q: My exported .ansi files take up more space than the original image** The ANSI code that produces the text-rendered images isn't (currently) optimized for file size. If using this tool to batch process lots of text art for use in a game or application, I'd consider compressing the resulting text files and decompressing them as needed. ``` -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- ```markdown MIT License Copyright (c) 2024 Andrew Albers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -------------------------------------------------------------------------------- /env/os_darwin.go: -------------------------------------------------------------------------------- ```go //go:build darwin package env const PollForSizeChange = false ``` -------------------------------------------------------------------------------- /env/os_linux.go: -------------------------------------------------------------------------------- ```go //go:build linux package env const PollForSizeChange = false ``` -------------------------------------------------------------------------------- /env/os_windows.go: -------------------------------------------------------------------------------- ```go //go:build windows package env const PollForSizeChange = true ``` -------------------------------------------------------------------------------- /global/file.go: -------------------------------------------------------------------------------- ```go package global var ( ImgExtensions = map[string]bool{".png": true, ".jpg": true, ".jpeg": true} ) ``` -------------------------------------------------------------------------------- /controls/settings/palettes/loader/item.go: -------------------------------------------------------------------------------- ```go package loader import ( "github.com/Zebbeni/ansizalizer/palette" ) type item struct { palette palette.Model } func (i item) FilterValue() string { return i.palette.Name() } func (i item) Title() string { return i.palette.Name() } func (i item) Description() string { return i.palette.View() } ``` -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- ```go package main import ( "fmt" "os" tea "github.com/charmbracelet/bubbletea" "github.com/Zebbeni/ansizalizer/app" "github.com/Zebbeni/ansizalizer/event" ) func init() { event.InitKeyMap() } func main() { m := app.New() p := tea.NewProgram(m) if _, err := p.Run(); err != nil { fmt.Println("Error running program:", err) os.Exit(1) } } ``` -------------------------------------------------------------------------------- /controls/settings/state.go: -------------------------------------------------------------------------------- ```go package settings type State int const ( None State = iota Colors Characters Size Advanced ) var States = []State{ Colors, Characters, Size, Advanced, } var stateOrder = []State{Colors, Characters, Size, Advanced} var stateTitles = map[State]string{ Colors: "Colors", Characters: "Characters", Size: "Size", Advanced: "Advanced", } ``` -------------------------------------------------------------------------------- /viewer/update.go: -------------------------------------------------------------------------------- ```go package viewer import ( "fmt" "path/filepath" tea "github.com/charmbracelet/bubbletea" "github.com/Zebbeni/ansizalizer/event" ) func (m Model) handleFinishRenderMsg(msg event.FinishRenderToViewMsg) (Model, tea.Cmd) { m.WaitingOnRender = false m.imgString = msg.ImgString displayMsg := fmt.Sprintf("viewing %s/%s with %s palette", filepath.Base(filepath.Dir(msg.FilePath)), filepath.Base(msg.FilePath), msg.ColorsString) return m, event.BuildDisplayCmd(displayMsg) } ``` -------------------------------------------------------------------------------- /controls/settings/item.go: -------------------------------------------------------------------------------- ```go package settings //type item struct { // name string // state State //} // //func (i item) FilterValue() string { // return i.name //} // //func (i item) Title() string { // return i.name //} // //func (i item) Description() string { // return "" //} //func newMenu() list.Model { // items := []list.Item{ // item{name: "Loader", state: Loader}, // item{name: "Advanced", state: Advanced}, // //item{name: "Limited", state: Limited}, // //item{name: "Characters", state: Characters}, // } // return menu.New(items) //} ``` -------------------------------------------------------------------------------- /controls/settings/palettes/adaptive/init.go: -------------------------------------------------------------------------------- ```go package adaptive import ( "fmt" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/lipgloss" ) var ( promptStyle = lipgloss.NewStyle().Width(8).PaddingLeft(1) placeholderStyle = lipgloss.NewStyle() ) func newInput(state State) textinput.Model { textinput.New() input := textinput.New() input.Prompt = stateNames[state] input.PromptStyle = promptStyle input.PlaceholderStyle = placeholderStyle input.Cursor.Blink = true input.CharLimit = 3 input.SetValue(fmt.Sprintf("16")) return input } ``` -------------------------------------------------------------------------------- /controls/settings/advanced/sampling/const.go: -------------------------------------------------------------------------------- ```go package sampling import "github.com/nfnt/resize" var Functions = []resize.InterpolationFunction{ resize.NearestNeighbor, resize.Bicubic, resize.Bilinear, resize.Lanczos2, resize.Lanczos3, resize.MitchellNetravali, } var nameMap = map[resize.InterpolationFunction]string{ resize.NearestNeighbor: "Nearest Neighbor", resize.Bicubic: "Bicubic", resize.Bilinear: "Bilinear", resize.Lanczos2: "Lanczos2", resize.Lanczos3: "Lanczos3", resize.MitchellNetravali: "MitchellNetravali", } ``` -------------------------------------------------------------------------------- /viewer/model.go: -------------------------------------------------------------------------------- ```go package viewer import ( tea "github.com/charmbracelet/bubbletea" "github.com/Zebbeni/ansizalizer/controls/settings" "github.com/Zebbeni/ansizalizer/event" ) type Model struct { imgString string settings settings.Model WaitingOnRender bool } func New() Model { return Model{} } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { switch msg := msg.(type) { case event.FinishRenderToViewMsg: return m.handleFinishRenderMsg(msg) } return m, nil } func (m Model) View() string { if m.WaitingOnRender { return "" } return m.imgString } ``` -------------------------------------------------------------------------------- /controls/settings/characters/init.go: -------------------------------------------------------------------------------- ```go package characters import ( "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/style" ) var ( promptStyle = lipgloss.NewStyle().Padding(0, 1, 0, 1) placeholderStyle = lipgloss.NewStyle() ) // TODO: This is basically the same as we have in adaptive. Maybe generalize? func newInput(prompt string, value string) textinput.Model { textinput.New() input := textinput.New() input.Prompt = prompt input.PromptStyle = style.NormalButtonNode.Copy().Padding(0, 1, 0, 0) input.PlaceholderStyle = placeholderStyle input.Cursor.Blink = true input.SetValue(value) return input } ``` -------------------------------------------------------------------------------- /controls/settings/palettes/matrix.go: -------------------------------------------------------------------------------- ```go package palettes import ( "github.com/makeworld-the-better-one/dither/v2" ) type Matrix struct { Name string Method dither.ErrorDiffusionMatrix } func getMatrixMenuItems() []Matrix { return []Matrix{ Matrix{Name: "Simple2D", Method: dither.Simple2D}, Matrix{Name: "FloydSteinberg", Method: dither.FloydSteinberg}, Matrix{Name: "JarvisJudiceNinke", Method: dither.JarvisJudiceNinke}, Matrix{Name: "Atkinson", Method: dither.Atkinson}, Matrix{Name: "Stucki", Method: dither.Stucki}, Matrix{Name: "Burkes", Method: dither.Burkes}, Matrix{Name: "Sierra", Method: dither.Sierra}, Matrix{Name: "StevenPigeon", Method: dither.StevenPigeon}, } } ``` -------------------------------------------------------------------------------- /app/adapt/generate.go: -------------------------------------------------------------------------------- ```go package adapt import ( "bufio" "image" "image/color" "os" "path/filepath" "strings" "github.com/mccutchen/palettor" "github.com/Zebbeni/ansizalizer/controls/settings/palettes/adaptive" ) func GeneratePalette(m adaptive.Model, imgFilePath string) (color.Palette, string) { if imgFilePath == "" { return nil, "" } var img image.Image imgFile, err := os.Open(imgFilePath) if err != nil { return nil, "" } defer imgFile.Close() imageReader := bufio.NewReader(imgFile) img, _, err = image.Decode(imageReader) if err != nil { return nil, "" } count, iterations := m.Info() palette, err := palettor.Extract(count, iterations, img) name := strings.Split(filepath.Base(imgFilePath), ".")[0] return palette.Colors(), name } ``` -------------------------------------------------------------------------------- /app/item.go: -------------------------------------------------------------------------------- ```go package app import "github.com/charmbracelet/bubbles/list" type item struct { name string state State } func (i item) FilterValue() string { return i.name } func (i item) Title() string { return i.name } func (i item) Description() string { return "" } func newMenu() list.Model { items := []list.Item{ item{name: "File", state: Browser}, item{name: "Settings", state: Settings}, } menu := list.New(items, NewDelegate(), 20, 20) menu.SetShowHelp(false) menu.SetShowFilter(false) menu.SetShowTitle(false) menu.SetShowStatusBar(false) menu.KeyMap.ForceQuit.Unbind() menu.KeyMap.Quit.Unbind() return menu } func NewDelegate() list.DefaultDelegate { delegate := list.NewDefaultDelegate() delegate.SetSpacing(0) delegate.ShowDescription = false return delegate } ``` -------------------------------------------------------------------------------- /controls/settings/palettes/lospec/init.go: -------------------------------------------------------------------------------- ```go package lospec import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/bubbles/textinput" ) var ( promptStyle = lipgloss.NewStyle().Padding(0, 1, 0, 1) placeholderStyle = lipgloss.NewStyle() ) // TODO: This is basically the same as we have in adaptive. Maybe generalize? func newInput(state State, value string) textinput.Model { textinput.New() input := textinput.New() input.Prompt = stateNames[state] input.PromptStyle = promptStyle input.PlaceholderStyle = placeholderStyle input.Cursor.Blink = true input.SetValue(value) return input } func (m Model) InitializeList() (Model, tea.Cmd) { m.didInitializeList = true return m.searchLospec(0) } func (m Model) DidInitializeList() bool { return m.didInitializeList } ``` -------------------------------------------------------------------------------- /display/model.go: -------------------------------------------------------------------------------- ```go package display import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/event" "github.com/Zebbeni/ansizalizer/style" ) type Model struct { msg string width int } func New() Model { return Model{} } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { switch msg := msg.(type) { case event.DisplayMsg: m.msg = string(msg) } return m, nil } func (m Model) View() string { // TODO: Switch style based on event type (warning, info, etc.) displayStyle := style.ExtraDimTitle.Copy().Width(m.width - 2) return displayStyle.Border(lipgloss.RoundedBorder()).BorderForeground(style.ExtraDimColor).Render(m.msg) } func (m Model) SetWidth(w int) Model { m.width = w return m } ``` -------------------------------------------------------------------------------- /controls/settings/view.go: -------------------------------------------------------------------------------- ```go package settings import ( "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/style" ) var ( activeColor = lipgloss.Color("#aaaaaa") focusColor = lipgloss.Color("#ffffff") normalColor = lipgloss.Color("#555555") ) func (m Model) renderWithBorder(content string, state State) string { renderColor := normalColor if m.active == state { renderColor = activeColor } else if m.focus == state { renderColor = focusColor } textStyle := lipgloss.NewStyle(). AlignHorizontal(lipgloss.Center). Padding(0, 1, 0, 1). Foreground(renderColor) borderStyle := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(renderColor) renderer := style.BoxWithLabel{ BoxStyle: borderStyle, LabelStyle: textStyle, } return renderer.Render(stateTitles[state], content, m.width-2) } ``` -------------------------------------------------------------------------------- /controls/settings/advanced/sampling/update.go: -------------------------------------------------------------------------------- ```go package sampling import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/Zebbeni/ansizalizer/event" ) func (m Model) handleEsc() (Model, tea.Cmd) { m.ShouldClose = true m.list.SetDelegate(NewDelegate(false)) return m, nil } func (m Model) handleEnter() (Model, tea.Cmd) { m.ShouldClose = true m.list.SetDelegate(NewDelegate(false)) return m, nil } func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { if key.Matches(msg, event.KeyMap.Up) && m.list.Index() == 0 { m.list.SetDelegate(NewDelegate(false)) m.ShouldClose = true return m, nil } var cmd tea.Cmd m.list, cmd = m.list.Update(msg) m.list.SetDelegate(NewDelegate(true)) selectedItem := m.list.SelectedItem().(item) if selectedItem.Function == m.Function { return m, cmd } m.Function = selectedItem.Function return m, tea.Batch(cmd, event.StartRenderToViewCmd) } ``` -------------------------------------------------------------------------------- /controls/settings/size/init.go: -------------------------------------------------------------------------------- ```go package size import ( "fmt" "strconv" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/lipgloss" ) var ( promptStyle = lipgloss.NewStyle().Width(8).Padding(0, 0, 0, 1) placeholderStyle = lipgloss.NewStyle() floatPromptStyle = lipgloss.NewStyle().Padding(0, 1) floatPlaceholderStyle = lipgloss.NewStyle() ) func newInput(state State, value int) textinput.Model { textinput.New() input := textinput.New() input.Prompt = stateNames[state] input.PromptStyle = promptStyle input.PlaceholderStyle = placeholderStyle input.CharLimit = 3 input.SetValue(strconv.Itoa(value)) return input } func newFloatInput(state State, value float64) textinput.Model { textinput.New() input := textinput.New() input.Prompt = stateNames[state] input.PromptStyle = floatPromptStyle input.PlaceholderStyle = floatPlaceholderStyle input.CharLimit = 5 input.SetValue(fmt.Sprintf("%1.2f", value)) return input } ``` -------------------------------------------------------------------------------- /controls/settings/colors/view.go: -------------------------------------------------------------------------------- ```go package colors import ( "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/style" ) func (m Model) drawPaletteToggles() string { title := style.DimmedTitle.Copy().PaddingLeft(1).Render("Mode:") trueColorStyle := style.NormalButtonNode if m.IsActive && m.focus == UseTrueColor { trueColorStyle = style.FocusButtonNode } else if m.mode == UseTrueColor { trueColorStyle = style.ActiveButtonNode } trueColorNode := trueColorStyle.Render("True Color") trueColorNode = lipgloss.NewStyle().PaddingLeft(1).Render(trueColorNode) palettedStyle := style.NormalButtonNode if m.IsActive && m.focus == UsePalette { palettedStyle = style.FocusButtonNode } else if m.mode == UsePalette { palettedStyle = style.ActiveButtonNode } palettedNode := palettedStyle.Render("Palette") palettedNode = lipgloss.NewStyle().PaddingLeft(1).Render(palettedNode) return lipgloss.JoinHorizontal(lipgloss.Left, title, trueColorNode, palettedNode) } ``` -------------------------------------------------------------------------------- /palette/view.go: -------------------------------------------------------------------------------- ```go package palette import ( "image/color" "math" "github.com/charmbracelet/lipgloss" "github.com/lucasb-eyer/go-colorful" ) func Palette(palette color.Palette, w, h int) string { runes := make([]string, len(palette)/2+1) rows := make([]string, 0, h) for idx := 0; idx < len(palette); idx += 2 { var fg, bg colorful.Color var lipFg, lipBg lipgloss.Color fg, _ = colorful.MakeColor(palette[idx]) lipFg = lipgloss.Color(fg.Hex()) style := lipgloss.NewStyle().Foreground(lipFg) if idx+1 < len(palette) { bg, _ = colorful.MakeColor(palette[idx+1]) lipBg = lipgloss.Color(bg.Hex()) style = style.Copy().Background(lipBg) } runes[idx/2] = style.Render(string('▀')) } for i := 0; i < h; i++ { start := w * i if start >= len(runes) { break } stop := int(math.Min(float64(w*(i+1)), float64(len(runes)))) rows = append(rows, "") rows[i] = lipgloss.JoinHorizontal(lipgloss.Left, runes[start:stop]...) } return lipgloss.JoinVertical(lipgloss.Left, rows...) } ``` -------------------------------------------------------------------------------- /controls/export/destination/view.go: -------------------------------------------------------------------------------- ```go package destination import ( "fmt" "path/filepath" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/style" ) func (m Model) drawSelected() string { title := style.DimmedTitle.Copy().Render("Selected") valueStyle := style.DimmedTitle.Copy() if Input == m.focus { if m.IsActive { valueStyle = style.SelectedTitle.Copy() } else { valueStyle = style.NormalTitle.Copy() } } valueStyle.Padding(0, 0, 1, 0) path := m.Browser.SelectedDir parent := filepath.Base(filepath.Dir(path)) selected := filepath.Base(path) value := fmt.Sprintf("%s/%s", parent, selected) valueRunes := []rune(value) if len(valueRunes) > m.width { value = string(valueRunes[len(valueRunes)-m.width:]) } valueContent := valueStyle.Render(value) valueWidth := m.width widthStyle := lipgloss.NewStyle().Width(valueWidth).AlignHorizontal(lipgloss.Center) content := lipgloss.JoinVertical(lipgloss.Center, title, valueContent) return widthStyle.Render(content) } func drawBrowserTitle() string { return style.DimmedTitle.Copy().Padding(0, 2, 1, 2).Render("Select a directory") } ``` -------------------------------------------------------------------------------- /controls/export/destination/update.go: -------------------------------------------------------------------------------- ```go package destination import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/Zebbeni/ansizalizer/event" ) type Direction int const ( Up Direction = iota Down ) var ( navMap = map[Direction]map[State]State{ Down: {Input: Browser}, Up: {Browser: Input}, } ) func (m Model) handleEsc() (Model, tea.Cmd) { m.ShouldClose = true m.IsActive = false return m, nil } func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { switch { case key.Matches(msg, event.KeyMap.Down): if next, hasNext := navMap[Down][m.focus]; hasNext { m.focus = next } case key.Matches(msg, event.KeyMap.Up): if next, hasNext := navMap[Up][m.focus]; hasNext { m.focus = next } else { m.ShouldClose = true } } return m, nil } func (m Model) handleEnter() (Model, tea.Cmd) { switch m.focus { case Input: m.focus = Browser } return m, nil } func (m Model) handleDstBrowserUpdate(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.Browser, cmd = m.Browser.Update(msg) m.selectedDir = m.Browser.SelectedDir if m.Browser.ShouldClose { m.focus = Input m.Browser.ShouldClose = false } return m, cmd } ``` -------------------------------------------------------------------------------- /app/process/image.go: -------------------------------------------------------------------------------- ```go package process import ( "bufio" "image" "os" "github.com/lucasb-eyer/go-colorful" "github.com/Zebbeni/ansizalizer/controls/settings" "github.com/Zebbeni/ansizalizer/controls/settings/characters" ) var ( black = colorful.Color{} ) func RenderImageFile(s settings.Model, imgFilePath string) string { if imgFilePath == "" { return "Browse an image to render" } var img image.Image imgFile, err := os.Open(imgFilePath) if err != nil { return "Could not open image " + imgFilePath } defer imgFile.Close() imageReader := bufio.NewReader(imgFile) img, _, err = image.Decode(imageReader) if err != nil { return "Could not decode image " + imgFilePath } renderer := New(s) imgString := renderer.process(img) return imgString } func (m Renderer) process(input image.Image) string { isTrueColor, _, palette := m.Settings.Colors.GetSelected() if !isTrueColor && len(palette.Colors()) == 0 { return "Choose a color palette" } mode, _, _, _ := m.Settings.Characters.Selected() switch mode { case characters.Ascii: return m.processAscii(input) case characters.Unicode: return m.processUnicode(input) case characters.Custom: return m.processCustom(input) } return "Choose a character type" } ``` -------------------------------------------------------------------------------- /controls/settings/palettes/lospec/list.go: -------------------------------------------------------------------------------- ```go package lospec import ( "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/style" ) func CreateList(items []list.Item, w int) list.Model { newList := list.New(items, NewDelegate(), w, 22) newList.KeyMap.ForceQuit.Unbind() newList.KeyMap.Quit.Unbind() newList.SetShowHelp(false) newList.SetShowStatusBar(false) newList.SetShowTitle(false) newList.SetFilteringEnabled(false) return newList } func NewDelegate() list.DefaultDelegate { delegate := list.NewDefaultDelegate() delegate.SetSpacing(0) delegate.ShowDescription = true delegate.Styles = ItemStyles() return delegate } func ItemStyles() (s list.DefaultItemStyles) { s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2) s.NormalDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2) s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1). Border(lipgloss.NormalBorder(), false, false, false, true). BorderForeground(style.SelectedColor1) s.SelectedDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2) s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0) s.DimmedDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2) return s } ``` -------------------------------------------------------------------------------- /event/keymap.go: -------------------------------------------------------------------------------- ```go package event import ( "github.com/charmbracelet/bubbles/key" ) type Map struct { Enter key.Binding Nav key.Binding Right key.Binding Left key.Binding Up key.Binding Down key.Binding Copy key.Binding Save key.Binding Esc key.Binding } var KeyMap Map func InitKeyMap() { KeyMap = Map{ Enter: key.NewBinding( key.WithKeys("return", "enter"), key.WithHelp("↲/enter", "select/focus menu"), ), Nav: key.NewBinding( key.WithKeys("up", "down", "right", "left"), key.WithHelp("↕/↔", "navigate"), ), Right: key.NewBinding( key.WithKeys("right"), ), Left: key.NewBinding( key.WithKeys("left"), ), Up: key.NewBinding( key.WithKeys("up"), ), Down: key.NewBinding( key.WithKeys("down"), ), Copy: key.NewBinding( key.WithKeys("ctrl+c"), key.WithHelp("ctrl+c", "copy to clipboard")), Save: key.NewBinding( key.WithKeys("ctrl+s"), key.WithHelp("ctrl+s", "save to file")), Esc: key.NewBinding( key.WithKeys("esc"), key.WithHelp("esc", "back/exit menu"), ), } } func (k Map) ShortHelp() []key.Binding { return []key.Binding{k.Nav, k.Enter, k.Esc, k.Copy, k.Save} } func (k Map) FullHelp() [][]key.Binding { return [][]key.Binding{{k.Nav, k.Enter, k.Esc, k.Copy, k.Save}} } ``` -------------------------------------------------------------------------------- /controls/settings/advanced/sampling/model.go: -------------------------------------------------------------------------------- ```go package sampling import ( "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/nfnt/resize" "github.com/Zebbeni/ansizalizer/event" "github.com/Zebbeni/ansizalizer/style" ) type Model struct { Function resize.InterpolationFunction list list.Model IsActive bool ShouldClose bool } func New(w int) Model { items := menuItems() selected := items[0].(item) menu := newMenu(items, w, len(items)) return Model{ Function: selected.Function, list: menu, IsActive: false, ShouldClose: false, } } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch { case key.Matches(msg, event.KeyMap.Esc): return m.handleEsc() case key.Matches(msg, event.KeyMap.Enter): return m.handleEnter() case key.Matches(msg, event.KeyMap.Nav): return m.handleNav(msg) } } return m, nil } func (m Model) View() string { prompt := style.DimmedTitle.Copy().Render("Select Method") menu := m.list.View() content := lipgloss.JoinVertical(lipgloss.Left, prompt, menu) return lipgloss.NewStyle().Padding(0, 1).Render(content) } ``` -------------------------------------------------------------------------------- /app/resize.go: -------------------------------------------------------------------------------- ```go package app import ( "os" "time" "golang.org/x/term" tea "github.com/charmbracelet/bubbletea" ) // There is (currently) no support on Windows for detecting resize events, so // we instead poll at regular intervals to check if the terminal size changed. // If a resize is detected in this way, we send a WindowSizeMsg with the new // dimensions to bubbletea, and handle it in the Model event handler type checkSizeMsg int const ( resizeCheckDuration = time.Second / 4 ) func (m Model) handleSizeMsg(msg tea.WindowSizeMsg) (Model, tea.Cmd) { w, h := msg.Width, msg.Height m.w, m.h = w, h m.display = m.display.SetWidth(m.rPanelWidth()) tea.ClearScreen() return m, nil } func (m Model) handleCheckSizeMsg() (Model, tea.Cmd) { w, h, _ := term.GetSize(int(os.Stdout.Fd())) if w == m.w && h == m.h { return m, pollForSizeChange } updateSizeCmd := func() tea.Msg { return tea.WindowSizeMsg{Width: w, Height: h} } return m, tea.Batch(pollForSizeChange, updateSizeCmd) } func pollForSizeChange() tea.Msg { time.Sleep(resizeCheckDuration) return checkSizeMsg(1) } func (m Model) leftPanelHeight() int { return m.h - helpHeight } func (m Model) rPanelWidth() int { return m.w - controlsWidth } func (m Model) rPanelHeight() int { return m.h - helpHeight } ``` -------------------------------------------------------------------------------- /controls/export/view.go: -------------------------------------------------------------------------------- ```go package export import ( "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/style" ) var ( activeColor = lipgloss.Color("#aaaaaa") focusColor = lipgloss.Color("#ffffff") normalColor = lipgloss.Color("#555555") ) func (m Model) renderWithBorder(content string, state State) string { renderColor := normalColor if m.active == state { renderColor = activeColor } else if m.focus == state { renderColor = focusColor } textStyle := lipgloss.NewStyle(). AlignHorizontal(lipgloss.Center). Padding(0, 1, 0, 1). Foreground(renderColor) borderStyle := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(renderColor) renderer := style.BoxWithLabel{ BoxStyle: borderStyle, LabelStyle: textStyle, } return renderer.Render(stateTitles[state], content, m.width-2) } func (m Model) drawProcessButton() string { buttonStyle := style.NormalButton if m.focus == Process { buttonStyle = style.FocusButton } centerStyle := lipgloss.NewStyle().AlignHorizontal(lipgloss.Center) internalStyle := centerStyle.Copy().Width(m.width - 2) title := internalStyle.Render(stateTitles[Process]) button := buttonStyle.Render(title) return centerStyle.Copy().Width(m.width).AlignHorizontal(lipgloss.Center).Render(button) } ``` -------------------------------------------------------------------------------- /controls/settings/palettes/loader/view.go: -------------------------------------------------------------------------------- ```go package loader import ( "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/style" ) const ( maxWidth = 30 maxNormalHeight = 1 maxSelectedHeight = 2 ) // NewItemStyles returns style definitions for a default item. // DefaultItemView for when these come into play. func NewItemStyles() (s list.DefaultItemStyles) { s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2) s.NormalDesc = style.DimmedParagraph.Copy().MaxHeight(maxNormalHeight).Padding(0, 0, 0, 2) s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1). Border(lipgloss.NormalBorder(), false, false, false, true). BorderForeground(style.SelectedColor1) s.SelectedDesc = style.SelectedTitle.Copy().MaxHeight(maxSelectedHeight).Padding(0, 0, 0, 1). Border(lipgloss.NormalBorder(), false, false, false, true). BorderForeground(style.SelectedColor1) s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0) s.DimmedDesc = style.DimmedParagraph.Copy().MaxHeight(maxNormalHeight).Padding(0, 0, 0, 2) return s } func (m Model) drawTitle() string { title := style.DimmedTitle.Copy().Italic(true).Render("Load from .hex file") return lipgloss.NewStyle().Width(m.width).PaddingBottom(1).AlignHorizontal(lipgloss.Center).Render(title) } ``` -------------------------------------------------------------------------------- /controls/menu/model.go: -------------------------------------------------------------------------------- ```go package menu import ( "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/style" ) func New(items []list.Item, w int) list.Model { newList := list.New(items, NewDelegate(), w, 18) newList.KeyMap.ForceQuit.Unbind() newList.KeyMap.Quit.Unbind() newList.SetShowHelp(false) newList.SetShowStatusBar(false) newList.SetShowTitle(false) newList.SetFilteringEnabled(false) return newList } func NewDelegate() list.DefaultDelegate { delegate := list.NewDefaultDelegate() delegate.SetSpacing(0) delegate.ShowDescription = false delegate.Styles = ItemStyles() return delegate } func ItemStyles() (s list.DefaultItemStyles) { s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2) s.NormalDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2) s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1). Border(lipgloss.NormalBorder(), false, false, false, true). BorderForeground(style.SelectedColor1) s.NormalDesc = style.SelectedTitle.Copy().MaxHeight(1).Padding(0, 0, 0, 2). Border(lipgloss.NormalBorder(), false, false, false, true). BorderForeground(style.SelectedColor1) s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0) s.DimmedDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2) return s } ``` -------------------------------------------------------------------------------- /controls/settings/palettes/view.go: -------------------------------------------------------------------------------- ```go package palettes import "github.com/charmbracelet/lipgloss" var ( stateOrder = []State{Load, Adapt, Lospec} stateNames = map[State]string{ Load: "Load", Adapt: "Sample", Lospec: "Lospec", } activeStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("#aaaaaa")). Foreground(lipgloss.Color("#aaaaaa")) focusStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("#ffffff")). Foreground(lipgloss.Color("#ffffff")) normalStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("#555555")). Foreground(lipgloss.Color("#555555")) titleStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#888888")) ) func (m Model) drawTitle() string { return titleStyle.Copy().Italic(true).Width(m.width).Align(lipgloss.Center).Render("Colors") } func (m Model) drawButtons() string { buttons := make([]string, len(stateOrder)) for i, state := range stateOrder { style := normalStyle if m.IsActive && state == m.focus { style = focusStyle } else if state == m.selected { style = activeStyle } buttons[i] = style.Copy().AlignHorizontal(lipgloss.Center).Padding(0, 1).Render(stateNames[state]) } return lipgloss.JoinHorizontal(lipgloss.Left, buttons...) } ``` -------------------------------------------------------------------------------- /controls/browser/item.go: -------------------------------------------------------------------------------- ```go package browser import ( "fmt" "os" "path/filepath" "github.com/charmbracelet/bubbles/list" ) type item struct { name string path string isDir bool isTop bool } func (i item) FilterValue() string { return i.name } func (i item) Title() string { if i.isTop { return "↑" } if i.isDir { return fmt.Sprintf("%s/", i.name) } return i.name } func (i item) Description() string { if i.isDir { return "directory" } return "file" } func getItems(extensions map[string]bool, dir string) []list.Item { entries, err := os.ReadDir(dir) if err != nil { fmt.Println("Error reading directory entries:", err) os.Exit(1) } parentPath := filepath.Dir(dir) parentName := filepath.Base(parentPath) parentItem := item{name: parentName, path: parentPath, isDir: true, isTop: true} dirItems := []list.Item{parentItem} fileItems := make([]list.Item, 0) for _, e := range entries { path := fmt.Sprintf("%s/%s", dir, e.Name()) if e.IsDir() { name := e.Name() dirItem := item{name: name, path: path, isDir: true, isTop: false} dirItems = append(dirItems, dirItem) continue } ext := filepath.Ext(e.Name()) if _, ok := extensions[ext]; ok { fileItem := item{name: e.Name(), path: path, isDir: false, isTop: false} fileItems = append(fileItems, fileItem) } } return append(dirItems, fileItems...) } ``` -------------------------------------------------------------------------------- /controls/export/model.go: -------------------------------------------------------------------------------- ```go package export import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/controls/export/destination" "github.com/Zebbeni/ansizalizer/controls/export/source" ) type State int const ( None State = iota Source Destination Process ) var ( stateTitles = map[State]string{ Source: "Source", Destination: "Destination", Process: "Process", } ) type Model struct { active State focus State Source source.Model Destination destination.Model ShouldClose bool ShouldUnfocus bool width int } func New(w int) Model { return Model{ focus: Source, active: None, Source: source.New(w - 2), Destination: destination.New(w - 2), ShouldClose: false, ShouldUnfocus: false, width: w, } } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { switch m.active { case Source: return m.handleSourceUpdate(msg) case Destination: return m.handleDestinationUpdate(msg) } keyMsg, ok := msg.(tea.KeyMsg) if !ok { return m, nil } return m.handleKeyMsg(keyMsg) } func (m Model) View() string { src := m.renderWithBorder(m.Source.View(), Source) dst := m.renderWithBorder(m.Destination.View(), Destination) process := m.drawProcessButton() return lipgloss.JoinVertical(lipgloss.Left, src, dst, process) } ``` -------------------------------------------------------------------------------- /controls/settings/advanced/dithering/model.go: -------------------------------------------------------------------------------- ```go package dithering import ( "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/makeworld-the-better-one/dither/v2" ) type State int const ( DitherOn State = iota DitherOff SerpentineOn SerpentineOff Matrix ) type Model struct { focus State doDithering bool doSerpentine bool matrix dither.ErrorDiffusionMatrix list list.Model IsActive bool ShouldClose bool width int } func New(w int) Model { return Model{ focus: DitherOff, doDithering: false, doSerpentine: false, matrix: dither.FloydSteinberg, list: newMatrixMenu(w), ShouldClose: false, IsActive: false, width: w, } } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { if m.focus == Matrix { return m.handleMatrixListUpdate(msg) } if keyMsg, ok := msg.(tea.KeyMsg); ok { return m.handleKeyMsg(keyMsg) } return m, nil } func (m Model) View() string { ditheringOpts := m.drawDitheringOptions() serpentineOpts := m.drawSerpentineOptions() matrixList := m.drawMatrix() content := lipgloss.JoinVertical(lipgloss.Left, ditheringOpts, serpentineOpts, matrixList) return lipgloss.NewStyle().Padding(0, 1).Render(content) } func (m Model) Settings() (bool, bool, dither.ErrorDiffusionMatrix) { return m.doDithering, m.doSerpentine, m.matrix } ``` -------------------------------------------------------------------------------- /controls/browser/model.go: -------------------------------------------------------------------------------- ```go package browser import ( "fmt" "os" "path/filepath" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/event" ) type Model struct { SelectedDir string SelectedFile string ActiveDir string ActiveFile string lists []list.Model fileExtensions map[string]bool ShouldClose bool width int } func New(exts map[string]bool, w int) Model { dir, err := os.Getwd() if err != nil { fmt.Println("Error getting starting directory:", err) os.Exit(1) } m := Model{ width: w, fileExtensions: exts, } m = m.addListForDirectory(dir) return m } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch { case key.Matches(msg, event.KeyMap.Esc): return m.handleEsc() case key.Matches(msg, event.KeyMap.Nav): return m.handleNav(msg) case key.Matches(msg, event.KeyMap.Enter): return m.handleEnter() } } return m, nil } func (m Model) currentList() list.Model { return m.lists[m.listIndex()] } func (m Model) listIndex() int { return len(m.lists) - 1 } func (m Model) View() string { browser := m.currentList().View() return lipgloss.JoinVertical(lipgloss.Left, browser) } func (m Model) ActiveFilename() string { return filepath.Base(m.ActiveFile) } ``` -------------------------------------------------------------------------------- /controls/export/destination/model.go: -------------------------------------------------------------------------------- ```go package destination import ( "os" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/controls/browser" "github.com/Zebbeni/ansizalizer/event" ) type State int const ( Input State = iota Browser ) type Model struct { focus State Browser browser.Model selectedDir string ShouldClose bool ShouldUnfocus bool IsActive bool width int } func New(w int) Model { filepath, _ := os.Getwd() return Model{ focus: Input, Browser: browser.New(nil, w-2), selectedDir: filepath, width: w, ShouldClose: false, } } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd switch m.focus { case Browser: return m.handleDstBrowserUpdate(msg) } switch msg := msg.(type) { case tea.KeyMsg: switch { case key.Matches(msg, event.KeyMap.Esc): return m.handleEsc() case key.Matches(msg, event.KeyMap.Nav): return m.handleNav(msg) case key.Matches(msg, event.KeyMap.Enter): return m.handleEnter() } } return m, cmd } func (m Model) View() string { content := make([]string, 0, 5) selected := lipgloss.NewStyle().PaddingTop(1).Render(m.drawSelected()) content = append(content, selected) if m.focus == Browser { content = append(content, m.Browser.View()) } return lipgloss.JoinVertical(lipgloss.Left, content...) } func (m Model) GetSelected() string { return m.selectedDir } ``` -------------------------------------------------------------------------------- /controls/settings/model.go: -------------------------------------------------------------------------------- ```go package settings import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/controls/settings/advanced" "github.com/Zebbeni/ansizalizer/controls/settings/characters" "github.com/Zebbeni/ansizalizer/controls/settings/colors" "github.com/Zebbeni/ansizalizer/controls/settings/size" ) type Model struct { active State focus State Colors colors.Model Characters characters.Model Size size.Model Advanced advanced.Model ShouldUnfocus bool ShouldClose bool width int } func New(w int) Model { return Model{ active: None, focus: Colors, Colors: colors.New(w), Characters: characters.New(w - 2), Size: size.New(), Advanced: advanced.New(w - 2), ShouldUnfocus: false, ShouldClose: false, width: w, } } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { switch m.active { case Colors: return m.handleColorsUpdate(msg) case Characters: return m.handleCharactersUpdate(msg) case Size: return m.handleSizeUpdate(msg) case Advanced: return m.handleAdvancedUpdate(msg) } keyMsg, ok := msg.(tea.KeyMsg) if !ok { return m, nil } return m.handleKeyMsg(keyMsg) } func (m Model) View() string { colorCtrls := m.Colors.View() charCtrls := m.Characters.View() sizeCtrls := m.Size.View() sampCtrls := m.Advanced.View() col := m.renderWithBorder(colorCtrls, Colors) char := m.renderWithBorder(charCtrls, Characters) siz := m.renderWithBorder(sizeCtrls, Size) sam := m.renderWithBorder(sampCtrls, Advanced) return lipgloss.JoinVertical(lipgloss.Top, col, char, siz, sam) } ``` -------------------------------------------------------------------------------- /controls/settings/advanced/model.go: -------------------------------------------------------------------------------- ```go package advanced import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/makeworld-the-better-one/dither/v2" "github.com/nfnt/resize" "github.com/Zebbeni/ansizalizer/controls/settings/advanced/dithering" "github.com/Zebbeni/ansizalizer/controls/settings/advanced/sampling" "github.com/Zebbeni/ansizalizer/event" ) type State int const ( Menu State = iota Sampling Dithering SamplingControls DitheringControls ) type Model struct { focus State active State activeTab State sampling sampling.Model dithering dithering.Model ShouldClose bool IsActive bool width int } func New(w int) Model { return Model{ focus: Sampling, active: Menu, activeTab: Sampling, sampling: sampling.New(w - 2), dithering: dithering.New(w - 2), ShouldClose: false, IsActive: false, width: w, } } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { switch m.active { case SamplingControls: return m.handleSamplingUpdate(msg) case DitheringControls: return m.handleDitheringUpdate(msg) } switch msg := msg.(type) { case tea.KeyMsg: switch { case key.Matches(msg, event.KeyMap.Enter): return m.handleEnter() case key.Matches(msg, event.KeyMap.Nav): return m.handleNav(msg) case key.Matches(msg, event.KeyMap.Esc): return m.handleEsc() } } return m, nil } func (m Model) View() string { return m.drawTabs() } func (m Model) SamplingFunction() resize.InterpolationFunction { return m.sampling.Function } func (m Model) Dithering() (bool, bool, dither.ErrorDiffusionMatrix) { return m.dithering.Settings() } ``` -------------------------------------------------------------------------------- /controls/settings/advanced/dithering/view.go: -------------------------------------------------------------------------------- ```go package dithering import ( "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/style" ) func (m Model) drawDitheringOptions() string { prompt := style.DimmedTitle.Render("Use Dithering:") prompt = lipgloss.NewStyle().Width(15).Render(prompt) nodeStyle := style.NormalButtonNode if m.IsActive && m.focus == DitherOn { nodeStyle = style.FocusButtonNode } else if m.doDithering { nodeStyle = style.ActiveButtonNode } onNode := lipgloss.NewStyle().Width(4).Render(nodeStyle.Copy().Render("On")) nodeStyle = style.NormalButtonNode if m.IsActive && m.focus == DitherOff { nodeStyle = style.FocusButtonNode } else if !m.doDithering { nodeStyle = style.ActiveButtonNode } offNode := nodeStyle.Copy().Render("Off") return lipgloss.JoinHorizontal(lipgloss.Left, prompt, onNode, offNode) } func (m Model) drawSerpentineOptions() string { prompt := style.DimmedTitle.Render("Do Serpentine:") prompt = lipgloss.NewStyle().Width(15).Render(prompt) nodeStyle := style.NormalButtonNode if m.IsActive && m.focus == SerpentineOn { nodeStyle = style.FocusButtonNode } else if m.doSerpentine { nodeStyle = style.ActiveButtonNode } onNode := lipgloss.NewStyle().Width(4).Render(nodeStyle.Copy().Render("On")) nodeStyle = style.NormalButtonNode if m.IsActive && m.focus == SerpentineOff { nodeStyle = style.FocusButtonNode } else if !m.doSerpentine { nodeStyle = style.ActiveButtonNode } offNode := nodeStyle.Copy().Render("Off") return lipgloss.JoinHorizontal(lipgloss.Left, prompt, onNode, offNode) } func (m Model) drawMatrix() string { prompt := style.DimmedTitle.Copy().PaddingTop(1).Render("Select Matrix") return lipgloss.JoinVertical(lipgloss.Left, prompt, m.list.View()) } ``` -------------------------------------------------------------------------------- /controls/update.go: -------------------------------------------------------------------------------- ```go package controls import ( "os" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/Zebbeni/ansizalizer/event" ) type Direction int const ( Left Direction = iota Right Up Down ) var navMap = map[Direction]map[State]State{ Right: {Browse: Settings, Settings: Export}, Left: {Export: Settings, Settings: Browse}, } func (m Model) handleOpenUpdate(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.FileBrowser, cmd = m.FileBrowser.Update(msg) if m.FileBrowser.ShouldClose { m.FileBrowser.ShouldClose = false m.active = Menu } return m, cmd } func (m Model) handleSettingsUpdate(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.Settings, cmd = m.Settings.Update(msg) if m.Settings.ShouldClose { m.Settings.ShouldClose = false m.active = Menu } return m, cmd } func (m Model) handleExportUpdate(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.Export, cmd = m.Export.Update(msg) if m.Export.ShouldClose { m.Export.ShouldClose = false m.active = Menu } return m, cmd } func (m Model) handleMenuUpdate(msg tea.Msg) (Model, tea.Cmd) { m.active = Menu switch msg := msg.(type) { case tea.KeyMsg: switch { case key.Matches(msg, event.KeyMap.Enter): m.active = m.focus case key.Matches(msg, event.KeyMap.Nav): switch { case key.Matches(msg, event.KeyMap.Right): if next, hasNext := navMap[Right][m.focus]; hasNext { m.focus = next } case key.Matches(msg, event.KeyMap.Left): if next, hasNext := navMap[Left][m.focus]; hasNext { m.focus = next } } case key.Matches(msg, event.KeyMap.Esc): // Quit program if top-level menu is active and escape pressed tea.Quit() os.Exit(0) } } return m, nil } ``` -------------------------------------------------------------------------------- /controls/model.go: -------------------------------------------------------------------------------- ```go package controls import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/controls/browser" "github.com/Zebbeni/ansizalizer/controls/export" "github.com/Zebbeni/ansizalizer/controls/settings" "github.com/Zebbeni/ansizalizer/global" ) type State int const ( Menu State = iota Browse Settings Export numButtons = 3 ) var ( stateOrder = []State{Browse, Settings, Export} stateNames = map[State]string{ Browse: "Browse", Settings: "Settings", Export: "Export", } ) type Model struct { active State focus State FileBrowser browser.Model Settings settings.Model Export export.Model width int } func New(w int) Model { return Model{ active: Menu, focus: Browse, FileBrowser: browser.New(global.ImgExtensions, w), Settings: settings.New(w), Export: export.New(w), width: w, } } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { switch m.active { case Browse: return m.handleOpenUpdate(msg) case Settings: return m.handleSettingsUpdate(msg) case Export: return m.handleExportUpdate(msg) } return m.handleMenuUpdate(msg) } // View displays a row of 3 buttons above 1 of 3 control panels: // Browse | Settings | Export func (m Model) View() string { title := m.drawTitle() // draw the top three buttons buttons := m.drawButtons() var controls string switch m.active { case Browse: browserTitle := m.drawBrowserTitle() controls = lipgloss.JoinVertical(lipgloss.Left, browserTitle, m.FileBrowser.View()) case Settings: controls = m.Settings.View() case Export: controls = m.Export.View() } return lipgloss.JoinVertical(lipgloss.Top, title, buttons, controls) } ``` -------------------------------------------------------------------------------- /controls/settings/advanced/sampling/item.go: -------------------------------------------------------------------------------- ```go package sampling import ( "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/lipgloss" "github.com/nfnt/resize" "github.com/Zebbeni/ansizalizer/style" ) type item struct { name string Function resize.InterpolationFunction } func (i item) FilterValue() string { return i.name } func (i item) Title() string { return i.name } func (i item) Description() string { return "" } func menuItems() []list.Item { items := make([]list.Item, len(nameMap)) for i, f := range Functions { items[i] = item{name: nameMap[f], Function: f} } return items } func newMenu(items []list.Item, width, height int) list.Model { l := list.New(items, NewDelegate(false), width, height) l.SetShowHelp(false) l.SetFilteringEnabled(false) l.SetShowTitle(false) l.SetShowPagination(false) l.SetShowStatusBar(false) l.KeyMap.ForceQuit.Unbind() l.KeyMap.Quit.Unbind() return l } func NewDelegate(isActive bool) list.DefaultDelegate { delegate := list.NewDefaultDelegate() delegate.SetSpacing(0) delegate.ShowDescription = false if isActive { delegate.Styles = ItemStylesActive() } else { delegate.Styles = ItemStylesInactive() } return delegate } func ItemStylesActive() (s list.DefaultItemStyles) { s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2) s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1). Border(lipgloss.NormalBorder(), false, false, false, true). BorderForeground(style.SelectedColor1) s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0) return s } func ItemStylesInactive() (s list.DefaultItemStyles) { s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2) s.SelectedTitle = style.NormalTitle.Copy().Padding(0, 1, 0, 2) s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0) return s } ``` -------------------------------------------------------------------------------- /app/view.go: -------------------------------------------------------------------------------- ```go package app import ( "fmt" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/viewport" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/event" "github.com/Zebbeni/ansizalizer/style" ) const ( displayHeight = 3 helpHeight = 1 controlsWidth = 30 ) func (m Model) renderControls() string { viewport := viewport.New(controlsWidth, m.leftPanelHeight()) leftContent := m.controls.View() viewport.SetContent(lipgloss.NewStyle(). Width(controlsWidth). Height(m.leftPanelHeight()). Render(leftContent)) return viewport.View() } func (m Model) renderViewer() string { imgString := m.viewer.View() imgWidth, imgHeight := lipgloss.Size(imgString) imgViewer := imgString // only render box label border around content if big enough. if imgHeight > 1 && imgWidth > 4 { boxLabelRenderer := style.BoxWithLabel{ BoxStyle: lipgloss.NewStyle().BorderForeground(style.ExtraDimColor).Border(lipgloss.RoundedBorder()), LabelStyle: lipgloss.NewStyle().Foreground(style.ExtraDimColor).AlignHorizontal(lipgloss.Center).AlignVertical(lipgloss.Bottom), } imgViewer = boxLabelRenderer.Render(fmt.Sprintf("%dx%d", imgWidth, imgHeight), imgString, imgWidth) } renderViewport := viewport.New(m.rPanelWidth()-2, m.rPanelHeight()-displayHeight-2) vpRightStyle := lipgloss.NewStyle().Align(lipgloss.Center).AlignVertical(lipgloss.Center) rightContent := vpRightStyle.Copy().Width(m.rPanelWidth() - 2).Height(m.rPanelHeight() - 4).Render(imgViewer) renderViewport.SetContent(rightContent) content := renderViewport.View() return style.NormalButton.Copy().BorderForeground(style.DimmedColor1).Render(content) } func (m Model) renderHelp() string { helpBar := help.New() helpContent := helpBar.View(event.KeyMap) return lipgloss.NewStyle().PaddingLeft(1).Render(helpContent) } ``` -------------------------------------------------------------------------------- /style/color.go: -------------------------------------------------------------------------------- ```go package style import "github.com/charmbracelet/lipgloss" var ( NormalColor1 = lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#aaaaaa"} NormalColor2 = lipgloss.AdaptiveColor{Light: "#3a3a3a", Dark: "#888888"} SelectedColor1 = lipgloss.AdaptiveColor{Light: "#444444", Dark: "#ffffff"} SelectedColor2 = lipgloss.AdaptiveColor{Light: "#666666", Dark: "#dddddd"} ExtraDimColor = lipgloss.AdaptiveColor{Light: "#bbbbbb", Dark: "#444444"} DimmedColor1 = lipgloss.AdaptiveColor{Light: "#999999", Dark: "#777777"} DimmedColor2 = lipgloss.AdaptiveColor{Light: "#aaaaaa", Dark: "#666666"} NormalTitle = lipgloss.NewStyle().Foreground(NormalColor1) NormalParagraph = lipgloss.NewStyle().Foreground(NormalColor2) SelectedTitle = lipgloss.NewStyle().Foreground(SelectedColor1) SelectedParagraph = lipgloss.NewStyle().Foreground(SelectedColor2) DimmedTitle = lipgloss.NewStyle().Foreground(DimmedColor1) ExtraDimTitle = lipgloss.NewStyle().Foreground(ExtraDimColor) DimmedParagraph = lipgloss.NewStyle().Foreground(DimmedColor2) ActiveButton = lipgloss.NewStyle(). BorderStyle(lipgloss.RoundedBorder()). BorderForeground(NormalColor1). Foreground(NormalColor1) FocusButton = lipgloss.NewStyle(). BorderStyle(lipgloss.RoundedBorder()). BorderForeground(SelectedColor1). Foreground(SelectedColor1) NormalButton = lipgloss.NewStyle(). BorderStyle(lipgloss.RoundedBorder()). BorderForeground(DimmedColor1). Foreground(DimmedColor1) ActiveButtonNode = lipgloss.NewStyle(). PaddingLeft(1). Foreground(NormalColor1) FocusButtonNode = lipgloss.NewStyle(). Border(lipgloss.RoundedBorder(), false, false, false, true). BorderForeground(SelectedColor1). Foreground(SelectedColor1). Padding(0) NormalButtonNode = lipgloss.NewStyle(). PaddingLeft(1). Foreground(DimmedColor1) ) ``` -------------------------------------------------------------------------------- /palette/model.go: -------------------------------------------------------------------------------- ```go package palette import ( "image/color" "math" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/lucasb-eyer/go-colorful" "github.com/Zebbeni/ansizalizer/style" ) type Model struct { name string colors color.Palette width int height int } func New(name string, colors color.Palette, w, h int) Model { return Model{ name: name, colors: colors, width: w, height: h, } } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } func (m Model) View() string { title := style.SelectedTitle.Render(m.name) description := m.Description() return lipgloss.JoinVertical(lipgloss.Top, title, description) } func (m Model) FilterValue() string { return m.name } func (m Model) Title() string { return m.name } func (m Model) Description() string { runes := make([]string, len(m.colors)/2+1) rows := make([]string, 0, m.height) for idx := 0; idx < len(m.colors); idx += 2 { var fg, bg colorful.Color var lipFg, lipBg lipgloss.Color fg, _ = colorful.MakeColor(m.colors[idx]) lipFg = lipgloss.Color(fg.Hex()) blockStyle := lipgloss.NewStyle().Foreground(lipFg) if idx+1 < len(m.colors) { bg, _ = colorful.MakeColor(m.colors[idx+1]) lipBg = lipgloss.Color(bg.Hex()) blockStyle = blockStyle.Copy().Background(lipBg) } runes[idx/2] = blockStyle.Render(string('▀')) } for i := 0; i < m.height; i++ { start := m.width * i if start >= len(runes) { break } stop := int(math.Min(float64(m.width*(i+1)), float64(len(runes)))) rows = append(rows, "") rows[i] = lipgloss.JoinHorizontal(lipgloss.Left, runes[start:stop]...) } return lipgloss.JoinVertical(lipgloss.Left, rows...) } func (m Model) Name() string { return m.name } func (m Model) Colors() color.Palette { colorsCopy := make([]color.Color, len(m.colors)) copy(colorsCopy, m.colors) return colorsCopy } ``` -------------------------------------------------------------------------------- /controls/settings/colors/model.go: -------------------------------------------------------------------------------- ```go package colors import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/controls/settings/palettes" "github.com/Zebbeni/ansizalizer/event" "github.com/Zebbeni/ansizalizer/palette" ) type State int const ( UsePalette State = iota UseTrueColor Palette ) type Model struct { focus State mode State width int PaletteControls palettes.Model IsActive bool ShouldClose bool } func New(w int) Model { return Model{ focus: UseTrueColor, mode: UseTrueColor, width: w, PaletteControls: palettes.New(w), IsActive: false, ShouldClose: false, } } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { switch m.focus { case Palette: return m.handlePaletteUpdate(msg) } switch msg := msg.(type) { case tea.KeyMsg: switch { case key.Matches(msg, event.KeyMap.Enter): return m.handleEnter() case key.Matches(msg, event.KeyMap.Nav): return m.handleNav(msg) case key.Matches(msg, event.KeyMap.Esc): return m.handleEsc() } } return m, nil } func (m Model) View() string { paletteToggles := m.drawPaletteToggles() if m.mode == UseTrueColor { return paletteToggles } paletteTabs := m.PaletteControls.View() return lipgloss.JoinVertical(lipgloss.Left, paletteToggles, paletteTabs) } // GetSelected returns isPaletted, isAdaptive, and the palette (if applicable) func (m Model) GetSelected() (bool, bool, palette.Model) { colorPalette := m.PaletteControls.GetCurrentPalette() if m.mode == UseTrueColor { return true, false, colorPalette } return false, m.PaletteControls.IsAdaptive(), colorPalette } func (m Model) GetCurrentPalette() palette.Model { return m.PaletteControls.GetCurrentPalette() } func (m Model) IsLimited() bool { return m.mode == UsePalette } ``` -------------------------------------------------------------------------------- /controls/settings/colors/update.go: -------------------------------------------------------------------------------- ```go package colors import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/Zebbeni/ansizalizer/event" ) type Direction int const ( Left Direction = iota Right Up Down ) var navMap = map[Direction]map[State]State{ Right: { UseTrueColor: UsePalette, }, Left: { UsePalette: UseTrueColor, }, Up: { Palette: UsePalette, }, Down: { UseTrueColor: Palette, UsePalette: Palette, }, } func (m Model) handlePaletteUpdate(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.PaletteControls, cmd = m.PaletteControls.Update(msg) if m.PaletteControls.ShouldClose { m.PaletteControls.IsActive = false m.PaletteControls.ShouldClose = false m.focus = UsePalette } return m, cmd } func (m Model) handleEnter() (Model, tea.Cmd) { switch m.focus { case UsePalette: m.mode = UsePalette case UseTrueColor: m.mode = UseTrueColor } return m, nil } func (m Model) handleEsc() (Model, tea.Cmd) { return m, nil } func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { var cmd tea.Cmd switch { case key.Matches(msg, event.KeyMap.Right): if next, hasNext := navMap[Right][m.focus]; hasNext { return m.setFocus(next) } case key.Matches(msg, event.KeyMap.Left): if next, hasNext := navMap[Left][m.focus]; hasNext { return m.setFocus(next) } case key.Matches(msg, event.KeyMap.Up): if next, hasNext := navMap[Up][m.focus]; hasNext { return m.setFocus(next) } else { m.IsActive = false m.ShouldClose = true } case key.Matches(msg, event.KeyMap.Down): if next, hasNext := navMap[Down][m.focus]; hasNext { return m.setFocus(next) } else { m.IsActive = false m.ShouldClose = true } } return m, cmd } func (m Model) setFocus(focus State) (Model, tea.Cmd) { if m.mode == UseTrueColor && focus == Palette { return m, nil } m.focus = focus switch m.focus { case Palette: m.PaletteControls.IsActive = true } return m, nil } ``` -------------------------------------------------------------------------------- /controls/export/source/model.go: -------------------------------------------------------------------------------- ```go package source import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/controls/browser" "github.com/Zebbeni/ansizalizer/event" ) type State int const ( ExpFile State = iota ExpDirectory Input Browser SubDirsYes SubDirsNo ) type Model struct { focus State doExportDirectory bool includeSubdirectories bool Browser browser.Model selectedDir string selectedFile string ShouldClose bool ShouldUnfocus bool IsActive bool width int } func New(w int) Model { browserModel := browser.New(nil, w-2) return Model{ focus: ExpDirectory, Browser: browserModel, doExportDirectory: true, includeSubdirectories: false, selectedDir: "", selectedFile: "", width: w, ShouldClose: false, } } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd switch m.focus { case Browser: return m.handleSrcBrowserUpdate(msg) } switch msg := msg.(type) { case tea.KeyMsg: switch { case key.Matches(msg, event.KeyMap.Esc): return m.handleEsc() case key.Matches(msg, event.KeyMap.Nav): return m.handleNav(msg) case key.Matches(msg, event.KeyMap.Enter): return m.handleEnter() } } return m, cmd } func (m Model) View() string { content := make([]string, 0, 5) content = append(content, m.drawExportTypeOptions()) selected := lipgloss.NewStyle().PaddingTop(1).Render(m.drawSelected()) content = append(content, selected) if m.focus == Browser { content = append(content, m.Browser.View()) } if m.doExportDirectory { content = append(content, m.drawSubDirOptions()) } return lipgloss.JoinVertical(lipgloss.Left, content...) } func (m Model) GetSelected() (path string, isDir, useSubDirs bool) { if m.doExportDirectory { isDir = true path = m.selectedDir useSubDirs = m.includeSubdirectories } else { path = m.selectedFile isDir = false useSubDirs = false } return } ``` -------------------------------------------------------------------------------- /event/command.go: -------------------------------------------------------------------------------- ```go package event import ( "fmt" "image/color" tea "github.com/charmbracelet/bubbletea" ) type StartRenderToViewMsg bool func StartRenderToViewCmd() tea.Msg { return StartRenderToViewMsg(true) } type FinishRenderToViewMsg struct { FilePath string ImgString string ColorsString string } type StartRenderToExportMsg bool func StartRenderToExportCmd() tea.Msg { return StartRenderToExportMsg(true) } type FinishRenderToExportMsg struct { FilePath string ImgString string ColorsString string } func BuildFinishRenderToExportCmd(msg FinishRenderToExportMsg) tea.Cmd { return func() tea.Msg { return msg } } type StartAdaptingMsg bool func StartAdaptingCmd() tea.Msg { return StartAdaptingMsg(true) } type FinishAdaptingMsg struct { Name string Colors color.Palette } type StartExportMsg struct { SourcePath string DestinationPath string IsDir bool UseSubDirs bool } func BuildStartExportCmd(msg StartExportMsg) tea.Cmd { return func() tea.Msg { return msg } } type FinishExportMsg bool func FinishExportingCmd() tea.Msg { return FinishExportMsg(true) } // DisplayMsg could eventually contain a type // that indicates what style to use (warning, error, etc.) type DisplayMsg string func BuildDisplayCmd(msg string) tea.Cmd { return func() tea.Msg { return DisplayMsg(msg) } } func ClearDisplayCmd() tea.Msg { return DisplayMsg("") } // LospecRequestMsg is a url request used to get a list of type LospecRequestMsg struct { ID int Page int URL string } func BuildLospecRequestCmd(msg LospecRequestMsg) tea.Cmd { display := fmt.Sprintf("loading palettes") return tea.Batch(func() tea.Msg { return msg }, BuildDisplayCmd(display)) } type LospecData struct { Palettes []struct { Colors []string `json:"colors"` Title string `json:"title"` } `json:"palettes"` TotalCount int `json:"totalCount"` } type LospecResponseMsg struct { ID int Page int Data LospecData } func BuildLospecResponseCmd(msg LospecResponseMsg) tea.Cmd { return tea.Batch(func() tea.Msg { return msg }, ClearDisplayCmd) } ``` -------------------------------------------------------------------------------- /controls/settings/characters/model.go: -------------------------------------------------------------------------------- ```go package characters import ( "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/event" ) type State int const ( Ascii State = iota Unicode Custom AsciiAz AsciiNums AsciiSpec AsciiAll UnicodeFull UnicodeHalf UnicodeQuart UnicodeShadeLight UnicodeShadeMed UnicodeShadeHeavy SymbolsForm OneColor TwoColor ) type Model struct { focus State active State mode State charControls State unicodeMode State asciiMode State useFgBg State customInput textinput.Model ShouldClose bool IsActive bool width int } func New(w int) Model { return Model{ focus: Unicode, active: Unicode, mode: Unicode, charControls: Unicode, asciiMode: AsciiAz, unicodeMode: UnicodeHalf, useFgBg: TwoColor, customInput: newInput("Symbols", "/%A"), ShouldClose: false, IsActive: false, width: w, } } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { switch m.active { case SymbolsForm: if m.customInput.Focused() { return m.handleSymbolsFormUpdate(msg) } } switch msg := msg.(type) { case tea.KeyMsg: switch { case key.Matches(msg, event.KeyMap.Enter): return m.handleEnter() case key.Matches(msg, event.KeyMap.Nav): return m.handleNav(msg) case key.Matches(msg, event.KeyMap.Esc): return m.handleEsc() } } return m, nil } func (m Model) View() string { colorsButtons := m.drawColorsButtons() charTabs := m.drawCharTabs() return lipgloss.JoinVertical(lipgloss.Top, colorsButtons, charTabs) } // Selected returns the mode, charMode, whether to use two colors, and the // current set of custom-defined characters func (m Model) Selected() (State, State, State, []rune) { var charMode State switch m.mode { case Unicode: charMode = m.unicodeMode case Ascii: charMode = m.asciiMode case Custom: charMode = Custom } return m.mode, charMode, m.useFgBg, []rune(m.customInput.Value()) } ``` -------------------------------------------------------------------------------- /controls/export/source/update.go: -------------------------------------------------------------------------------- ```go package source import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/Zebbeni/ansizalizer/controls/browser" "github.com/Zebbeni/ansizalizer/event" "github.com/Zebbeni/ansizalizer/global" ) type Direction int const ( Left Direction = iota Right Up Down ) var ( navMap = map[Direction]map[State]State{ Right: {ExpFile: ExpDirectory, SubDirsYes: SubDirsNo}, Left: {ExpDirectory: ExpFile, SubDirsNo: SubDirsYes}, Down: {ExpFile: Input, ExpDirectory: Input, Input: SubDirsYes}, Up: {Input: ExpFile, SubDirsYes: Input, SubDirsNo: Input}, } ) func (m Model) handleEsc() (Model, tea.Cmd) { m.ShouldClose = true m.IsActive = false return m, nil } func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { switch { case key.Matches(msg, event.KeyMap.Right): if next, hasNext := navMap[Right][m.focus]; hasNext { m.focus = next } case key.Matches(msg, event.KeyMap.Left): if next, hasNext := navMap[Left][m.focus]; hasNext { m.focus = next } case key.Matches(msg, event.KeyMap.Down): if next, hasNext := navMap[Down][m.focus]; hasNext { m.focus = next } else { m.ShouldClose = true } case key.Matches(msg, event.KeyMap.Up): if next, hasNext := navMap[Up][m.focus]; hasNext { m.focus = next } else { m.ShouldClose = true } } return m, nil } func (m Model) handleEnter() (Model, tea.Cmd) { switch m.focus { case ExpFile: m.focus = Browser m.doExportDirectory = false m.Browser = browser.New(global.ImgExtensions, m.width) case ExpDirectory: m.focus = Browser m.doExportDirectory = true m.Browser = browser.New(nil, m.width) case Input: m.focus = Browser case SubDirsYes: m.includeSubdirectories = true case SubDirsNo: m.includeSubdirectories = false } return m, nil } func (m Model) handleSrcBrowserUpdate(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.Browser, cmd = m.Browser.Update(msg) if m.doExportDirectory { m.selectedDir = m.Browser.SelectedDir } else { m.selectedFile = m.Browser.SelectedFile } if m.Browser.ShouldClose { m.focus = Input m.Browser.ShouldClose = false } return m, cmd } func (m Model) handleIncludeSubdirectories(shouldInclude bool) (Model, tea.Cmd) { m.includeSubdirectories = shouldInclude return m, nil } ``` -------------------------------------------------------------------------------- /controls/settings/palettes/adaptive/model.go: -------------------------------------------------------------------------------- ```go package adaptive import ( "image/color" "strconv" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/event" "github.com/Zebbeni/ansizalizer/palette" ) type State int const ( CountForm State = iota IterForm Generate Save ) type Model struct { focus State active State palette palette.Model countInput textinput.Model iterInput textinput.Model width, height int ShouldClose bool ShouldUnfocus bool IsActive bool IsSelected bool // true if we've selected something (ie. render w/ adaptive) } func New(w int) Model { return Model{ focus: CountForm, countInput: newInput(CountForm), iterInput: newInput(IterForm), ShouldUnfocus: false, IsActive: false, IsSelected: false, width: w, } } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { switch m.active { case CountForm: if m.countInput.Focused() { return m.handleCountUpdate(msg) } case IterForm: if m.iterInput.Focused() { return m.handleIterUpdate(msg) } } switch msg := msg.(type) { case tea.KeyMsg: switch { case key.Matches(msg, event.KeyMap.Enter): return m.handleEnter() case key.Matches(msg, event.KeyMap.Nav): return m.handleNav(msg) case key.Matches(msg, event.KeyMap.Esc): return m.handleEsc() } } return m, nil } func (m Model) View() string { title := m.drawTitle() inputs := m.drawInputs() generate := m.drawGenerateButton() if len(m.palette.Colors()) == 0 { return lipgloss.JoinVertical(lipgloss.Top, title, inputs, generate) } palette := lipgloss.NewStyle().Padding(0, 1, 0, 1).Render(m.palette.View()) saveButton := m.drawSaveButton() content := lipgloss.JoinVertical(lipgloss.Top, title, inputs, generate, palette, saveButton) return content } func (m Model) Info() (int, int) { var count, iterations int count, _ = strconv.Atoi(m.countInput.Value()) iterations, _ = strconv.Atoi(m.iterInput.Value()) return count, iterations } func (m Model) GetCurrent() palette.Model { return m.palette } func (m Model) SetPalette(colors color.Palette, name string) Model { m.palette = palette.New(name, colors, m.width-4, 3) return m } ``` -------------------------------------------------------------------------------- /controls/settings/advanced/update.go: -------------------------------------------------------------------------------- ```go package advanced import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/Zebbeni/ansizalizer/event" ) type Direction int const ( Left Direction = iota Right Up Down ) var navMap = map[Direction]map[State]State{ Right: { Sampling: Dithering, }, Left: { Dithering: Sampling, }, Down: { Sampling: SamplingControls, Dithering: DitheringControls, }, Up: { SamplingControls: Sampling, DitheringControls: Dithering, }, } func (m Model) handleSamplingUpdate(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.sampling, cmd = m.sampling.Update(msg) if m.sampling.ShouldClose { m.active = Menu m.focus = Sampling m.sampling.ShouldClose = false m.sampling.IsActive = false } return m, cmd } func (m Model) handleDitheringUpdate(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.dithering, cmd = m.dithering.Update(msg) if m.dithering.ShouldClose { m.active = Menu m.focus = Dithering m.dithering.ShouldClose = false m.dithering.IsActive = false } return m, cmd } func (m Model) handleEsc() (Model, tea.Cmd) { m.ShouldClose = true return m, nil } func (m Model) handleEnter() (Model, tea.Cmd) { m.active = m.focus return m, nil } func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { var cmd tea.Cmd switch { case key.Matches(msg, event.KeyMap.Right): if next, hasNext := navMap[Right][m.focus]; hasNext { return m.setFocus(next) } case key.Matches(msg, event.KeyMap.Left): if next, hasNext := navMap[Left][m.focus]; hasNext { return m.setFocus(next) } case key.Matches(msg, event.KeyMap.Up): if next, hasNext := navMap[Up][m.focus]; hasNext { return m.setFocus(next) } else { m.IsActive = false m.ShouldClose = true } case key.Matches(msg, event.KeyMap.Down): if next, hasNext := navMap[Down][m.focus]; hasNext { return m.setFocus(next) } else { m.IsActive = false m.ShouldClose = true } } return m, cmd } func (m Model) setFocus(focus State) (Model, tea.Cmd) { m.focus = focus switch m.focus { case Sampling: m.activeTab = Sampling case Dithering: m.activeTab = Dithering case SamplingControls: m.active = SamplingControls m.sampling.IsActive = true case DitheringControls: m.active = DitheringControls m.dithering.IsActive = true } return m, nil } ``` -------------------------------------------------------------------------------- /controls/settings/size/model.go: -------------------------------------------------------------------------------- ```go package size import ( "strconv" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/event" ) const DEFAULT_CHAR_W_TO_H_RATIO = 0.5 type State int type Mode int const ( Fit Mode = iota Stretch ) const ( FitButton State = iota StretchButton WidthForm HeightForm CharRatioForm None ) type Model struct { focus State active State mode Mode widthInput textinput.Model heightInput textinput.Model charRatioInput textinput.Model ShouldUnfocus bool ShouldClose bool IsActive bool } func New() Model { return Model{ focus: FitButton, active: None, mode: Fit, widthInput: newInput(WidthForm, 50), heightInput: newInput(HeightForm, 40), charRatioInput: newFloatInput(CharRatioForm, DEFAULT_CHAR_W_TO_H_RATIO), ShouldUnfocus: false, ShouldClose: false, IsActive: false, } } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { var cmd1, cmd2 tea.Cmd newM := m switch m.active { case WidthForm: if m.widthInput.Focused() { newM, cmd1 = newM.handleWidthUpdate(msg) } case HeightForm: if m.heightInput.Focused() { newM, cmd1 = newM.handleHeightUpdate(msg) } case CharRatioForm: if m.charRatioInput.Focused() { newM, cmd1 = newM.handleCharRatioUpdate(msg) } } switch msg := msg.(type) { case tea.KeyMsg: switch { case key.Matches(msg, event.KeyMap.Enter): newM, cmd2 = newM.handleEnter() case key.Matches(msg, event.KeyMap.Nav): newM, cmd2 = newM.handleNav(msg) case key.Matches(msg, event.KeyMap.Esc): newM, cmd2 = newM.handleEsc() } } return newM, tea.Batch(cmd1, cmd2) } func (m Model) View() string { buttonRow := m.drawButtons() forms := m.drawSizeForms() ratioForm := m.drawCharRatioForm() return lipgloss.JoinVertical(lipgloss.Left, buttonRow, forms, ratioForm) } func (m Model) Info() (Mode, int, int, float64) { var width, height int width, _ = strconv.Atoi(m.widthInput.Value()) height, _ = strconv.Atoi(m.heightInput.Value()) charRatio, err := strconv.ParseFloat(m.charRatioInput.Value(), 64) if err != nil { charRatio = DEFAULT_CHAR_W_TO_H_RATIO } return m.mode, width, height, charRatio } ``` -------------------------------------------------------------------------------- /controls/browser/update.go: -------------------------------------------------------------------------------- ```go package browser import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/Zebbeni/ansizalizer/controls/menu" "github.com/Zebbeni/ansizalizer/event" ) func (m Model) handleEnter() (Model, tea.Cmd) { return m.updateSelected() } func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { if m.currentList().Index() == 0 && key.Matches(msg, event.KeyMap.Up) { m.ShouldClose = true return m, nil } cmds := make([]tea.Cmd, 2) m.lists[m.listIndex()], cmds[0] = m.currentList().Update(msg) m, cmds[1] = m.updateActive() return m, tea.Batch(cmds...) } func (m Model) handleEsc() (Model, tea.Cmd) { // remove last list if possible (go back to previous) if len(m.lists) > 1 { m.lists = m.lists[:m.listIndex()] return m, nil } m.ShouldClose = true return m, nil } func (m Model) updateActive() (Model, tea.Cmd) { itm, ok := m.currentList().SelectedItem().(item) if !ok { panic("Unexpected list item type") } if itm.isDir && m.ActiveDir != itm.path { m.ActiveDir = itm.path return m, nil } if itm.isDir == false && m.ActiveFile != itm.path { m.ActiveFile = itm.path return m, event.StartRenderToViewCmd } return m, nil } func (m Model) updateSelected() (Model, tea.Cmd) { itm, ok := m.currentList().SelectedItem().(item) if !ok { panic("Unexpected list item type") } if itm.isDir { m.SelectedDir = itm.path m = m.addListForDirectory(itm.path) } else { m.SelectedFile = itm.path m.ShouldClose = true } return m, nil } func (m Model) addListForDirectory(dir string) Model { newList := menu.New(getItems(m.fileExtensions, dir), m.width) newList.SetShowTitle(false) //title := filepath.Join(filepath.Base(filepath.Dir(dir)), filepath.Base(dir)) //newList.Title = fitString(title, m.width-10) //newList.Styles.Title = newList.Styles.Title.Copy().Foreground(style.DimmedColor2).UnsetBackground() //newList.Styles.TitleBar = newList.Styles.TitleBar.Copy().Padding(0).Height(2) newList.SetShowStatusBar(false) newList.SetFilteringEnabled(false) newList.SetShowFilter(false) newList.SetWidth(m.width) m.lists = append(m.lists, newList) m.SelectedDir = dir return m } func fitString(value string, width int) string { valueRunes := []rune(value) start := len(valueRunes) - width - 2 if start < 0 { start = 0 } if len(valueRunes) > width { value = "\n.." + string(valueRunes[start:]) } return value } ``` -------------------------------------------------------------------------------- /controls/export/update.go: -------------------------------------------------------------------------------- ```go package export import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/Zebbeni/ansizalizer/event" ) type Direction int const ( Down Direction = iota Up ) var navMap = map[Direction]map[State]State{ Down: {Source: Destination, Destination: Process}, Up: {Destination: Source, Process: Destination}, } func (m Model) handleSourceUpdate(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.Source, cmd = m.Source.Update(msg) if m.Source.ShouldClose { m.active = None m.Source.ShouldClose = false } if m.Source.ShouldUnfocus { return m.handleMenuUpdate(msg) } return m, cmd } func (m Model) handleDestinationUpdate(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.Destination, cmd = m.Destination.Update(msg) if m.Destination.ShouldClose { m.active = None m.Destination.ShouldClose = false } return m, cmd } func (m Model) handleEnter() (Model, tea.Cmd) { m.active = m.focus switch m.active { case Source: m.Source.IsActive = true case Destination: m.Destination.IsActive = true case Process: return m.handleProcess() } return m, nil } func (m Model) handleEsc() (Model, tea.Cmd) { m.ShouldClose = true return m, nil } func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { switch { case key.Matches(msg, event.KeyMap.Down): if next, hasNext := navMap[Down][m.focus]; hasNext { m.focus = next } else { m.ShouldClose = true } case key.Matches(msg, event.KeyMap.Up): if next, hasNext := navMap[Up][m.focus]; hasNext { m.focus = next } else { m.ShouldClose = true } } return m, nil } func (m Model) handleMenuUpdate(msg tea.Msg) (Model, tea.Cmd) { if keyMsg, ok := msg.(tea.KeyMsg); ok { return m.handleKeyMsg(keyMsg) } return m, nil } func (m Model) handleKeyMsg(msg tea.KeyMsg) (Model, tea.Cmd) { var cmd tea.Cmd switch { case key.Matches(msg, event.KeyMap.Enter): return m.handleEnter() case key.Matches(msg, event.KeyMap.Nav): return m.handleNav(msg) case key.Matches(msg, event.KeyMap.Esc): return m.handleEsc() } return m, cmd } func (m Model) handleProcess() (Model, tea.Cmd) { sourcePath, isDir, useSubDirs := m.Source.GetSelected() destinationPath := m.Destination.GetSelected() return m, event.BuildStartExportCmd(event.StartExportMsg{ SourcePath: sourcePath, DestinationPath: destinationPath, IsDir: isDir, UseSubDirs: useSubDirs, }) } func (m Model) GetDestination() (path string) { return m.Destination.GetSelected() } ``` -------------------------------------------------------------------------------- /controls/settings/palettes/loader/model.go: -------------------------------------------------------------------------------- ```go package loader import ( "bufio" "fmt" "image/color" "os" "path/filepath" "strings" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/lucasb-eyer/go-colorful" "github.com/Zebbeni/ansizalizer/controls/browser" "github.com/Zebbeni/ansizalizer/event" "github.com/Zebbeni/ansizalizer/palette" "github.com/Zebbeni/ansizalizer/style" ) var ( paletteExtensions = map[string]bool{".hex": true} ) type Model struct { FileBrowser browser.Model paletteFilepath string palette palette.Model IsSelected bool // true if we've selected something (ie. render w/ loader) ShouldUnfocus bool width int } func New(w int) Model { fileBrowser := browser.New(paletteExtensions, w-2) return Model{ FileBrowser: fileBrowser, IsSelected: false, ShouldUnfocus: false, width: w, } } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.FileBrowser, cmd = m.FileBrowser.Update(msg) if m.FileBrowser.ActiveFile != m.paletteFilepath { m.paletteFilepath = m.FileBrowser.ActiveFile name := strings.Split(filepath.Base(m.paletteFilepath), ".hex")[0] colors, err := parsePaletteFile(m.paletteFilepath) if err != nil { return m, tea.Batch(cmd, event.BuildDisplayCmd("error parsing paletteFilepath file")) } m.palette = palette.New(name, colors, m.width-5, 3) m.IsSelected = true return m, tea.Batch(cmd, event.StartRenderToViewCmd) } if m.FileBrowser.ShouldClose { m.IsSelected = false m.FileBrowser.ShouldClose = false m.ShouldUnfocus = true } return m, cmd } func (m Model) View() string { activePreview := style.DimmedTitle.Render("No palette selected") if len(m.palette.Colors()) != 0 { activePreview = m.palette.View() } activePreview = lipgloss.NewStyle().Padding(0, 0, 1, 2).Render(activePreview) title := m.drawTitle() browser := m.FileBrowser.View() return lipgloss.JoinVertical(lipgloss.Top, title, browser, activePreview) } func (m Model) GetCurrent() palette.Model { return m.palette } func parsePaletteFile(filepath string) (color.Palette, error) { readFile, err := os.Open(filepath) if err != nil { return nil, err } fileScanner := bufio.NewScanner(readFile) fileScanner.Split(bufio.ScanLines) var col colorful.Color p := make(color.Palette, 0, 256) for fileScanner.Scan() { col, err = colorful.Hex(fmt.Sprintf("#%s", fileScanner.Text())) if err != nil { return nil, err } p = append(p, col) } return p, nil } ``` -------------------------------------------------------------------------------- /controls/settings/palettes/model.go: -------------------------------------------------------------------------------- ```go package palettes import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/controls/settings/palettes/adaptive" "github.com/Zebbeni/ansizalizer/controls/settings/palettes/loader" "github.com/Zebbeni/ansizalizer/controls/settings/palettes/lospec" "github.com/Zebbeni/ansizalizer/palette" ) type State int // None consists of a few different components that are shown or hidden // depending on which toggles have been set on / off. The Model state indicates // which component is currently focused. From top to bottom the components are: // 1) Limited (on/off) // 2) Loader (Name) (if Limited) -> [Enter] displays Loader menu // 3) Dithering (on/off) (if Limited) // 4) Serpentine (on/off) (if Dithering) // 5) Matrix (Name) (if Dithering) -> [Enter] displays to Matrix menu // These can all be part of a single list, but we need to onSelect the list items const ( Adapt State = iota Load Lospec AdaptiveControls LoadControls LospecControls ) type Model struct { selected State focus State // the component taking input controls State Adapter adaptive.Model Loader loader.Model Lospec lospec.Model ShouldClose bool IsActive bool width int } func New(w int) Model { m := Model{ selected: Load, focus: Load, controls: Load, Adapter: adaptive.New(w), Loader: loader.New(w), Lospec: lospec.New(w), ShouldClose: false, IsActive: false, width: w, } return m } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { switch m.focus { case AdaptiveControls: return m.handleAdaptiveUpdate(msg) case LoadControls: return m.handleLoaderUpdate(msg) case LospecControls: return m.handleLospecUpdate(msg) } return m.handleMenuUpdate(msg) } func (m Model) View() string { buttons := m.drawButtons() if m.IsActive == false { return buttons } var controls string switch m.controls { case Adapt: controls = m.Adapter.View() case Load: controls = m.Loader.View() case Lospec: controls = m.Lospec.View() } if len(controls) == 0 { return buttons } return lipgloss.JoinVertical(lipgloss.Top, buttons, controls) } func (m Model) IsAdaptive() bool { return m.selected == Adapt } func (m Model) IsPaletted() bool { return m.selected == Load } func (m Model) GetCurrentPalette() palette.Model { switch m.selected { case Load: return m.Loader.GetCurrent() case Adapt: return m.Adapter.GetCurrent() case Lospec: return m.Lospec.GetCurrent() } return palette.Model{} } ``` -------------------------------------------------------------------------------- /controls/settings/update.go: -------------------------------------------------------------------------------- ```go package settings import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/Zebbeni/ansizalizer/event" ) type Direction int const ( Down Direction = iota Up ) var navMap = map[Direction]map[State]State{ Down: {Colors: Characters, Characters: Size, Size: Advanced}, Up: {Advanced: Size, Size: Characters, Characters: Colors}, } func (m Model) handleSettingsUpdate(msg tea.Msg) (Model, tea.Cmd) { if keyMsg, ok := msg.(tea.KeyMsg); ok { return m.handleKeyMsg(keyMsg) } return m, nil } func (m Model) handleColorsUpdate(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.Colors, cmd = m.Colors.Update(msg) if m.Colors.ShouldClose { m.active = None m.Colors.IsActive = false m.Colors.ShouldClose = false } return m, cmd } func (m Model) handleCharactersUpdate(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.Characters, cmd = m.Characters.Update(msg) if m.Characters.ShouldClose { m.active = None m.Characters.IsActive = false m.Characters.ShouldClose = false } return m, cmd } func (m Model) handleSizeUpdate(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.Size, cmd = m.Size.Update(msg) if m.Size.ShouldClose { m.active = None m.Size.IsActive = false m.Size.ShouldClose = false } if m.Size.ShouldUnfocus { return m.handleSettingsUpdate(msg) } return m, cmd } func (m Model) handleAdvancedUpdate(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.Advanced, cmd = m.Advanced.Update(msg) if m.Advanced.ShouldClose { m.active = None m.Advanced.ShouldClose = false } return m, cmd } func (m Model) handleEnter() (Model, tea.Cmd) { m.active = m.focus switch m.active { case Colors: m.Colors.IsActive = true case Characters: m.Characters.IsActive = true case Size: m.Size.IsActive = true case Advanced: m.Advanced.IsActive = true } return m, nil } func (m Model) handleEsc() (Model, tea.Cmd) { m.ShouldClose = true return m, nil } func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { switch { case key.Matches(msg, event.KeyMap.Down): if next, hasNext := navMap[Down][m.focus]; hasNext { m.focus = next } case key.Matches(msg, event.KeyMap.Up): if next, hasNext := navMap[Up][m.focus]; hasNext { m.focus = next } else { m.ShouldClose = true } } return m, nil } func (m Model) handleKeyMsg(msg tea.KeyMsg) (Model, tea.Cmd) { var cmd tea.Cmd switch { case key.Matches(msg, event.KeyMap.Enter): return m.handleEnter() case key.Matches(msg, event.KeyMap.Nav): return m.handleNav(msg) case key.Matches(msg, event.KeyMap.Esc): return m.handleEsc() } return m, cmd } ``` -------------------------------------------------------------------------------- /style/box.go: -------------------------------------------------------------------------------- ```go package style import ( "strings" "github.com/charmbracelet/lipgloss" ) type BoxWithLabel struct { BoxStyle lipgloss.Style LabelStyle lipgloss.Style } func NewDefaultBoxWithLabel() BoxWithLabel { return BoxWithLabel{ BoxStyle: lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("63")), // You could, of course, also set background and foreground colors here // as well. LabelStyle: lipgloss.NewStyle(). AlignHorizontal(lipgloss.Center). PaddingTop(0). PaddingBottom(0), } } func (b BoxWithLabel) Render(label, content string, width int) string { var ( // Query the box style for some of its border properties so we can // essentially take the top border apart and put it around the label. border lipgloss.Border = b.BoxStyle.GetBorderStyle() topBorderStyler func(string) string = lipgloss.NewStyle().Foreground(b.BoxStyle.GetBorderTopForeground()).Render bottomBorderStyler func(string) string = lipgloss.NewStyle().Foreground(b.BoxStyle.GetBorderBottomForeground()).Render topLeft string = topBorderStyler(border.TopLeft) topRight string = topBorderStyler(border.TopRight) botLeft string = bottomBorderStyler(border.BottomLeft) botRight string = bottomBorderStyler(border.BottomRight) renderedLabel string = b.LabelStyle.Render(label) ) // Render top row with the label borderWidth := b.BoxStyle.GetHorizontalBorderSize() cellsShort := max(0, width+borderWidth-lipgloss.Width(topLeft+topRight+renderedLabel)) gap := strings.Repeat(border.Top, cellsShort) var gapLeft, gapRight string switch b.LabelStyle.GetAlignHorizontal() { case lipgloss.Left: gapRight = gap case lipgloss.Right: gapLeft = gap case lipgloss.Center: gapLeft = strings.Repeat(border.Top, cellsShort/2) gapRight = strings.Repeat(border.Top, cellsShort-(cellsShort/2)) } var top, bottom string switch b.LabelStyle.GetAlignVertical() { case lipgloss.Top: strings.Repeat(border.Top, cellsShort) top = topLeft + topBorderStyler(gapLeft) + renderedLabel + topBorderStyler(gapRight) + topRight bottom = b.BoxStyle.Copy(). BorderTop(false). Width(width). Render(content) case lipgloss.Bottom: strings.Repeat(border.Bottom, cellsShort) bottom = botLeft + bottomBorderStyler(gapLeft) + renderedLabel + bottomBorderStyler(gapRight) + botRight top = b.BoxStyle.Copy(). BorderBottom(false). Width(width). Render(content) } // Stack the pieces return top + "\n" + bottom } func max(a, b int) int { if a > b { return a } return b } ``` -------------------------------------------------------------------------------- /controls/settings/advanced/dithering/update.go: -------------------------------------------------------------------------------- ```go package dithering import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/Zebbeni/ansizalizer/event" ) type Direction int const ( Left Direction = iota Right Up Down ) var navMap = map[Direction]map[State]State{ Right: { DitherOn: DitherOff, SerpentineOn: SerpentineOff, }, Left: { DitherOff: DitherOn, SerpentineOff: SerpentineOn, }, Down: { DitherOn: SerpentineOn, DitherOff: SerpentineOff, SerpentineOn: Matrix, SerpentineOff: Matrix, }, Up: { SerpentineOn: DitherOn, SerpentineOff: DitherOff, Matrix: SerpentineOn, }, } func (m Model) handleMatrixListUpdate(msg tea.Msg) (Model, tea.Cmd) { if keyMsg, ok := msg.(tea.KeyMsg); ok { switch { case key.Matches(keyMsg, event.KeyMap.Up) && m.list.Index() == 0: return m.handleNav(keyMsg) case key.Matches(keyMsg, event.KeyMap.Esc): case key.Matches(keyMsg, event.KeyMap.Enter): var cmd tea.Cmd m, cmd = m.setFocus(navMap[Up][Matrix]) return m, tea.Batch(cmd, event.StartRenderToViewCmd) } } var cmd tea.Cmd m.list, cmd = m.list.Update(msg) return m, cmd } func (m Model) handleEsc() (Model, tea.Cmd) { m.ShouldClose = true return m, nil } func (m Model) handleEnter() (Model, tea.Cmd) { switch m.focus { case DitherOn: m.doDithering = true case DitherOff: m.doDithering = false case SerpentineOn: m.doSerpentine = true case SerpentineOff: m.doSerpentine = false } return m, event.StartRenderToViewCmd } func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { var cmd tea.Cmd switch { case key.Matches(msg, event.KeyMap.Right): if next, hasNext := navMap[Right][m.focus]; hasNext { return m.setFocus(next) } case key.Matches(msg, event.KeyMap.Left): if next, hasNext := navMap[Left][m.focus]; hasNext { return m.setFocus(next) } case key.Matches(msg, event.KeyMap.Up): if next, hasNext := navMap[Up][m.focus]; hasNext { return m.setFocus(next) } else { m.ShouldClose = true } case key.Matches(msg, event.KeyMap.Down): if next, hasNext := navMap[Down][m.focus]; hasNext { return m.setFocus(next) } else { m.ShouldClose = true } } return m, cmd } func (m Model) handleKeyMsg(msg tea.KeyMsg) (Model, tea.Cmd) { var cmd tea.Cmd switch { case key.Matches(msg, event.KeyMap.Enter): return m.handleEnter() case key.Matches(msg, event.KeyMap.Nav): return m.handleNav(msg) case key.Matches(msg, event.KeyMap.Esc): return m.handleEsc() } return m, cmd } func (m Model) setFocus(focus State) (Model, tea.Cmd) { m.focus = focus if focus != Matrix { m.list.SetDelegate(NewDelegate(false)) } else { m.list.SetDelegate(NewDelegate(true)) } return m, nil } ``` -------------------------------------------------------------------------------- /app/export.go: -------------------------------------------------------------------------------- ```go package app import ( "fmt" "os" "path/filepath" "strings" "github.com/Zebbeni/ansizalizer/global" ) const ( maxExportJobs = 1000 ) type exportJob struct { sourcePath string destinationPath string } type MaxExportQueueError struct { count int } func (r *MaxExportQueueError) Error() string { return fmt.Sprintf("%d+ export jobs exceed %d max", r.count, maxExportJobs) } // this process may get more complicated if we want to do animated gifs, // since each gif will require multiple image exports. func buildExportQueue(dirPath, destPath string, useSubDirs bool) ([]exportJob, error) { // for each image file found in the dirPath, append an exportJob object // with the source filepath and its corresponding .ansi destination filepath entries, err := os.ReadDir(dirPath) if err != nil { return nil, err } exportJobs := make([]exportJob, 0, len(entries)) subDirs := make([]string, 0, len(entries)) for _, e := range entries { sourcePath := filepath.Join(dirPath, e.Name()) if e.IsDir() { subDirs = append(subDirs, sourcePath) continue } ext := filepath.Ext(e.Name()) if _, ok := global.ImgExtensions[ext]; ok { nameWithoutExt := strings.Split(filepath.Base(sourcePath), ".")[0] nameWithExt := fmt.Sprintf("%s.ansi", nameWithoutExt) destFilePath := filepath.Join(destPath, nameWithExt) exportJobs = append(exportJobs, exportJob{ sourcePath: sourcePath, destinationPath: destFilePath, }) } } if useSubDirs { // call buildExportQueue on each subdirectory in dirPath, creating // subdirectories in the destination path to mimic the source directory // structure, and providing these subdirectory paths to the build call as well for _, subDir := range subDirs { subDirName := filepath.Base(subDir) subDestPath := filepath.Join(destPath, subDirName) var subDirExportJobs []exportJob subDirExportJobs, err = buildExportQueue(subDir, subDestPath, true) if err != nil { return nil, err } // append resulting exportJob lists to the main list exportJobs = append(exportJobs, subDirExportJobs...) if len(exportJobs) > maxExportJobs { return nil, &MaxExportQueueError{count: len(exportJobs)} } // skip creating mirrored subdirectories if no files found there if len(subDirExportJobs) == 0 { continue } // create the destination folder if it doesn't already exist // do this after the recursive call to buildExportQueue. Otherwise, // we can hit an infinite loop where our newly created directories // get picked up by subsequent buildExportQueue calls, forever. if _, err = os.Stat(subDestPath); os.IsNotExist(err) { err = os.MkdirAll(subDestPath, os.ModeDir) if err != nil { return nil, err } } } } return exportJobs, nil } ``` -------------------------------------------------------------------------------- /app/process/custom.go: -------------------------------------------------------------------------------- ```go package process import ( "image" "math" "github.com/charmbracelet/lipgloss" "github.com/lucasb-eyer/go-colorful" "github.com/makeworld-the-better-one/dither/v2" "github.com/nfnt/resize" "github.com/Zebbeni/ansizalizer/controls/settings/characters" "github.com/Zebbeni/ansizalizer/controls/settings/size" ) func (m Renderer) processCustom(input image.Image) string { imgW, imgH := float32(input.Bounds().Dx()), float32(input.Bounds().Dy()) dimensionType, width, height, charRatio := m.Settings.Size.Info() if dimensionType == size.Fit { fitHeight := float32(width) * (imgH / imgW) * float32(charRatio) fitWidth := (float32(height) * (imgW / imgH)) / float32(charRatio) if fitHeight > float32(height) { width = int(fitWidth) } else { height = int(fitHeight) } } resizeFunc := m.Settings.Advanced.SamplingFunction() refImg := resize.Resize(uint(width)*2, uint(height)*2, input, resizeFunc) isTrueColor, _, palette := m.Settings.Colors.GetSelected() isPaletted := !isTrueColor doDither, doSerpentine, matrix := m.Settings.Advanced.Dithering() if doDither && isPaletted { ditherer := dither.NewDitherer(palette.Colors()) ditherer.Matrix = matrix if doSerpentine { ditherer.Serpentine = true } refImg = ditherer.Dither(refImg) } _, _, useFgBg, chars := m.Settings.Characters.Selected() if len(chars) == 0 { return "Enter at least one custom character" } content := "" rows := make([]string, height) row := make([]string, width) for y := 0; y < height*2; y += 2 { for x := 0; x < width*2; x += 2 { r1, _ := colorful.MakeColor(refImg.At(x, y)) r2, _ := colorful.MakeColor(refImg.At(x+1, y)) r3, _ := colorful.MakeColor(refImg.At(x, y+1)) r4, _ := colorful.MakeColor(refImg.At(x+1, y+1)) if useFgBg == characters.TwoColor { fg, bg, brightness := m.fgBgBrightness(r1, r2, r3, r4) lipFg := lipgloss.Color(fg.Hex()) lipBg := lipgloss.Color(bg.Hex()) style := lipgloss.NewStyle().Foreground(lipFg).Background(lipBg).Bold(true) index := min(int(brightness*float64(len(chars))), len(chars)-1) char := chars[index] charString := string(char) row[x/2] = style.Render(charString) } else { fg := m.avgColTrue(r1, r2, r3, r4) brightness := math.Min(1.0, math.Abs(fg.DistanceLuv(black))) if isPaletted { fg, _ = colorful.MakeColor(palette.Colors().Convert(fg)) } lipFg := lipgloss.Color(fg.Hex()) style := lipgloss.NewStyle().Foreground(lipFg).Bold(true) index := min(int(brightness*float64(len(chars))), len(chars)-1) char := chars[index] charString := string(char) row[x/2] = style.Render(charString) } } rows[y/2] = lipgloss.JoinHorizontal(lipgloss.Top, row...) } content += lipgloss.JoinVertical(lipgloss.Left, rows...) return content } func min(a, b int) int { if a < b { return a } return b } ``` -------------------------------------------------------------------------------- /controls/settings/characters/tabs.go: -------------------------------------------------------------------------------- ```go package characters import ( "strings" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/style" ) var ( inactiveTabBorder = tabBorderWithBottom("┴", "─", "┴") activeTabBorder = tabBorderWithBottom("┘", " ", "└") docStyle = lipgloss.NewStyle().Padding(0) inactiveTabStyle = lipgloss.NewStyle().Border(inactiveTabBorder, true) activeTabStyle = lipgloss.NewStyle().Border(activeTabBorder, true) focusTabStyle = activeTabStyle.Copy().BorderForeground(style.SelectedColor1) windowStyle = lipgloss.NewStyle().Align(lipgloss.Center).Border(lipgloss.NormalBorder()).UnsetBorderTop().Padding(1, 0) ) func (m Model) drawCharTabs() string { doc := strings.Builder{} var renderedTabs []string tabs := []State{Ascii, Unicode, Custom} borderColor := style.DimmedColor2 if m.IsActive { borderColor = style.NormalColor1 } for i, t := range tabs { var tabStyle lipgloss.Style isFirst := i == 0 isLast := i == len(tabs)-1 isActive := m.focus == t showControls := m.charControls == t fgColor := style.DimmedColor2 if m.IsActive { if isActive { fgColor = style.SelectedColor1 } else { fgColor = style.DimmedColor1 } } else { if isActive { fgColor = style.NormalColor2 } } if showControls { tabStyle = activeTabStyle.Copy() } else { tabStyle = inactiveTabStyle.Copy() } border, _, _, _, _ := tabStyle.GetBorder() if isFirst && showControls { border.BottomLeft = "│" } else if isFirst && !showControls { border.BottomLeft = "├" } else if isLast && showControls { border.BottomRight = "└" } else if isLast && !showControls { border.BottomRight = "┴" } tabStyle = tabStyle.Border(border).BorderForeground(borderColor).Foreground(fgColor) renderedTabs = append(renderedTabs, tabStyle.Render(stateNames[t])) } tabBlock := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...) extW, extH := max(m.width-lipgloss.Width(tabBlock)-2, 0), 1 border := lipgloss.Border{BottomLeft: "─", Bottom: "─", BottomRight: "┐"} extendedStyle := windowStyle.Copy().Border(border).BorderForeground(borderColor).Padding(0) extended := extendedStyle.Copy().Width(extW).Height(extH).Render("") renderedTabs = append(renderedTabs, extended) row := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...) doc.WriteString(row) doc.WriteString("\n") charButtons := m.drawCharControls() doc.WriteString(windowStyle.Copy().BorderForeground(borderColor).Width(lipgloss.Width(row) - windowStyle.GetHorizontalFrameSize()).Render(charButtons)) return docStyle.Render(doc.String()) } func max(a, b int) int { if a > b { return a } return b } func min(a, b int) int { if a < b { return a } return b } func tabBorderWithBottom(left, middle, right string) lipgloss.Border { border := lipgloss.RoundedBorder() border.BottomLeft = left border.Bottom = middle border.BottomRight = right return border } ``` -------------------------------------------------------------------------------- /controls/settings/size/view.go: -------------------------------------------------------------------------------- ```go package size import ( "github.com/charmbracelet/bubbles/cursor" "github.com/charmbracelet/lipgloss" ) var ( stateOrder = []State{FitButton, StretchButton} stateNames = map[State]string{ FitButton: "Fit", StretchButton: "Stretch", WidthForm: "Width", HeightForm: "Height", CharRatioForm: "Char Size Ratio (Width/Height)", } inputStyle = lipgloss.NewStyle().Width(14).AlignHorizontal(lipgloss.Left) activeColor = lipgloss.Color("#aaaaaa") focusColor = lipgloss.Color("#ffffff") normalColor = lipgloss.Color("#555555") titleStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#888888")) ) func (m Model) drawButtons() string { buttons := make([]string, len(stateOrder)) for i, state := range stateOrder { styleColor := normalColor if m.IsActive { if state == m.focus { styleColor = focusColor } else if state == m.active { styleColor = activeColor } } style := lipgloss.NewStyle(). BorderStyle(lipgloss.RoundedBorder()). BorderForeground(styleColor). Foreground(styleColor) buttons[i] = style.Copy().Width(12).AlignHorizontal(lipgloss.Center).Render(stateNames[state]) } return lipgloss.JoinHorizontal(lipgloss.Left, buttons...) } func (m Model) drawSizeForms() string { prompt, text := m.getInputColors(WidthForm) m.widthInput.Width = 3 m.widthInput.PromptStyle = m.widthInput.PromptStyle.Copy().Foreground(prompt) m.heightInput.TextStyle = m.heightInput.TextStyle.Copy().Foreground(text) if m.widthInput.Focused() { m.widthInput.Cursor.SetMode(cursor.CursorBlink) } else { m.widthInput.Cursor.SetMode(cursor.CursorHide) } prompt, text = m.getInputColors(HeightForm) m.heightInput.PromptStyle = m.heightInput.PromptStyle.Copy().Foreground(prompt) m.heightInput.TextStyle = m.heightInput.TextStyle.Copy().Foreground(text) if m.heightInput.Focused() { m.heightInput.Cursor.SetMode(cursor.CursorBlink) } else { m.heightInput.Cursor.SetMode(cursor.CursorHide) } width := inputStyle.Render(m.widthInput.View()) height := inputStyle.Render(m.heightInput.View()) return lipgloss.JoinHorizontal(lipgloss.Top, width, height) } func (m Model) drawCharRatioForm() string { prompt, text := m.getInputColors(CharRatioForm) m.charRatioInput.Width = 30 m.charRatioInput.PromptStyle = m.charRatioInput.PromptStyle.Copy().Width(20).Foreground(prompt) m.charRatioInput.TextStyle = m.charRatioInput.TextStyle.Copy().Foreground(text) if m.charRatioInput.Focused() { m.charRatioInput.Cursor.SetMode(cursor.CursorBlink) } else { m.charRatioInput.Cursor.SetMode(cursor.CursorHide) } return inputStyle.Copy().Width(28).AlignHorizontal(lipgloss.Left).PaddingTop(1).Render(m.charRatioInput.View()) } func (m Model) getInputColors(state State) (lipgloss.Color, lipgloss.Color) { if m.focus == state { if m.active == state { return activeColor, focusColor } else { return focusColor, activeColor } } return normalColor, normalColor } ``` -------------------------------------------------------------------------------- /controls/settings/palettes/adaptive/view.go: -------------------------------------------------------------------------------- ```go package adaptive import ( "github.com/charmbracelet/bubbles/cursor" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/style" ) var ( stateOrder = []State{CountForm, IterForm} stateNames = map[State]string{ CountForm: "Colors", IterForm: "Passes", } inputStyle = lipgloss.NewStyle().Width(13).AlignHorizontal(lipgloss.Left) activeColor = lipgloss.Color("#aaaaaa") focusColor = lipgloss.Color("#ffffff") normalColor = lipgloss.Color("#555555") titleStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#888888")) ) func (m Model) drawTitle() string { title := style.DimmedTitle.Copy().Italic(true).Render("Create palette From image") return lipgloss.NewStyle().Width(m.width).PaddingBottom(1).AlignHorizontal(lipgloss.Center).Render(title) } func (m Model) drawInputs() string { prompt, placeholder := m.getInputColors(CountForm) m.countInput.PromptStyle = m.countInput.PromptStyle.Copy().Foreground(prompt) m.countInput.PlaceholderStyle = m.countInput.PlaceholderStyle.Copy().Foreground(placeholder) if m.countInput.Focused() { m.countInput.Cursor.SetMode(cursor.CursorBlink) } else { m.countInput.Cursor.SetMode(cursor.CursorHide) } prompt, placeholder = m.getInputColors(IterForm) m.iterInput.PromptStyle = m.countInput.PromptStyle.Copy().Foreground(prompt) m.iterInput.PlaceholderStyle = m.countInput.PlaceholderStyle.Copy().Foreground(placeholder) if m.iterInput.Focused() { m.iterInput.Cursor.SetMode(cursor.CursorBlink) } else { m.iterInput.Cursor.SetMode(cursor.CursorHide) } countInput := inputStyle.Render(m.countInput.View()) iterInput := inputStyle.Render(m.iterInput.View()) return lipgloss.JoinHorizontal(lipgloss.Top, countInput, iterInput) } func (m Model) drawGenerateButton() string { styleColor := normalColor if m.IsActive && m.focus == Generate { styleColor = focusColor } else if m.active == Generate { styleColor = activeColor } style := lipgloss.NewStyle(). Width(m.width - 4). AlignHorizontal(lipgloss.Center). BorderStyle(lipgloss.RoundedBorder()). BorderForeground(styleColor). Foreground(styleColor) button := style.Render("Generate New") return lipgloss.NewStyle().Width(m.width - 2).AlignHorizontal(lipgloss.Center).Render(button) } // TODO: This is almost the same as drawGenerateButton. See if we can generalize func (m Model) drawSaveButton() string { styleColor := normalColor if m.IsActive && m.focus == Save { styleColor = focusColor } else if m.active == Save { styleColor = activeColor } style := lipgloss.NewStyle(). Width(m.width - 4). AlignHorizontal(lipgloss.Center). PaddingTop(1). Foreground(styleColor) button := style.Render("Save to .hex File") return lipgloss.NewStyle().Width(m.width - 2).AlignHorizontal(lipgloss.Center).Render(button) } func (m Model) getInputColors(state State) (lipgloss.Color, lipgloss.Color) { if m.IsActive { if m.focus == state { return focusColor, focusColor } else if m.active == state { return activeColor, activeColor } } return normalColor, normalColor } ``` -------------------------------------------------------------------------------- /controls/settings/advanced/view.go: -------------------------------------------------------------------------------- ```go package advanced import ( "strings" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/style" ) var ( inactiveTabBorder = tabBorderWithBottom("┴", "─", "┴") activeTabBorder = tabBorderWithBottom("┘", " ", "└") docStyle = lipgloss.NewStyle().Padding(0) inactiveTabStyle = lipgloss.NewStyle().Border(inactiveTabBorder, true) activeTabStyle = lipgloss.NewStyle().Border(activeTabBorder, true) focusTabStyle = activeTabStyle.Copy().BorderForeground(style.SelectedColor1) windowStyle = lipgloss.NewStyle().Align(lipgloss.Left).Border(lipgloss.NormalBorder()).UnsetBorderTop().Padding(1, 0) stateNames = map[State]string{Sampling: "Sampling", Dithering: "Dithering"} ) func (m Model) drawTabs() string { doc := strings.Builder{} var renderedTabs []string tabs := []State{Sampling, Dithering} borderColor := style.DimmedColor2 if m.IsActive { borderColor = style.NormalColor1 } for i, t := range tabs { var tabStyle lipgloss.Style isFirst, isLast, isActive, isActiveTab := i == 0, i == len(tabs)-1, m.focus == t, m.activeTab == t fgColor := style.DimmedColor2 if m.IsActive { if isActive { fgColor = style.SelectedColor1 } else { fgColor = style.DimmedColor1 } } else { if isActive { fgColor = style.NormalColor2 } } if m.activeTab == t { tabStyle = activeTabStyle.Copy() } else { tabStyle = inactiveTabStyle.Copy() } border, _, _, _, _ := tabStyle.GetBorder() if isFirst && isActiveTab { border.BottomLeft = "│" } else if isFirst && !isActiveTab { border.BottomLeft = "├" } else if isLast && isActiveTab { border.BottomRight = "└" } else if isLast && !isActiveTab { border.BottomRight = "┴" } tabStyle = tabStyle.Border(border).BorderForeground(borderColor).Foreground(fgColor) renderedTabs = append(renderedTabs, tabStyle.Render(stateNames[t])) } tabBlock := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...) extW, extH := max(m.width-lipgloss.Width(tabBlock)-2, 0), 1 border := lipgloss.Border{BottomLeft: "─", Bottom: "─", BottomRight: "┐"} extendedStyle := windowStyle.Copy().Border(border).BorderForeground(borderColor).Padding(0) extended := extendedStyle.Copy().Width(extW).Height(extH).Render("") renderedTabs = append(renderedTabs, extended) row := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...) doc.WriteString(row) doc.WriteString("\n") content := m.drawTabContent() doc.WriteString(windowStyle.Copy().BorderForeground(borderColor).Width(lipgloss.Width(row) - windowStyle.GetHorizontalFrameSize()).Render(content)) return docStyle.Render(doc.String()) } func (m Model) drawTabContent() string { switch m.activeTab { case Sampling: return m.sampling.View() case Dithering: return m.dithering.View() } return "" } func max(a, b int) int { if a > b { return a } return b } func min(a, b int) int { if a < b { return a } return b } func tabBorderWithBottom(left, middle, right string) lipgloss.Border { border := lipgloss.RoundedBorder() border.BottomLeft = left border.Bottom = middle border.BottomRight = right return border } ``` -------------------------------------------------------------------------------- /controls/settings/palettes/adaptive/update.go: -------------------------------------------------------------------------------- ```go package adaptive import ( "fmt" "image/color" "os" "path/filepath" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/Zebbeni/ansizalizer/event" ) type Direction int const ( Left Direction = iota Right Up Down ) var navMap = map[Direction]map[State]State{ Right: {CountForm: IterForm}, Left: {IterForm: CountForm}, Up: {Generate: CountForm, Save: Generate}, Down: {CountForm: Generate, IterForm: Generate, Generate: Save}, } func (m Model) handleEsc() (Model, tea.Cmd) { m.ShouldClose = true m.IsSelected = false return m, nil } func (m Model) handleEnter() (Model, tea.Cmd) { m.active = m.focus m.IsSelected = true switch m.active { case CountForm: m.countInput.Focus() return m, nil case IterForm: m.iterInput.Focus() return m, nil case Save: return m.savePaletteFile() } return m, event.StartAdaptingCmd } func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { var cmd tea.Cmd switch { case key.Matches(msg, event.KeyMap.Right): if next, hasNext := navMap[Right][m.focus]; hasNext { m.focus = next } case key.Matches(msg, event.KeyMap.Left): if next, hasNext := navMap[Left][m.focus]; hasNext { m.focus = next } case key.Matches(msg, event.KeyMap.Down): if next, hasNext := navMap[Down][m.focus]; hasNext { m.focus = next } else { m.IsSelected = false m.ShouldUnfocus = true } case key.Matches(msg, event.KeyMap.Up): if next, hasNext := navMap[Up][m.focus]; hasNext { m.focus = next } else { m.IsSelected = false m.ShouldUnfocus = true } } return m, cmd } func (m Model) handleCountUpdate(msg tea.Msg) (Model, tea.Cmd) { if keyMsg, ok := msg.(tea.KeyMsg); ok { switch { case key.Matches(keyMsg, event.KeyMap.Enter): m.IsSelected = true m.countInput.Blur() return m, event.StartAdaptingCmd case key.Matches(keyMsg, event.KeyMap.Esc): m.countInput.Blur() } } var cmd tea.Cmd m.countInput, cmd = m.countInput.Update(msg) return m, cmd } func (m Model) handleIterUpdate(msg tea.Msg) (Model, tea.Cmd) { if keyMsg, ok := msg.(tea.KeyMsg); ok { switch { case key.Matches(keyMsg, event.KeyMap.Enter): m.IsSelected = true m.iterInput.Blur() return m, event.StartAdaptingCmd case key.Matches(keyMsg, event.KeyMap.Esc): m.iterInput.Blur() } } var cmd tea.Cmd m.iterInput, cmd = m.iterInput.Update(msg) return m, cmd } func (m Model) savePaletteFile() (Model, tea.Cmd) { filename := fmt.Sprintf("%s.hex", m.palette.Name()) f, err := os.Create(filename) if err != nil { return m, event.BuildDisplayCmd("error saving palette file") } defer f.Close() var hexStrings string for _, c := range m.palette.Colors() { hexStrings += hexColor(c) + "\n" if err != nil { return m, event.BuildDisplayCmd("error writing to palette file") } } _, err = f.WriteString(hexStrings) dir, _ := os.Getwd() msg := fmt.Sprintf("saved %s in /%s", filename, filepath.Base(dir)) return m, event.BuildDisplayCmd(msg) } func hexColor(c color.Color) string { rgba := color.RGBAModel.Convert(c).(color.RGBA) return fmt.Sprintf("%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B) } ``` -------------------------------------------------------------------------------- /controls/settings/size/update.go: -------------------------------------------------------------------------------- ```go package size import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/Zebbeni/ansizalizer/event" ) type Direction int const ( Left Direction = iota Right Up Down ) var navMap = map[Direction]map[State]State{ Right: {FitButton: StretchButton, WidthForm: HeightForm}, Left: {StretchButton: FitButton, HeightForm: WidthForm}, Up: {WidthForm: FitButton, HeightForm: StretchButton, CharRatioForm: HeightForm}, Down: {FitButton: WidthForm, StretchButton: HeightForm, WidthForm: CharRatioForm, HeightForm: CharRatioForm}, } func (m Model) handleEsc() (Model, tea.Cmd) { m.ShouldClose = true return m, nil } func (m Model) handleEnter() (Model, tea.Cmd) { if m.active == m.focus { if m.active == FitButton || m.active == StretchButton { m.ShouldClose = true return m, nil } else { switch m.active { case WidthForm: m.widthInput.Blur() m.active = None case HeightForm: m.heightInput.Blur() m.active = None case CharRatioForm: m.charRatioInput.Blur() m.active = None } return m, event.StartRenderToViewCmd } } m.active = m.focus switch m.active { case FitButton: m.mode = Fit case StretchButton: m.mode = Stretch case WidthForm: m.widthInput.Focus() case HeightForm: m.heightInput.Focus() case CharRatioForm: m.charRatioInput.Focus() } return m, event.StartRenderToViewCmd } func (m Model) handleWidthUpdate(msg tea.Msg) (Model, tea.Cmd) { if keyMsg, ok := msg.(tea.KeyMsg); ok { switch { case key.Matches(keyMsg, event.KeyMap.Enter): m.widthInput.Blur() return m, event.StartRenderToViewCmd case key.Matches(keyMsg, event.KeyMap.Esc): m.widthInput.Blur() } } var cmd tea.Cmd m.widthInput, cmd = m.widthInput.Update(msg) return m, cmd } func (m Model) handleHeightUpdate(msg tea.Msg) (Model, tea.Cmd) { if keyMsg, ok := msg.(tea.KeyMsg); ok { switch { case key.Matches(keyMsg, event.KeyMap.Enter): m.heightInput.Blur() return m, event.StartRenderToViewCmd case key.Matches(keyMsg, event.KeyMap.Esc): m.heightInput.Blur() } } var cmd tea.Cmd m.heightInput, cmd = m.heightInput.Update(msg) return m, cmd } func (m Model) handleCharRatioUpdate(msg tea.Msg) (Model, tea.Cmd) { if keyMsg, ok := msg.(tea.KeyMsg); ok { switch { case key.Matches(keyMsg, event.KeyMap.Enter): m.charRatioInput.Blur() return m, event.StartRenderToViewCmd case key.Matches(keyMsg, event.KeyMap.Esc): m.charRatioInput.Blur() } } var cmd tea.Cmd m.charRatioInput, cmd = m.charRatioInput.Update(msg) return m, cmd } func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { var cmd tea.Cmd switch { case key.Matches(msg, event.KeyMap.Right): if next, hasNext := navMap[Right][m.focus]; hasNext { m.focus = next } case key.Matches(msg, event.KeyMap.Left): if next, hasNext := navMap[Left][m.focus]; hasNext { m.focus = next } case key.Matches(msg, event.KeyMap.Up): if next, hasNext := navMap[Up][m.focus]; hasNext { m.focus = next } else { m.ShouldClose = true } case key.Matches(msg, event.KeyMap.Down): if next, hasNext := navMap[Down][m.focus]; hasNext { m.focus = next } else { m.ShouldClose = true } } return m, cmd } ``` -------------------------------------------------------------------------------- /controls/settings/palettes/lospec/model.go: -------------------------------------------------------------------------------- ```go package lospec import ( "fmt" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/event" "github.com/Zebbeni/ansizalizer/palette" "github.com/Zebbeni/ansizalizer/style" ) type State int const ( CountForm State = iota TagForm FilterExact FilterMax FilterMin SortAlphabetical SortDownloads SortNewest List ) type Model struct { focus State active State countInput textinput.Model tagInput textinput.Model filterType State sortType State paletteList list.Model palettes []list.Item palette palette.Model isPaletteListAllocated bool highestPageRequested int requestID int ShouldClose bool ShouldUnfocus bool IsActive bool IsSelected bool // true if we've selected something (ie. render w/ lospec) width int didInitializeList bool } func New(w int) Model { return Model{ focus: CountForm, countInput: newInput(CountForm, "16"), tagInput: newInput(TagForm, ""), filterType: FilterMin, sortType: SortDownloads, isPaletteListAllocated: false, highestPageRequested: 0, requestID: 0, ShouldClose: false, ShouldUnfocus: false, IsActive: false, IsSelected: false, width: w, } } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { switch m.active { case CountForm: if m.countInput.Focused() { return m.handleCountFormUpdate(msg) } case TagForm: if m.tagInput.Focused() { return m.handleTagFormUpdate(msg) } } switch msg := msg.(type) { case event.LospecResponseMsg: return m.handleLospecResponse(msg) case tea.KeyMsg: if m.focus == List { return m.handleListUpdate(msg) } switch { case key.Matches(msg, event.KeyMap.Enter): return m.handleEnter() case key.Matches(msg, event.KeyMap.Nav): return m.handleNav(msg) case key.Matches(msg, event.KeyMap.Esc): return m.handleEsc() } } return m, nil } // View draws a control panel like this: // // Colors ___ |Exact Max Min // Tag _____________________ // Sort By |A-Z Downloads New // // (Palette List) // <palette name> // <preview> // <...> // <...> // .. func (m Model) View() string { title := m.drawTitle() colorsInput := m.drawColorsInput() filters := m.drawFilterButtons() colorFilters := lipgloss.JoinHorizontal(lipgloss.Left, colorsInput, filters) tagInput := m.drawTagInput() sortButtons := m.drawSortButtons() results := fmt.Sprintf("%d results found\npage %d of %d", len(m.paletteList.Items()), m.paletteList.Paginator.Page, m.paletteList.Paginator.TotalPages) results = style.DimmedTitle.Copy().Width(m.width).Height(2).AlignHorizontal(lipgloss.Center).Padding(1, 0, 1, 0).Render(results) paletteList := m.paletteList.View() if len(m.paletteList.Items()) == 0 { paletteList = "" } return lipgloss.JoinVertical(lipgloss.Top, title, colorFilters, tagInput, sortButtons, results, paletteList) } func (m Model) LoadInitial() (Model, tea.Cmd) { return m.searchLospec(0) } func (m Model) GetCurrent() palette.Model { return m.palette } ``` -------------------------------------------------------------------------------- /controls/settings/characters/view.go: -------------------------------------------------------------------------------- ```go package characters import ( "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/style" ) var ( stateOrder = []State{Ascii, Unicode, Custom} asciiButtonOrder = []State{AsciiAz, AsciiNums, AsciiSpec, AsciiAll} unicodeButtonOrder = []State{UnicodeFull, UnicodeHalf, UnicodeQuart, UnicodeShadeLight, UnicodeShadeMed, UnicodeShadeHeavy} stateNames = map[State]string{ Ascii: "Ascii", Unicode: "Unicode", Custom: "Custom", AsciiAz: "AZ", AsciiNums: "0-9", AsciiSpec: "!$", AsciiAll: "All", UnicodeFull: "█", UnicodeHalf: "▀▄", UnicodeQuart: "▞▟", UnicodeShadeLight: "░", UnicodeShadeMed: "▒", UnicodeShadeHeavy: "▓", OneColor: "1 Color", TwoColor: "2 Colors", } activeColor = lipgloss.Color("#aaaaaa") focusColor = lipgloss.Color("#ffffff") normalColor = lipgloss.Color("#555555") titleStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#888888")) ) func (m Model) drawCharControls() string { if m.charControls == Custom { content := m.drawCustomControls() return lipgloss.NewStyle().Width(m.width).AlignHorizontal(lipgloss.Left).Render(content) } whitespace := 0 var buttonOrder []State switch m.charControls { case Ascii: buttonOrder = asciiButtonOrder case Unicode: buttonOrder = unicodeButtonOrder } buttons := make([]string, len(buttonOrder)) for i, state := range buttonOrder { buttonStyle := style.NormalButtonNode if m.IsActive && state == m.focus { buttonStyle = style.FocusButtonNode } else if state == m.asciiMode || state == m.unicodeMode { buttonStyle = style.ActiveButtonNode } buttons[i] = buttonStyle.Copy().Render(stateNames[state]) whitespace += lipgloss.Width(buttons[i]) } gapSpace := whitespace / (len(buttons)) for i, button := range buttons { buttons[i] = lipgloss.NewStyle().PaddingRight(gapSpace).Render(button) } content := lipgloss.JoinHorizontal(lipgloss.Left, buttons...) return lipgloss.NewStyle().Width(m.width).AlignHorizontal(lipgloss.Left).Render(content) } func (m Model) drawCustomControls() string { nodeStyle := style.NormalButtonNode.Copy().PaddingRight(1) if m.customInput.Focused() { nodeStyle = style.ActiveButtonNode.Copy().PaddingRight(1) } else if m.focus == SymbolsForm { nodeStyle = style.FocusButtonNode.Copy().PaddingRight(1) } m.customInput.PromptStyle = nodeStyle.Copy() return m.customInput.View() } func (m Model) drawColorsButtons() string { title := style.DimmedTitle.Copy().PaddingLeft(1).Render("Colors per Char:") oneStyle := style.NormalButtonNode if m.IsActive && OneColor == m.focus { oneStyle = style.FocusButtonNode } else if m.useFgBg == OneColor { oneStyle = style.ActiveButtonNode } oneButton := oneStyle.Render("1") oneButton = lipgloss.NewStyle().Width(5).AlignHorizontal(lipgloss.Center).Render(oneButton) twoStyle := style.NormalButtonNode if m.IsActive && TwoColor == m.focus { twoStyle = style.FocusButtonNode } else if m.useFgBg == TwoColor { twoStyle = style.ActiveButtonNode } twoButton := twoStyle.Render("2") twoButton = lipgloss.NewStyle().Width(5).AlignHorizontal(lipgloss.Center).Render(twoButton) return lipgloss.JoinHorizontal(lipgloss.Left, title, oneButton, twoButton) } ``` -------------------------------------------------------------------------------- /controls/export/source/view.go: -------------------------------------------------------------------------------- ```go package source import ( "fmt" "path/filepath" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/style" ) var ( stateNames = map[State]string{ ExpFile: "Single File", ExpDirectory: "Directory", } ) func (m Model) drawExportTypeOptions() string { widthStyle := lipgloss.NewStyle().Width((m.width / 2) - 2).AlignHorizontal(lipgloss.Center) optionStyle := style.NormalButton if ExpFile == m.focus && m.IsActive { optionStyle = style.FocusButton } else if m.doExportDirectory == false { optionStyle = style.ActiveButton } singleFileButtonText := widthStyle.Render(stateNames[ExpFile]) singleFileButton := optionStyle.Render(singleFileButtonText) optionStyle = style.NormalButton if ExpDirectory == m.focus && m.IsActive { optionStyle = style.FocusButton } else if m.doExportDirectory { optionStyle = style.ActiveButton } directoryButtonText := widthStyle.Render(stateNames[ExpDirectory]) directoryButton := optionStyle.Render(directoryButtonText) return lipgloss.JoinHorizontal(lipgloss.Center, singleFileButton, directoryButton) } func (m Model) drawSubDirOptions() string { title := style.DimmedTitle.Copy().Render("Include Subdirectories") nodeWidthStyle := lipgloss.NewStyle().Width(m.width / 2).AlignHorizontal(lipgloss.Center) yesStyle := style.NormalButtonNode.Copy() if m.includeSubdirectories { yesStyle = style.ActiveButtonNode.Copy() } if m.focus == SubDirsYes { yesStyle = style.FocusButtonNode.Copy() } yesNode := nodeWidthStyle.Render(yesStyle.Render("Yes")) noStyle := style.NormalButtonNode.Copy() if !m.includeSubdirectories { noStyle = style.ActiveButtonNode.Copy() } if m.focus == SubDirsNo { noStyle = style.FocusButtonNode.Copy() } noStyle.Padding(0) noNode := nodeWidthStyle.Render(noStyle.Render("No")) options := lipgloss.JoinHorizontal(lipgloss.Center, yesNode, noNode) widthStyle := lipgloss.NewStyle().Width(m.width).AlignHorizontal(lipgloss.Left).PaddingBottom(1) content := lipgloss.JoinVertical(lipgloss.Center, title, options) return widthStyle.Render(content) } func (m Model) drawPrompt() string { return style.DimmedTitle.Copy().AlignHorizontal(lipgloss.Center).Padding(0).Render("Select") } func (m Model) drawSelected() string { title := style.DimmedTitle.Copy().Render("Selected") valueStyle := style.DimmedTitle.Copy() if Input == m.focus { if m.IsActive { valueStyle = style.SelectedTitle.Copy() } else { valueStyle = style.NormalTitle.Copy() } } valueStyle.Padding(0, 0, 1, 0) path := m.Browser.SelectedFile if m.doExportDirectory { path = m.Browser.SelectedDir } parent := filepath.Base(filepath.Dir(path)) selected := filepath.Base(path) value := fmt.Sprintf("%s/%s", parent, selected) valueRunes := []rune(value) if len(valueRunes) > m.width { value = string(valueRunes[len(valueRunes)-m.width:]) } valueContent := valueStyle.Render(value) widthStyle := lipgloss.NewStyle().Width(m.width).AlignHorizontal(lipgloss.Center) content := lipgloss.JoinVertical(lipgloss.Center, title, valueContent) return widthStyle.Render(content) } func (m Model) drawBrowserTitle() string { if m.doExportDirectory { return style.DimmedTitle.Copy().Padding(0, 2, 1, 2).Render("Select a directory") } return style.DimmedTitle.Copy().Padding(0, 2, 1, 2).Render("Select a .png or .jpg file") } ``` -------------------------------------------------------------------------------- /controls/settings/advanced/dithering/list.go: -------------------------------------------------------------------------------- ```go package dithering import ( "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/lipgloss" "github.com/makeworld-the-better-one/dither/v2" "github.com/Zebbeni/ansizalizer/style" ) type MatrixType int const ( Atkinson MatrixType = iota Burkes FloydSteinberg FalseFloydSteinberg JarvisJudiceNinke Sierra Sierra2 Sierra3 SierraLite TwoRowSierra Sierra2_4A Simple2D StevenPigeon Stucki ) var Matrices = []MatrixType{ Atkinson, Burkes, FloydSteinberg, FalseFloydSteinberg, JarvisJudiceNinke, Sierra, Sierra2, Sierra3, SierraLite, TwoRowSierra, Sierra2_4A, Simple2D, Stucki, StevenPigeon, } var nameMap = map[MatrixType]string{ Atkinson: "Atkinson", Burkes: "Burkes", FloydSteinberg: "FloydSteinberg", FalseFloydSteinberg: "FalseFloydSteinberg", JarvisJudiceNinke: "JarvisJudiceNinke", Sierra: "Sierra", Sierra2: "Sierra2", Sierra3: "Sierra3", SierraLite: "SierraLite", TwoRowSierra: "TwoRowSierra", Sierra2_4A: "Sierra2_4A", Simple2D: "Simple2D", Stucki: "Stucki", StevenPigeon: "StevenPigeon", } var errorDiffMatrixMap = map[MatrixType]dither.ErrorDiffusionMatrix{ Atkinson: dither.Atkinson, Burkes: dither.Burkes, FloydSteinberg: dither.FloydSteinberg, FalseFloydSteinberg: dither.FalseFloydSteinberg, JarvisJudiceNinke: dither.JarvisJudiceNinke, Sierra: dither.Sierra, Sierra2: dither.Sierra2, Sierra3: dither.Sierra3, SierraLite: dither.SierraLite, TwoRowSierra: dither.TwoRowSierra, Sierra2_4A: dither.Sierra2_4A, Simple2D: dither.Simple2D, Stucki: dither.Stucki, StevenPigeon: dither.StevenPigeon, } func newMatrixMenu(width int) list.Model { items := menuItems() return newMenu(items, width, len(items)) } type item struct { Type MatrixType } func (i item) FilterValue() string { return nameMap[i.Type] } func (i item) Title() string { return nameMap[i.Type] } func (i item) Description() string { return "" } func menuItems() []list.Item { items := make([]list.Item, len(Matrices)) for i, matrix := range Matrices { items[i] = item{Type: matrix} } return items } func newMenu(items []list.Item, width, height int) list.Model { l := list.New(items, NewDelegate(false), width, height/2) l.SetShowHelp(false) l.SetFilteringEnabled(false) l.SetShowTitle(false) l.SetShowPagination(true) l.SetShowStatusBar(false) l.KeyMap.ForceQuit.Unbind() l.KeyMap.Quit.Unbind() return l } func NewDelegate(isActive bool) list.DefaultDelegate { delegate := list.NewDefaultDelegate() delegate.SetSpacing(0) delegate.ShowDescription = false if isActive { delegate.Styles = ItemStylesActive() } else { delegate.Styles = ItemStylesInactive() } return delegate } func ItemStylesActive() (s list.DefaultItemStyles) { s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2) s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1). Border(lipgloss.NormalBorder(), false, false, false, true). BorderForeground(style.SelectedColor1) s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0) return s } func ItemStylesInactive() (s list.DefaultItemStyles) { s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2) s.SelectedTitle = style.NormalTitle.Copy().Padding(0, 1, 0, 2) s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0) return s } ``` -------------------------------------------------------------------------------- /controls/settings/palettes/lospec/view.go: -------------------------------------------------------------------------------- ```go package lospec import ( "github.com/charmbracelet/bubbles/cursor" "github.com/charmbracelet/lipgloss" "github.com/Zebbeni/ansizalizer/style" ) var ( stateNames = map[State]string{ CountForm: "Colors", TagForm: "Tag", FilterExact: "Exact", FilterMax: "Max", FilterMin: "Min", SortAlphabetical: "A-Z", SortDownloads: "Downloads", SortNewest: "Newest", } filterOrder = []State{FilterExact, FilterMax, FilterMin} sortOrder = []State{SortAlphabetical, SortDownloads, SortNewest} activeColor = lipgloss.Color("#aaaaaa") focusColor = lipgloss.Color("#ffffff") normalColor = lipgloss.Color("#555555") titleStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#888888")) ) func (m Model) drawInputs() string { colorsInput := m.drawColorsInput() tagInput := m.drawTagInput() return lipgloss.JoinHorizontal(lipgloss.Left, colorsInput, tagInput) } func (m Model) drawTitle() string { title := style.DimmedTitle.Copy().Italic(true).Render("Browse Lospec.com") return lipgloss.NewStyle().Width(m.width).PaddingBottom(1).AlignHorizontal(lipgloss.Center).Render(title) } func (m Model) drawColorsInput() string { prompt, placeholder := m.getInputColors(CountForm) m.countInput.CharLimit = 3 m.countInput.Width = 3 m.countInput.PromptStyle = m.countInput.PromptStyle.Copy().Foreground(prompt) m.countInput.TextStyle = m.countInput.TextStyle.Copy().Foreground(prompt).MaxWidth(3) m.countInput.PlaceholderStyle = m.countInput.PlaceholderStyle.Copy().Foreground(placeholder) if m.countInput.Focused() { m.countInput.Cursor.SetMode(cursor.CursorBlink) } else { m.countInput.Cursor.SetMode(cursor.CursorHide) } return lipgloss.NewStyle().Width(13).Render(m.countInput.View()) } func (m Model) drawTagInput() string { prompt, placeholder := m.getInputColors(TagForm) m.tagInput.Width = m.width - 5 m.tagInput.PromptStyle = m.countInput.PromptStyle.Copy().Foreground(prompt) m.tagInput.PlaceholderStyle = m.countInput.PlaceholderStyle.Copy().Foreground(placeholder) if m.tagInput.Focused() { m.tagInput.Cursor.SetMode(cursor.CursorBlink) } else { m.tagInput.Cursor.SetMode(cursor.CursorHide) } return m.tagInput.View() } func (m Model) drawFilterButtons() string { buttons := make([]string, len(filterOrder)) for i, filter := range filterOrder { buttonStyle := style.NormalButtonNode if filter == m.focus { buttonStyle = style.FocusButtonNode } else if filter == m.filterType { buttonStyle = style.ActiveButtonNode } buttons[i] = buttonStyle.Render(stateNames[filter]) } return lipgloss.JoinHorizontal(lipgloss.Left, buttons...) } func (m Model) drawSortButtons() string { title := style.DimmedTitle.Copy().PaddingLeft(1).Render("Sort:") buttons := make([]string, len(sortOrder)) for i, sort := range sortOrder { buttonStyle := style.NormalButtonNode if sort == m.focus { buttonStyle = style.FocusButtonNode } else if sort == m.sortType { buttonStyle = style.ActiveButtonNode } buttons[i] = buttonStyle.Render(stateNames[sort]) } buttonContent := lipgloss.JoinHorizontal(lipgloss.Left, buttons...) return lipgloss.JoinHorizontal(lipgloss.Left, title, buttonContent) } func (m Model) drawPaletteList() string { if len(m.paletteList.Items()) == 0 { return "" } return m.paletteList.View() } func (m Model) getInputColors(state State) (lipgloss.Color, lipgloss.Color) { if m.IsActive { if m.focus == state { return focusColor, focusColor } else if m.active == state { return activeColor, activeColor } } return normalColor, normalColor } ``` -------------------------------------------------------------------------------- /controls/settings/palettes/update.go: -------------------------------------------------------------------------------- ```go package palettes import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/Zebbeni/ansizalizer/event" ) type Direction int const ( Left Direction = iota Right Down Up ) var navMap = map[Direction]map[State]State{ Right: {Load: Adapt, Adapt: Lospec}, Left: {Lospec: Adapt, Adapt: Load}, Down: {Adapt: AdaptiveControls, Load: LoadControls, Lospec: LospecControls}, Up: {AdaptiveControls: Adapt, LoadControls: Load, LospecControls: Lospec}, } func (m Model) handleMenuUpdate(msg tea.Msg) (Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch { case key.Matches(msg, event.KeyMap.Esc): return m.handleEsc() case key.Matches(msg, event.KeyMap.Enter): return m.handleEnter() case key.Matches(msg, event.KeyMap.Nav): return m.handleNav(msg) } } return m, nil } func (m Model) handleAdaptiveUpdate(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.Adapter, cmd = m.Adapter.Update(msg) if m.Adapter.IsSelected { m.selected = Adapt } else if m.Adapter.ShouldUnfocus { m.Adapter.IsActive = true m.Adapter.ShouldUnfocus = false m.focus = Adapt } else if m.Adapter.ShouldClose { m.Adapter.IsActive = true m.Adapter.ShouldClose = false m.ShouldClose = true } return m, cmd } func (m Model) handleLoaderUpdate(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.Loader, cmd = m.Loader.Update(msg) if m.Loader.IsSelected { m.selected = Load } if m.Loader.ShouldUnfocus { m.Loader.ShouldUnfocus = false m.focus = Load } return m, cmd } func (m Model) handleLospecUpdate(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.Lospec, cmd = m.Lospec.Update(msg) if m.Lospec.IsSelected { m.selected = Lospec } else if m.Lospec.ShouldUnfocus { m.Lospec.IsActive = true m.Lospec.ShouldUnfocus = false m.focus = Lospec } else if m.Lospec.ShouldClose { m.Lospec.IsActive = true m.Lospec.ShouldClose = false m.ShouldClose = true } return m, cmd } func (m Model) handleEsc() (Model, tea.Cmd) { m.ShouldClose = true return m, nil } func (m Model) handleEnter() (Model, tea.Cmd) { m.selected = m.focus // Kick off a new palette generation before rendering if not done yet. // Allow the app to trigger a render when the generation is complete. if m.IsAdaptive() && len(m.Adapter.GetCurrent().Colors()) == 0 { return m, event.StartAdaptingCmd } return m, event.StartRenderToViewCmd } func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { var cmd tea.Cmd switch { case key.Matches(msg, event.KeyMap.Right): if next, hasNext := navMap[Right][m.focus]; hasNext { return m.setFocus(next) } case key.Matches(msg, event.KeyMap.Left): if next, hasNext := navMap[Left][m.focus]; hasNext { return m.setFocus(next) } case key.Matches(msg, event.KeyMap.Down): if next, hasNext := navMap[Down][m.focus]; hasNext { return m.setFocus(next) } else { m.IsActive = false m.ShouldClose = true } case key.Matches(msg, event.KeyMap.Up): if next, hasNext := navMap[Up][m.focus]; hasNext { return m.setFocus(next) } else { m.IsActive = false m.ShouldClose = true } } return m, cmd } func (m Model) setFocus(focus State) (Model, tea.Cmd) { var cmd tea.Cmd m.focus = focus switch m.focus { case Adapt: m.controls = Adapt case Load: m.controls = Load case Lospec: m.controls = Lospec case AdaptiveControls: m.Adapter.IsActive = true case LoadControls: m.controls = Load case LospecControls: m.Lospec.IsActive = true } if m.controls == Lospec && !m.Lospec.DidInitializeList() { m.Lospec, cmd = m.Lospec.InitializeList() } return m, cmd } ``` -------------------------------------------------------------------------------- /controls/settings/characters/update.go: -------------------------------------------------------------------------------- ```go package characters import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/Zebbeni/ansizalizer/event" ) type Direction int const ( Left Direction = iota Right Up Down ) var navMap = map[Direction]map[State]State{ Right: { Ascii: Unicode, Unicode: Custom, AsciiAz: AsciiNums, AsciiNums: AsciiSpec, AsciiSpec: AsciiAll, UnicodeFull: UnicodeHalf, UnicodeHalf: UnicodeQuart, UnicodeQuart: UnicodeShadeLight, UnicodeShadeLight: UnicodeShadeMed, UnicodeShadeMed: UnicodeShadeHeavy, OneColor: TwoColor, }, Left: { Unicode: Ascii, Custom: Unicode, AsciiAll: AsciiSpec, AsciiSpec: AsciiNums, AsciiNums: AsciiAz, UnicodeShadeHeavy: UnicodeShadeMed, UnicodeShadeMed: UnicodeShadeLight, UnicodeShadeLight: UnicodeQuart, UnicodeQuart: UnicodeHalf, UnicodeHalf: UnicodeFull, TwoColor: OneColor, }, Up: { Ascii: OneColor, Unicode: OneColor, Custom: OneColor, AsciiAz: Ascii, AsciiNums: Ascii, AsciiSpec: Ascii, AsciiAll: Ascii, UnicodeFull: Unicode, UnicodeHalf: Unicode, UnicodeQuart: Unicode, UnicodeShadeLight: Unicode, UnicodeShadeMed: Unicode, UnicodeShadeHeavy: Unicode, SymbolsForm: Custom, }, Down: { OneColor: Custom, TwoColor: Custom, Ascii: AsciiAz, Unicode: UnicodeShadeMed, Custom: SymbolsForm, }, } var ( asciiCharModeMap = map[State]bool{AsciiAz: true, AsciiNums: true, AsciiSpec: true, AsciiAll: true} unicodeCharModeMap = map[State]bool{UnicodeFull: true, UnicodeHalf: true, UnicodeQuart: true, UnicodeShadeLight: true, UnicodeShadeMed: true, UnicodeShadeHeavy: true} ) func (m Model) handleSymbolsFormUpdate(msg tea.Msg) (Model, tea.Cmd) { if keyMsg, ok := msg.(tea.KeyMsg); ok { switch { case key.Matches(keyMsg, event.KeyMap.Enter): m.customInput.Blur() return m, event.StartRenderToViewCmd case key.Matches(keyMsg, event.KeyMap.Esc): m.customInput.Blur() } } var cmd tea.Cmd m.customInput, cmd = m.customInput.Update(msg) return m, cmd } func (m Model) handleEsc() (Model, tea.Cmd) { m.ShouldClose = true return m, nil } func (m Model) handleEnter() (Model, tea.Cmd) { m.active = m.focus switch m.active { case Ascii: m.mode = Ascii case Unicode: m.mode = Unicode case Custom: m.mode = Custom case SymbolsForm: m.mode = Custom m.customInput.Focus() case OneColor, TwoColor: m.useFgBg = m.active default: switch m.charControls { case Ascii: if _, ok := asciiCharModeMap[m.active]; ok { m.asciiMode = m.active m.mode = Ascii } case Unicode: if _, ok := unicodeCharModeMap[m.active]; ok { m.unicodeMode = m.active m.mode = Unicode } } } return m, event.StartRenderToViewCmd } func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { var cmd tea.Cmd switch { case key.Matches(msg, event.KeyMap.Right): if next, hasNext := navMap[Right][m.focus]; hasNext { return m.setFocus(next) } case key.Matches(msg, event.KeyMap.Left): if next, hasNext := navMap[Left][m.focus]; hasNext { return m.setFocus(next) } case key.Matches(msg, event.KeyMap.Up): if next, hasNext := navMap[Up][m.focus]; hasNext { return m.setFocus(next) } else { m.IsActive = false m.ShouldClose = true } case key.Matches(msg, event.KeyMap.Down): if next, hasNext := navMap[Down][m.focus]; hasNext { return m.setFocus(next) } else { m.IsActive = false m.ShouldClose = true } } return m, cmd } func (m Model) setFocus(focus State) (Model, tea.Cmd) { m.focus = focus switch m.focus { case Ascii: m.charControls = Ascii case Unicode: m.charControls = Unicode case Custom: m.charControls = Custom } return m, nil } ``` -------------------------------------------------------------------------------- /app/process/unicode.go: -------------------------------------------------------------------------------- ```go package process import ( "image" _ "image/gif" _ "image/jpeg" _ "image/png" "math" "github.com/charmbracelet/lipgloss" "github.com/lucasb-eyer/go-colorful" "github.com/makeworld-the-better-one/dither/v2" "github.com/nfnt/resize" "github.com/Zebbeni/ansizalizer/controls/settings/characters" "github.com/Zebbeni/ansizalizer/controls/settings/size" ) var unicodeShadeChars = []rune{' ', '░', '▒', '▓'} func (m Renderer) processUnicode(input image.Image) string { imgW, imgH := float32(input.Bounds().Dx()), float32(input.Bounds().Dy()) dimensionType, width, height, charRatio := m.Settings.Size.Info() if dimensionType == size.Fit { fitHeight := float32(width) * (imgH / imgW) * float32(charRatio) fitWidth := (float32(height) * (imgW / imgH)) / float32(charRatio) if fitHeight > float32(height) { width = int(fitWidth) } else { height = int(fitHeight) } } resizeFunc := m.Settings.Advanced.SamplingFunction() refImg := resize.Resize(uint(width)*2, uint(height)*2, input, resizeFunc) isTrueColor, _, palette := m.Settings.Colors.GetSelected() isPaletted := !isTrueColor doDither, doSerpentine, matrix := m.Settings.Advanced.Dithering() if doDither && isPaletted { ditherer := dither.NewDitherer(palette.Colors()) ditherer.Matrix = matrix if doSerpentine { ditherer.Serpentine = true } refImg = ditherer.Dither(refImg) } content := "" rows := make([]string, height) row := make([]string, width) for y := 0; y < height*2; y += 2 { for x := 0; x < width*2; x += 2 { // r1 r2 // r3 r4 r1, _ := colorful.MakeColor(refImg.At(x, y)) r2, _ := colorful.MakeColor(refImg.At(x+1, y)) r3, _ := colorful.MakeColor(refImg.At(x, y+1)) r4, _ := colorful.MakeColor(refImg.At(x+1, y+1)) // pick the block, fg and bg color with the lowest total difference // convert the colors to ansi, render the block and add it at row[x] r, fg, bg := m.getBlock(r1, r2, r3, r4) pFg, _ := colorful.MakeColor(fg) pBg, _ := colorful.MakeColor(bg) lipFg := lipgloss.Color(pFg.Hex()) lipBg := lipgloss.Color(pBg.Hex()) style := lipgloss.NewStyle().Foreground(lipFg) if _, _, mode, _ := m.Settings.Characters.Selected(); mode == characters.TwoColor { style = style.Copy().Background(lipBg) } row[x/2] = style.Render(string(r)) } rows[y/2] = lipgloss.JoinHorizontal(lipgloss.Top, row...) } content += lipgloss.JoinVertical(lipgloss.Left, rows...) return content } // find the best block character and foreground and background colors to match // a set of 4 pixels. return func (m Renderer) getBlock(r1, r2, r3, r4 colorful.Color) (r rune, fg, bg colorful.Color) { var blockFuncs map[rune]blockFunc switch _, charSet, _, _ := m.Settings.Characters.Selected(); charSet { case characters.UnicodeFull: blockFuncs = m.fullBlockFuncs case characters.UnicodeHalf: blockFuncs = m.halfBlockFuncs case characters.UnicodeQuart: blockFuncs = m.quarterBlockFuncs case characters.UnicodeShadeLight: blockFuncs = m.shadeLightBlockFuncs case characters.UnicodeShadeMed: blockFuncs = m.shadeMedBlockFuncs case characters.UnicodeShadeHeavy: blockFuncs = m.shadeHeavyBlockFuncs } minDist := 100.0 for bRune, bFunc := range blockFuncs { f, b, dist := bFunc(r1, r2, r3, r4) if dist < minDist { minDist = dist r, fg, bg = bRune, f, b } } return } func (m Renderer) avgCol(colors ...colorful.Color) (colorful.Color, float64) { rSum, gSum, bSum := 0.0, 0.0, 0.0 for _, col := range colors { rSum += col.R gSum += col.G bSum += col.B } count := float64(len(colors)) avg := colorful.Color{R: rSum / count, G: gSum / count, B: bSum / count} if m.Settings.Colors.IsLimited() { _, _, palette := m.Settings.Colors.GetSelected() paletteAvg := palette.Colors().Convert(avg) avg, _ = colorful.MakeColor(paletteAvg) } // compute sum of squares totalDist := 0.0 for _, col := range colors { totalDist += math.Pow(col.DistanceCIEDE2000(avg), 2) } return avg, totalDist } ``` -------------------------------------------------------------------------------- /app/process/ascii.go: -------------------------------------------------------------------------------- ```go package process import ( "image" "math" "github.com/charmbracelet/lipgloss" "github.com/lucasb-eyer/go-colorful" "github.com/makeworld-the-better-one/dither/v2" "github.com/nfnt/resize" "github.com/Zebbeni/ansizalizer/controls/settings/characters" "github.com/Zebbeni/ansizalizer/controls/settings/size" ) // A list of Ascii characters by ascending brightness var asciiChars = []rune(" `.-':_,^=;><+!rc*/z?sLTv)J7(|Fi{C}fI31tlu[neoZ5Yxjya]2ESwqkP6h9d4VpOGbUAKXHm8RD#$Bg0MNWQ%&@") var asciiAZChars = []rune(" rczsLTvJFiCfItluneoZYxjyaESwqkPhdVpOGbUAKXHmRDBgMNWQ") var asciiNumChars = []rune(" 7315269480") var asciiSpecChars = []rune(" `.-':_,^=;><+!*/?)(|{}[]#$%&@") func (m Renderer) processAscii(input image.Image) string { imgW, imgH := float32(input.Bounds().Dx()), float32(input.Bounds().Dy()) dimensionType, width, height, charRatio := m.Settings.Size.Info() if dimensionType == size.Fit { fitHeight := float32(width) * (imgH / imgW) * float32(charRatio) fitWidth := (float32(height) * (imgW / imgH)) / float32(charRatio) if fitHeight > float32(height) { width = int(fitWidth) } else { height = int(fitHeight) } } resizeFunc := m.Settings.Advanced.SamplingFunction() refImg := resize.Resize(uint(width)*2, uint(height)*2, input, resizeFunc) isTrueColor, _, palette := m.Settings.Colors.GetSelected() isPaletted := !isTrueColor doDither, doSerpentine, matrix := m.Settings.Advanced.Dithering() if doDither && isPaletted { ditherer := dither.NewDitherer(palette.Colors()) ditherer.Matrix = matrix if doSerpentine { ditherer.Serpentine = true } refImg = ditherer.Dither(refImg) } var chars []rune _, charMode, useFgBg, _ := m.Settings.Characters.Selected() switch charMode { case characters.AsciiAz: chars = asciiAZChars case characters.AsciiNums: chars = asciiNumChars case characters.AsciiSpec: chars = asciiSpecChars case characters.AsciiAll: chars = asciiChars } content := "" rows := make([]string, height) row := make([]string, width) for y := 0; y < height*2; y += 2 { for x := 0; x < width*2; x += 2 { r1, isTrans1 := colorful.MakeColor(refImg.At(x, y)) r2, isTrans2 := colorful.MakeColor(refImg.At(x+1, y)) r3, isTrans3 := colorful.MakeColor(refImg.At(x, y+1)) r4, isTrans4 := colorful.MakeColor(refImg.At(x+1, y+1)) if isTrans1 || isTrans2 || isTrans3 || isTrans4 { isTrans2 = !isTrans2 == false } if useFgBg == characters.TwoColor { fg, bg, brightness := m.fgBgBrightness(r1, r2, r3, r4) lipFg := lipgloss.Color(fg.Hex()) lipBg := lipgloss.Color(bg.Hex()) style := lipgloss.NewStyle().Foreground(lipFg).Background(lipBg).Bold(true) index := min(int(brightness*float64(len(chars))), len(chars)-1) char := chars[index] charString := string(char) row[x/2] = style.Render(charString) } else { fg := m.avgColTrue(r1, r2, r3, r4) brightness := math.Min(1.0, math.Abs(fg.DistanceLuv(black))) if !isTrueColor { fg, _ = colorful.MakeColor(palette.Colors().Convert(fg)) } lipFg := lipgloss.Color(fg.Hex()) style := lipgloss.NewStyle().Foreground(lipFg).Bold(true) index := min(int(brightness*float64(len(chars))), len(chars)-1) char := chars[index] charString := string(char) row[x/2] = style.Render(charString) } } rows[y/2] = lipgloss.JoinHorizontal(lipgloss.Top, row...) } content += lipgloss.JoinVertical(lipgloss.Left, rows...) return content } func (m Renderer) fgBgBrightness(c ...colorful.Color) (fg, bg colorful.Color, b float64) { // find the darkest and lightest among given colors light, dark := lightDark(c...) avg := m.avgColTrue(c...) avgCol, _ := colorful.MakeColor(avg) //distLight := avgCol.DistanceLuv(light) distDark := avgCol.DistanceLuv(dark) distTotal := light.DistanceLuv(dark) var brightness float64 if distTotal == 0 { brightness = 0 } else { brightness = math.Min(1.0, math.Abs(distDark/distTotal)) } // if paletted: // convert the darkest to its closest paletted color // convert the lightest to its closest paletted color (excluding the previously found color) if m.Settings.Colors.IsLimited() { light, dark = m.getLightDarkPaletted(light, dark) } return light, dark, brightness } func (m Renderer) avgColTrue(colors ...colorful.Color) colorful.Color { rSum, gSum, bSum := 0.0, 0.0, 0.0 for _, col := range colors { rSum += col.R gSum += col.G bSum += col.B } count := float64(len(colors)) avg := colorful.Color{R: rSum / count, G: gSum / count, B: bSum / count} return avg } func lightDark(c ...colorful.Color) (light, dark colorful.Color) { mostLight, mostDark := 0.0, 1.0 for _, col := range c { _, _, l := col.Hsl() if l < mostDark { mostDark = l dark = col } if l > mostLight { mostLight = l light = col } } return } ``` -------------------------------------------------------------------------------- /controls/settings/palettes/loader/values.go: -------------------------------------------------------------------------------- ```go package loader import ( "fmt" "image/color" "github.com/lucasb-eyer/go-colorful" "github.com/muesli/termenv" ) func BlackAndWhite() color.Palette { return color.Palette{ color.RGBA{R: 0, G: 0, B: 0, A: 255}, color.RGBA{R: 255, G: 255, B: 255, A: 255}, } } func AnsiVga16() color.Palette { return color.Palette{ color.RGBA{R: 0, G: 0, B: 0, A: 255}, color.RGBA{R: 170, G: 0, B: 0, A: 255}, color.RGBA{R: 0, G: 170, B: 0, A: 255}, color.RGBA{R: 170, G: 85, B: 0, A: 255}, color.RGBA{R: 0, G: 0, B: 170, A: 255}, color.RGBA{R: 170, G: 0, B: 170, A: 255}, color.RGBA{R: 0, G: 170, B: 170, A: 255}, color.RGBA{R: 170, G: 170, B: 170, A: 255}, color.RGBA{R: 85, G: 85, B: 85, A: 255}, color.RGBA{R: 255, G: 85, B: 85, A: 255}, color.RGBA{R: 85, G: 255, B: 85, A: 255}, color.RGBA{R: 255, G: 255, B: 85, A: 255}, color.RGBA{R: 85, G: 85, B: 255, A: 255}, color.RGBA{R: 255, G: 85, B: 255, A: 255}, color.RGBA{R: 85, G: 255, B: 255, A: 255}, color.RGBA{R: 255, G: 255, B: 255, A: 255}, } } func AnsiWinConsole16() color.Palette { return color.Palette{ color.RGBA{R: 0, G: 0, B: 0, A: 255}, color.RGBA{R: 128, G: 0, B: 0, A: 255}, color.RGBA{R: 0, G: 128, B: 0, A: 255}, color.RGBA{R: 128, G: 128, B: 0, A: 255}, color.RGBA{R: 0, G: 0, B: 128, A: 255}, color.RGBA{R: 128, G: 0, B: 128, A: 255}, color.RGBA{R: 0, G: 128, B: 128, A: 255}, color.RGBA{R: 192, G: 192, B: 192, A: 255}, color.RGBA{R: 128, G: 128, B: 128, A: 255}, color.RGBA{R: 255, G: 0, B: 0, A: 255}, color.RGBA{R: 0, G: 255, B: 0, A: 255}, color.RGBA{R: 255, G: 255, B: 0, A: 255}, color.RGBA{R: 0, G: 0, B: 255, A: 255}, color.RGBA{R: 255, G: 0, B: 255, A: 255}, color.RGBA{R: 0, G: 255, B: 255, A: 255}, color.RGBA{R: 255, G: 255, B: 255, A: 255}, } } func AnsiWinPowershell16() color.Palette { return color.Palette{ color.RGBA{R: 12, G: 12, B: 12, A: 255}, color.RGBA{R: 197, G: 15, B: 31, A: 255}, color.RGBA{R: 19, G: 161, B: 14, A: 255}, color.RGBA{R: 193, G: 156, B: 0, A: 255}, color.RGBA{R: 0, G: 55, B: 218, A: 255}, color.RGBA{R: 136, G: 23, B: 152, A: 255}, color.RGBA{R: 58, G: 150, B: 221, A: 255}, color.RGBA{R: 204, G: 204, B: 204, A: 255}, color.RGBA{R: 118, G: 118, B: 118, A: 255}, color.RGBA{R: 231, G: 72, B: 86, A: 255}, color.RGBA{R: 22, G: 198, B: 12, A: 255}, color.RGBA{R: 249, G: 241, B: 165, A: 255}, color.RGBA{R: 59, G: 120, B: 255, A: 255}, color.RGBA{R: 180, G: 0, B: 158, A: 255}, color.RGBA{R: 97, G: 214, B: 214, A: 255}, color.RGBA{R: 242, G: 242, B: 242, A: 255}, } } func Ansi16() color.Palette { p := make(color.Palette, 0, 16) for i := 0; i < 16; i++ { ansi := termenv.ANSI.Color(fmt.Sprintf("%d", i)) col := termenv.ConvertToRGB(ansi) p = append(p, col) } return p } func Ansi256() color.Palette { p := make(color.Palette, 0, 256) for i := 0; i < 256; i++ { ansi := termenv.ANSI256.Color(fmt.Sprintf("%d", i)) col := termenv.ConvertToRGB(ansi) p = append(p, col) } return p } func KlarikFilmic() color.Palette { hexes := []string{ "#ffffff", "#d6dfdf", "#b5c4c1", "#8fa6a0", "#6f837e", "#536a66", "#2b3b3e", "#162424", "#000000", "#250a1d", "#3f1526", "#5a2535", "#82363f", "#a64e54", "#b66868", "#c08780", "#ceaea4", "#b2897c", "#9a6a5d", "#7c4d3f", "#5b2e2b", "#3d181b", "#280b15", "#895938", "#b1834e", "#bb995f", "#caac7a", "#d3c59f", "#a8ad80", "#84935a", "#5a7645", "#305630", "#1a3725", "#0e2724", "#152f3c", "#2d4e59", "#4b7674", "#628e87", "#7ca294", "#a5bbae", "#bacbc9", "#a1b7bf", "#778faa", "#5e6d92", "#424372", "#352959", "#2c173d", "#492854", "#6e3f72", "#935c8d", "#ae7d9e", "#c6a7b5", "#ac7b90", "#8f516c", "#73415a", "#542846", "#3f1831", } return hexesToColorPalette(hexes) } func Mudstone() color.Palette { hexes := []string{ "#1b1611", "#1f253c", "#423c32", "#465d32", "#6e3f24", "#6b624e", "#90752e", "#cda465", } return hexesToColorPalette(hexes) } func IsleOfTheDead() color.Palette { hexes := []string{ "#0b0b0b", "#454848", "#4f514f", "#5a5a5a", "#666666", "#3e3f3f", "#373838", "#242421", "#2c2d25", "#36382a", "#1b1b17", "#313333", "#858585", "#a0a0a0", "#717171", "#2c2d2d", "#121210", "#3f4132", "#aeaeae", "#575a4a", "#737359", "#858562", "#93906c", "#686652", "#a9a681", "#48534d", "#252928", "#857d62", "#aea282", "#d0cec1", "#c0b9a5", "#58503b", "#7a6b54", "#413a28", "#53493a", "#685a44", "#443b2e", "#1a201e", "#362e23", "#7a704d", "#222b31", "#364550", } return hexesToColorPalette(hexes) } func hexesToColorPalette(hexes []string) color.Palette { var colorPalette color.Palette for _, h := range hexes { c, _ := colorful.Hex(h) colorPalette = append(colorPalette, c) } return colorPalette } ``` -------------------------------------------------------------------------------- /controls/settings/palettes/lospec/update.go: -------------------------------------------------------------------------------- ```go package lospec import ( "fmt" "image/color" "strconv" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/lucasb-eyer/go-colorful" "github.com/Zebbeni/ansizalizer/event" "github.com/Zebbeni/ansizalizer/palette" "github.com/Zebbeni/ansizalizer/style" ) // TODO: Direction is redefined in multiple places type Direction int type Param int const ( Left Direction = iota Right Up Down ) var ( navMap = map[Direction]map[State]State{ Right: {CountForm: FilterExact, FilterExact: FilterMax, FilterMax: FilterMin, SortAlphabetical: SortDownloads, SortDownloads: SortNewest}, Left: {TagForm: CountForm, FilterMin: FilterMax, FilterMax: FilterExact, FilterExact: CountForm, SortNewest: SortDownloads, SortDownloads: SortAlphabetical}, Up: {TagForm: CountForm, SortAlphabetical: TagForm, SortDownloads: TagForm, SortNewest: TagForm, List: SortAlphabetical}, Down: {CountForm: TagForm, FilterExact: TagForm, FilterMax: TagForm, FilterMin: TagForm, TagForm: SortAlphabetical, SortAlphabetical: List, SortDownloads: List, SortNewest: List}, } filterParams = map[State]string{ FilterExact: "exact", FilterMax: "max", FilterMin: "min", } sortParams = map[State]string{ SortAlphabetical: "alphabetical", SortDownloads: "downloads", SortNewest: "newest", } ) func (m Model) handleEsc() (Model, tea.Cmd) { m.ShouldClose = true m.IsSelected = false m.ShouldUnfocus = true return m, nil } func (m Model) handleEnter() (Model, tea.Cmd) { m.active = m.focus switch m.focus { case CountForm: m.countInput.Focus() return m, nil case TagForm: m.tagInput.Focus() return m, nil case FilterExact, FilterMax, FilterMin: m.filterType = m.focus return m.searchLospec(0) case SortAlphabetical, SortDownloads, SortNewest: m.sortType = m.focus return m.searchLospec(0) case List: m.palette, _ = m.paletteList.SelectedItem().(palette.Model) m.IsSelected = true return m, event.StartRenderToViewCmd } return m, nil } func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { switch { case key.Matches(msg, event.KeyMap.Right): if next, hasNext := navMap[Right][m.focus]; hasNext { m.focus = next } case key.Matches(msg, event.KeyMap.Left): if next, hasNext := navMap[Left][m.focus]; hasNext { m.focus = next } case key.Matches(msg, event.KeyMap.Down): if next, hasNext := navMap[Down][m.focus]; hasNext { m.focus = next } case key.Matches(msg, event.KeyMap.Up): if next, hasNext := navMap[Up][m.focus]; hasNext { m.focus = next } else { m.IsSelected = false m.ShouldUnfocus = true } } return m, nil } func (m Model) handleLospecResponse(msg event.LospecResponseMsg) (Model, tea.Cmd) { var cmd tea.Cmd // return early if response no longer matches current requestID if msg.ID != m.requestID { return m, cmd } // if we haven't initialized and allocated an array of palettes for the current request series, do that first if !m.isPaletteListAllocated { m.palettes = make([]list.Item, msg.Data.TotalCount) m.paletteList = CreateList(m.palettes, m.width-2) m.paletteList.Styles.Title = style.DimmedTitle m.paletteList.Styles.TitleBar = m.paletteList.Styles.TitleBar.Padding(0).Width(m.width).AlignHorizontal(lipgloss.Center) m.isPaletteListAllocated = true } // use the page number*10 (assumes 10 palettes per page) to populate palettes for i, p := range msg.Data.Palettes { colors := make([]color.Color, len(p.Colors)) var err error for colorIndex, c := range p.Colors { colors[colorIndex], err = colorful.Hex(fmt.Sprintf("#%s", c)) if err != nil { return m, event.BuildDisplayCmd("error converting hex value") } } idx := (msg.Page * 10) + i m.palettes[idx] = palette.New(p.Title, colors, m.width-4, 2) } m.paletteList.SetItems(m.palettes) return m, cmd } func (m Model) handleCountFormUpdate(msg tea.Msg) (Model, tea.Cmd) { if keyMsg, ok := msg.(tea.KeyMsg); ok { switch { case key.Matches(keyMsg, event.KeyMap.Enter): m.countInput.Blur() return m.searchLospec(0) case key.Matches(keyMsg, event.KeyMap.Esc): m.countInput.Blur() } } var cmd tea.Cmd m.countInput, cmd = m.countInput.Update(msg) return m, cmd } func (m Model) handleTagFormUpdate(msg tea.Msg) (Model, tea.Cmd) { if keyMsg, ok := msg.(tea.KeyMsg); ok { switch { case key.Matches(keyMsg, event.KeyMap.Enter): m.tagInput.Blur() return m.searchLospec(0) case key.Matches(keyMsg, event.KeyMap.Esc): m.tagInput.Blur() } } var cmd tea.Cmd m.tagInput, cmd = m.tagInput.Update(msg) return m, cmd } func (m Model) handleListUpdate(msg tea.Msg) (Model, tea.Cmd) { keyMsg, ok := msg.(tea.KeyMsg) if !ok { return m, nil } switch { case key.Matches(keyMsg, event.KeyMap.Enter): return m.handleEnter() case key.Matches(keyMsg, event.KeyMap.Up) && m.paletteList.Index() == 0: return m.handleNav(keyMsg) case key.Matches(keyMsg, event.KeyMap.Esc): m.focus = TagForm } var cmd tea.Cmd if len(m.paletteList.Items()) > 0 { m.paletteList, cmd = m.paletteList.Update(msg) } if m.paletteList.Index() < (m.highestPageRequested-1)*10 { return m, cmd } m.highestPageRequested += 1 return m.searchLospec(m.highestPageRequested) } func (m Model) searchLospec(page int) (Model, tea.Cmd) { if page == 0 { m.requestID += 1 m.highestPageRequested = 0 m.isPaletteListAllocated = false } colors, _ := strconv.Atoi(m.countInput.Value()) tag := m.tagInput.Value() filterType := filterParams[m.filterType] sortingType := sortParams[m.sortType] urlString := "https://lospec.com/palette-list/load?colorNumber=%d&tag=%s&colorNumberFilterType=%s&sortingType=%s&page=%d" url := fmt.Sprintf(urlString, colors, tag, filterType, sortingType, page) return m, event.BuildLospecRequestCmd(event.LospecRequestMsg{ URL: url, ID: m.requestID, Page: page, }) } ```