This is page 1 of 2. Use http://codebase.md/Zebbeni/ansizalizer?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitignore ├── ansizalizer ├── app │ ├── adapt │ │ └── generate.go │ ├── export.go │ ├── item.go │ ├── model.go │ ├── process │ │ ├── ascii.go │ │ ├── custom.go │ │ ├── image.go │ │ ├── renderer.go │ │ └── unicode.go │ ├── resize.go │ ├── update.go │ └── view.go ├── assets │ └── palettes │ ├── android-screenshot-editor.hex │ ├── cascade-gb.hex │ ├── dull-aquatic.hex │ ├── florescence.hex │ ├── gb-blue-steel.hex │ ├── hama-beads-tub.hex │ ├── kiwami64-v1.hex │ └── yes.hex ├── controls │ ├── browser │ │ ├── item.go │ │ ├── model.go │ │ └── update.go │ ├── export │ │ ├── destination │ │ │ ├── model.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── model.go │ │ ├── source │ │ │ ├── model.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── update.go │ │ └── view.go │ ├── menu │ │ └── model.go │ ├── model.go │ ├── settings │ │ ├── advanced │ │ │ ├── dithering │ │ │ │ ├── list.go │ │ │ │ ├── model.go │ │ │ │ ├── update.go │ │ │ │ └── view.go │ │ │ ├── model.go │ │ │ ├── sampling │ │ │ │ ├── const.go │ │ │ │ ├── item.go │ │ │ │ ├── model.go │ │ │ │ └── update.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── characters │ │ │ ├── init.go │ │ │ ├── model.go │ │ │ ├── tabs.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── colors │ │ │ ├── model.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── item.go │ │ ├── model.go │ │ ├── palettes │ │ │ ├── adaptive │ │ │ │ ├── init.go │ │ │ │ ├── model.go │ │ │ │ ├── update.go │ │ │ │ └── view.go │ │ │ ├── loader │ │ │ │ ├── item.go │ │ │ │ ├── model.go │ │ │ │ ├── values.go │ │ │ │ └── view.go │ │ │ ├── lospec │ │ │ │ ├── init.go │ │ │ │ ├── list.go │ │ │ │ ├── model.go │ │ │ │ ├── update.go │ │ │ │ └── view.go │ │ │ ├── matrix.go │ │ │ ├── model.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── size │ │ │ ├── init.go │ │ │ ├── model.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── state.go │ │ ├── update.go │ │ └── view.go │ ├── update.go │ └── view.go ├── display │ └── model.go ├── env │ ├── os_darwin.go │ ├── os_linux.go │ └── os_windows.go ├── event │ ├── command.go │ └── keymap.go ├── global │ └── file.go ├── go.mod ├── go.sum ├── images │ └── characters │ ├── char_001.png │ ├── char_002.png │ ├── char_003.png │ ├── char_004.png │ ├── char_005.png │ ├── char_006.png │ ├── char_007.png │ ├── char_008.png │ ├── char_009.png │ ├── char_010.png │ ├── char_011.png │ ├── char_012.png │ ├── char_013.png │ ├── char_014.png │ ├── char_015.png │ ├── char_016.png │ ├── char_017.png │ ├── char_018.png │ ├── char_019.png │ ├── char_020.png │ ├── char_021.png │ ├── char_022.png │ ├── char_023.png │ ├── char_024.png │ ├── char_025.png │ ├── char_026.png │ ├── char_027.png │ └── char_028.png ├── LICENSE.md ├── main.go ├── palette │ ├── model.go │ └── view.go ├── README.md ├── style │ ├── box.go │ └── color.go ├── test_images │ ├── dock.png │ ├── mermaid.png │ ├── mona_lisa.jpg │ ├── planet.png │ ├── robots.png │ ├── sewer.png │ └── throne.png └── viewer ├── model.go └── update.go ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Images test directory 2 | images/* 3 | 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | *.idea/ 11 | *.hex 12 | *.ansi 13 | 14 | # Test binary, built with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | # Dependency directories (remove the comment below to include it) 21 | # vendor/ 22 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # ANSIZALIZER 2 | A TUI to convert Images to ANSI strings using bubbletea 3 | 4 |  5 | 6 | ## Features 7 | - A keyboard-navigable Text-based UI 8 | - File browser: Search .png and .jpeg image files and preview in real-time 9 | - Export ANSI image strings to '.ansi' text files or copy directly to your Clipboard 10 | - Save files individually or Batch Process All Images in a chosen directory 11 | - Browse Lospec.com for cool color palettes 12 | 13 | ## Render Options 14 | - Set output Width and Height of rendered text images (in characters) 15 | - Choose character sets to use in output (ASCII, Unicode, or Custom) 16 | - Render images with "true" colors or convert using Limited Color Palettes 17 | - Generate new color palettes by sampling previewed image files 18 | - Use Advanced settings to tweak pixel Sampling mode and Dithering options 19 | 20 |  21 | 22 | ## To Run 23 | 24 | **On Windows:** 25 | ```bash 26 | go install 27 | go build 28 | start ansizalizer.exe 29 | ``` 30 | 31 | **On Mac/Linux:** 32 | ```bash 33 | go install 34 | go build 35 | ./ansizalizer 36 | ``` 37 | 38 |  39 | 40 | ## FAQ / Troubleshooting 41 | **Q: The UI isn't rendering correctly** 42 | 43 | Check your default console appearance settings. Make sure your chosen font, font size, and line height aren't the cause of the problem. 'DejaVu Sans Mono' works well for me on Windows. 44 | 45 | **Q: My images look squashed / stretched** 46 | 47 | Try adjusting the value of Char Size Ratio under Settings > Size. Depending on what font your console uses, your characters may have a width-to-height ratio different than 0.5. 48 | 49 | **Q: My exported .ansi files take up more space than the original image** 50 | 51 | The ANSI code that produces the text-rendered images isn't (currently) optimized for file size. If using this tool to batch process lots of text art for use in a game or application, I'd consider compressing the resulting text files and decompressing them as needed. 52 | ``` -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- ```markdown 1 | MIT License 2 | 3 | Copyright (c) 2024 Andrew Albers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | ``` -------------------------------------------------------------------------------- /env/os_darwin.go: -------------------------------------------------------------------------------- ```go 1 | //go:build darwin 2 | 3 | package env 4 | 5 | const PollForSizeChange = false ``` -------------------------------------------------------------------------------- /env/os_linux.go: -------------------------------------------------------------------------------- ```go 1 | //go:build linux 2 | 3 | package env 4 | 5 | const PollForSizeChange = false 6 | ``` -------------------------------------------------------------------------------- /env/os_windows.go: -------------------------------------------------------------------------------- ```go 1 | //go:build windows 2 | 3 | package env 4 | 5 | const PollForSizeChange = true 6 | ``` -------------------------------------------------------------------------------- /global/file.go: -------------------------------------------------------------------------------- ```go 1 | package global 2 | 3 | var ( 4 | ImgExtensions = map[string]bool{".png": true, ".jpg": true, ".jpeg": true} 5 | ) 6 | ``` -------------------------------------------------------------------------------- /controls/settings/palettes/loader/item.go: -------------------------------------------------------------------------------- ```go 1 | package loader 2 | 3 | import ( 4 | "github.com/Zebbeni/ansizalizer/palette" 5 | ) 6 | 7 | type item struct { 8 | palette palette.Model 9 | } 10 | 11 | func (i item) FilterValue() string { 12 | return i.palette.Name() 13 | } 14 | 15 | func (i item) Title() string { 16 | return i.palette.Name() 17 | } 18 | 19 | func (i item) Description() string { 20 | return i.palette.View() 21 | } 22 | ``` -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- ```go 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | tea "github.com/charmbracelet/bubbletea" 8 | 9 | "github.com/Zebbeni/ansizalizer/app" 10 | "github.com/Zebbeni/ansizalizer/event" 11 | ) 12 | 13 | func init() { 14 | event.InitKeyMap() 15 | } 16 | 17 | func main() { 18 | m := app.New() 19 | p := tea.NewProgram(m) 20 | if _, err := p.Run(); err != nil { 21 | fmt.Println("Error running program:", err) 22 | os.Exit(1) 23 | } 24 | } 25 | ``` -------------------------------------------------------------------------------- /controls/settings/state.go: -------------------------------------------------------------------------------- ```go 1 | package settings 2 | 3 | type State int 4 | 5 | const ( 6 | None State = iota 7 | Colors 8 | Characters 9 | Size 10 | Advanced 11 | ) 12 | 13 | var States = []State{ 14 | Colors, 15 | Characters, 16 | Size, 17 | Advanced, 18 | } 19 | 20 | var stateOrder = []State{Colors, Characters, Size, Advanced} 21 | 22 | var stateTitles = map[State]string{ 23 | Colors: "Colors", 24 | Characters: "Characters", 25 | Size: "Size", 26 | Advanced: "Advanced", 27 | } 28 | ``` -------------------------------------------------------------------------------- /viewer/update.go: -------------------------------------------------------------------------------- ```go 1 | package viewer 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | tea "github.com/charmbracelet/bubbletea" 8 | 9 | "github.com/Zebbeni/ansizalizer/event" 10 | ) 11 | 12 | func (m Model) handleFinishRenderMsg(msg event.FinishRenderToViewMsg) (Model, tea.Cmd) { 13 | m.WaitingOnRender = false 14 | m.imgString = msg.ImgString 15 | 16 | displayMsg := fmt.Sprintf("viewing %s/%s with %s palette", filepath.Base(filepath.Dir(msg.FilePath)), filepath.Base(msg.FilePath), msg.ColorsString) 17 | return m, event.BuildDisplayCmd(displayMsg) 18 | } 19 | ``` -------------------------------------------------------------------------------- /controls/settings/item.go: -------------------------------------------------------------------------------- ```go 1 | package settings 2 | 3 | //type item struct { 4 | // name string 5 | // state State 6 | //} 7 | // 8 | //func (i item) FilterValue() string { 9 | // return i.name 10 | //} 11 | // 12 | //func (i item) Title() string { 13 | // return i.name 14 | //} 15 | // 16 | //func (i item) Description() string { 17 | // return "" 18 | //} 19 | 20 | //func newMenu() list.Model { 21 | // items := []list.Item{ 22 | // item{name: "Loader", state: Loader}, 23 | // item{name: "Advanced", state: Advanced}, 24 | // //item{name: "Limited", state: Limited}, 25 | // //item{name: "Characters", state: Characters}, 26 | // } 27 | // return menu.New(items) 28 | //} 29 | ``` -------------------------------------------------------------------------------- /controls/settings/palettes/adaptive/init.go: -------------------------------------------------------------------------------- ```go 1 | package adaptive 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/charmbracelet/bubbles/textinput" 7 | "github.com/charmbracelet/lipgloss" 8 | ) 9 | 10 | var ( 11 | promptStyle = lipgloss.NewStyle().Width(8).PaddingLeft(1) 12 | placeholderStyle = lipgloss.NewStyle() 13 | ) 14 | 15 | func newInput(state State) textinput.Model { 16 | textinput.New() 17 | input := textinput.New() 18 | input.Prompt = stateNames[state] 19 | input.PromptStyle = promptStyle 20 | input.PlaceholderStyle = placeholderStyle 21 | input.Cursor.Blink = true 22 | input.CharLimit = 3 23 | input.SetValue(fmt.Sprintf("16")) 24 | return input 25 | } 26 | ``` -------------------------------------------------------------------------------- /controls/settings/advanced/sampling/const.go: -------------------------------------------------------------------------------- ```go 1 | package sampling 2 | 3 | import "github.com/nfnt/resize" 4 | 5 | var Functions = []resize.InterpolationFunction{ 6 | resize.NearestNeighbor, 7 | resize.Bicubic, 8 | resize.Bilinear, 9 | resize.Lanczos2, 10 | resize.Lanczos3, 11 | resize.MitchellNetravali, 12 | } 13 | 14 | var nameMap = map[resize.InterpolationFunction]string{ 15 | resize.NearestNeighbor: "Nearest Neighbor", 16 | resize.Bicubic: "Bicubic", 17 | resize.Bilinear: "Bilinear", 18 | resize.Lanczos2: "Lanczos2", 19 | resize.Lanczos3: "Lanczos3", 20 | resize.MitchellNetravali: "MitchellNetravali", 21 | } 22 | ``` -------------------------------------------------------------------------------- /viewer/model.go: -------------------------------------------------------------------------------- ```go 1 | package viewer 2 | 3 | import ( 4 | tea "github.com/charmbracelet/bubbletea" 5 | 6 | "github.com/Zebbeni/ansizalizer/controls/settings" 7 | "github.com/Zebbeni/ansizalizer/event" 8 | ) 9 | 10 | type Model struct { 11 | imgString string 12 | settings settings.Model 13 | 14 | WaitingOnRender bool 15 | } 16 | 17 | func New() Model { 18 | return Model{} 19 | } 20 | 21 | func (m Model) Init() tea.Cmd { 22 | return nil 23 | } 24 | 25 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 26 | switch msg := msg.(type) { 27 | case event.FinishRenderToViewMsg: 28 | return m.handleFinishRenderMsg(msg) 29 | } 30 | return m, nil 31 | } 32 | 33 | func (m Model) View() string { 34 | if m.WaitingOnRender { 35 | return "" 36 | } 37 | return m.imgString 38 | } 39 | ``` -------------------------------------------------------------------------------- /controls/settings/characters/init.go: -------------------------------------------------------------------------------- ```go 1 | package characters 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/textinput" 5 | "github.com/charmbracelet/lipgloss" 6 | 7 | "github.com/Zebbeni/ansizalizer/style" 8 | ) 9 | 10 | var ( 11 | promptStyle = lipgloss.NewStyle().Padding(0, 1, 0, 1) 12 | placeholderStyle = lipgloss.NewStyle() 13 | ) 14 | 15 | // TODO: This is basically the same as we have in adaptive. Maybe generalize? 16 | func newInput(prompt string, value string) textinput.Model { 17 | textinput.New() 18 | input := textinput.New() 19 | input.Prompt = prompt 20 | input.PromptStyle = style.NormalButtonNode.Copy().Padding(0, 1, 0, 0) 21 | input.PlaceholderStyle = placeholderStyle 22 | input.Cursor.Blink = true 23 | input.SetValue(value) 24 | return input 25 | } 26 | ``` -------------------------------------------------------------------------------- /controls/settings/palettes/matrix.go: -------------------------------------------------------------------------------- ```go 1 | package palettes 2 | 3 | import ( 4 | "github.com/makeworld-the-better-one/dither/v2" 5 | ) 6 | 7 | type Matrix struct { 8 | Name string 9 | Method dither.ErrorDiffusionMatrix 10 | } 11 | 12 | func getMatrixMenuItems() []Matrix { 13 | return []Matrix{ 14 | Matrix{Name: "Simple2D", Method: dither.Simple2D}, 15 | Matrix{Name: "FloydSteinberg", Method: dither.FloydSteinberg}, 16 | Matrix{Name: "JarvisJudiceNinke", Method: dither.JarvisJudiceNinke}, 17 | Matrix{Name: "Atkinson", Method: dither.Atkinson}, 18 | Matrix{Name: "Stucki", Method: dither.Stucki}, 19 | Matrix{Name: "Burkes", Method: dither.Burkes}, 20 | Matrix{Name: "Sierra", Method: dither.Sierra}, 21 | Matrix{Name: "StevenPigeon", Method: dither.StevenPigeon}, 22 | } 23 | } 24 | ``` -------------------------------------------------------------------------------- /app/adapt/generate.go: -------------------------------------------------------------------------------- ```go 1 | package adapt 2 | 3 | import ( 4 | "bufio" 5 | "image" 6 | "image/color" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/mccutchen/palettor" 12 | 13 | "github.com/Zebbeni/ansizalizer/controls/settings/palettes/adaptive" 14 | ) 15 | 16 | func GeneratePalette(m adaptive.Model, imgFilePath string) (color.Palette, string) { 17 | if imgFilePath == "" { 18 | return nil, "" 19 | } 20 | 21 | var img image.Image 22 | imgFile, err := os.Open(imgFilePath) 23 | if err != nil { 24 | return nil, "" 25 | } 26 | defer imgFile.Close() 27 | imageReader := bufio.NewReader(imgFile) 28 | img, _, err = image.Decode(imageReader) 29 | if err != nil { 30 | return nil, "" 31 | } 32 | 33 | count, iterations := m.Info() 34 | palette, err := palettor.Extract(count, iterations, img) 35 | 36 | name := strings.Split(filepath.Base(imgFilePath), ".")[0] 37 | 38 | return palette.Colors(), name 39 | } 40 | ``` -------------------------------------------------------------------------------- /app/item.go: -------------------------------------------------------------------------------- ```go 1 | package app 2 | 3 | import "github.com/charmbracelet/bubbles/list" 4 | 5 | type item struct { 6 | name string 7 | state State 8 | } 9 | 10 | func (i item) FilterValue() string { 11 | return i.name 12 | } 13 | 14 | func (i item) Title() string { 15 | return i.name 16 | } 17 | 18 | func (i item) Description() string { 19 | return "" 20 | } 21 | 22 | func newMenu() list.Model { 23 | items := []list.Item{ 24 | item{name: "File", state: Browser}, 25 | item{name: "Settings", state: Settings}, 26 | } 27 | menu := list.New(items, NewDelegate(), 20, 20) 28 | menu.SetShowHelp(false) 29 | menu.SetShowFilter(false) 30 | menu.SetShowTitle(false) 31 | menu.SetShowStatusBar(false) 32 | 33 | menu.KeyMap.ForceQuit.Unbind() 34 | menu.KeyMap.Quit.Unbind() 35 | return menu 36 | } 37 | 38 | func NewDelegate() list.DefaultDelegate { 39 | delegate := list.NewDefaultDelegate() 40 | delegate.SetSpacing(0) 41 | delegate.ShowDescription = false 42 | return delegate 43 | } 44 | ``` -------------------------------------------------------------------------------- /controls/settings/palettes/lospec/init.go: -------------------------------------------------------------------------------- ```go 1 | package lospec 2 | 3 | import ( 4 | tea "github.com/charmbracelet/bubbletea" 5 | "github.com/charmbracelet/lipgloss" 6 | 7 | "github.com/charmbracelet/bubbles/textinput" 8 | ) 9 | 10 | var ( 11 | promptStyle = lipgloss.NewStyle().Padding(0, 1, 0, 1) 12 | placeholderStyle = lipgloss.NewStyle() 13 | ) 14 | 15 | // TODO: This is basically the same as we have in adaptive. Maybe generalize? 16 | func newInput(state State, value string) textinput.Model { 17 | textinput.New() 18 | input := textinput.New() 19 | input.Prompt = stateNames[state] 20 | input.PromptStyle = promptStyle 21 | input.PlaceholderStyle = placeholderStyle 22 | input.Cursor.Blink = true 23 | input.SetValue(value) 24 | return input 25 | } 26 | 27 | func (m Model) InitializeList() (Model, tea.Cmd) { 28 | m.didInitializeList = true 29 | return m.searchLospec(0) 30 | } 31 | 32 | func (m Model) DidInitializeList() bool { 33 | return m.didInitializeList 34 | } 35 | ``` -------------------------------------------------------------------------------- /display/model.go: -------------------------------------------------------------------------------- ```go 1 | package display 2 | 3 | import ( 4 | tea "github.com/charmbracelet/bubbletea" 5 | "github.com/charmbracelet/lipgloss" 6 | 7 | "github.com/Zebbeni/ansizalizer/event" 8 | "github.com/Zebbeni/ansizalizer/style" 9 | ) 10 | 11 | type Model struct { 12 | msg string 13 | width int 14 | } 15 | 16 | func New() Model { 17 | return Model{} 18 | } 19 | 20 | func (m Model) Init() tea.Cmd { 21 | return nil 22 | } 23 | 24 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 25 | switch msg := msg.(type) { 26 | case event.DisplayMsg: 27 | m.msg = string(msg) 28 | } 29 | return m, nil 30 | } 31 | 32 | func (m Model) View() string { 33 | // TODO: Switch style based on event type (warning, info, etc.) 34 | displayStyle := style.ExtraDimTitle.Copy().Width(m.width - 2) 35 | return displayStyle.Border(lipgloss.RoundedBorder()).BorderForeground(style.ExtraDimColor).Render(m.msg) 36 | } 37 | 38 | func (m Model) SetWidth(w int) Model { 39 | m.width = w 40 | return m 41 | } 42 | ``` -------------------------------------------------------------------------------- /controls/settings/view.go: -------------------------------------------------------------------------------- ```go 1 | package settings 2 | 3 | import ( 4 | "github.com/charmbracelet/lipgloss" 5 | 6 | "github.com/Zebbeni/ansizalizer/style" 7 | ) 8 | 9 | var ( 10 | activeColor = lipgloss.Color("#aaaaaa") 11 | focusColor = lipgloss.Color("#ffffff") 12 | normalColor = lipgloss.Color("#555555") 13 | ) 14 | 15 | func (m Model) renderWithBorder(content string, state State) string { 16 | renderColor := normalColor 17 | if m.active == state { 18 | renderColor = activeColor 19 | } else if m.focus == state { 20 | renderColor = focusColor 21 | } 22 | 23 | textStyle := lipgloss.NewStyle(). 24 | AlignHorizontal(lipgloss.Center). 25 | Padding(0, 1, 0, 1). 26 | Foreground(renderColor) 27 | borderStyle := lipgloss.NewStyle(). 28 | Border(lipgloss.RoundedBorder()). 29 | BorderForeground(renderColor) 30 | 31 | renderer := style.BoxWithLabel{ 32 | BoxStyle: borderStyle, 33 | LabelStyle: textStyle, 34 | } 35 | 36 | return renderer.Render(stateTitles[state], content, m.width-2) 37 | } 38 | ``` -------------------------------------------------------------------------------- /controls/settings/advanced/sampling/update.go: -------------------------------------------------------------------------------- ```go 1 | package sampling 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/key" 5 | tea "github.com/charmbracelet/bubbletea" 6 | 7 | "github.com/Zebbeni/ansizalizer/event" 8 | ) 9 | 10 | func (m Model) handleEsc() (Model, tea.Cmd) { 11 | m.ShouldClose = true 12 | m.list.SetDelegate(NewDelegate(false)) 13 | return m, nil 14 | } 15 | 16 | func (m Model) handleEnter() (Model, tea.Cmd) { 17 | m.ShouldClose = true 18 | m.list.SetDelegate(NewDelegate(false)) 19 | return m, nil 20 | } 21 | 22 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { 23 | if key.Matches(msg, event.KeyMap.Up) && m.list.Index() == 0 { 24 | m.list.SetDelegate(NewDelegate(false)) 25 | m.ShouldClose = true 26 | return m, nil 27 | } 28 | 29 | var cmd tea.Cmd 30 | m.list, cmd = m.list.Update(msg) 31 | m.list.SetDelegate(NewDelegate(true)) 32 | selectedItem := m.list.SelectedItem().(item) 33 | 34 | if selectedItem.Function == m.Function { 35 | return m, cmd 36 | } 37 | 38 | m.Function = selectedItem.Function 39 | 40 | return m, tea.Batch(cmd, event.StartRenderToViewCmd) 41 | } 42 | ``` -------------------------------------------------------------------------------- /controls/settings/size/init.go: -------------------------------------------------------------------------------- ```go 1 | package size 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/charmbracelet/bubbles/textinput" 8 | "github.com/charmbracelet/lipgloss" 9 | ) 10 | 11 | var ( 12 | promptStyle = lipgloss.NewStyle().Width(8).Padding(0, 0, 0, 1) 13 | placeholderStyle = lipgloss.NewStyle() 14 | 15 | floatPromptStyle = lipgloss.NewStyle().Padding(0, 1) 16 | floatPlaceholderStyle = lipgloss.NewStyle() 17 | ) 18 | 19 | func newInput(state State, value int) textinput.Model { 20 | textinput.New() 21 | input := textinput.New() 22 | input.Prompt = stateNames[state] 23 | input.PromptStyle = promptStyle 24 | input.PlaceholderStyle = placeholderStyle 25 | input.CharLimit = 3 26 | input.SetValue(strconv.Itoa(value)) 27 | return input 28 | } 29 | 30 | func newFloatInput(state State, value float64) textinput.Model { 31 | textinput.New() 32 | input := textinput.New() 33 | input.Prompt = stateNames[state] 34 | input.PromptStyle = floatPromptStyle 35 | input.PlaceholderStyle = floatPlaceholderStyle 36 | input.CharLimit = 5 37 | input.SetValue(fmt.Sprintf("%1.2f", value)) 38 | return input 39 | } 40 | ``` -------------------------------------------------------------------------------- /controls/settings/colors/view.go: -------------------------------------------------------------------------------- ```go 1 | package colors 2 | 3 | import ( 4 | "github.com/charmbracelet/lipgloss" 5 | 6 | "github.com/Zebbeni/ansizalizer/style" 7 | ) 8 | 9 | func (m Model) drawPaletteToggles() string { 10 | title := style.DimmedTitle.Copy().PaddingLeft(1).Render("Mode:") 11 | 12 | trueColorStyle := style.NormalButtonNode 13 | if m.IsActive && m.focus == UseTrueColor { 14 | trueColorStyle = style.FocusButtonNode 15 | } else if m.mode == UseTrueColor { 16 | trueColorStyle = style.ActiveButtonNode 17 | } 18 | trueColorNode := trueColorStyle.Render("True Color") 19 | trueColorNode = lipgloss.NewStyle().PaddingLeft(1).Render(trueColorNode) 20 | 21 | palettedStyle := style.NormalButtonNode 22 | if m.IsActive && m.focus == UsePalette { 23 | palettedStyle = style.FocusButtonNode 24 | } else if m.mode == UsePalette { 25 | palettedStyle = style.ActiveButtonNode 26 | } 27 | palettedNode := palettedStyle.Render("Palette") 28 | palettedNode = lipgloss.NewStyle().PaddingLeft(1).Render(palettedNode) 29 | 30 | return lipgloss.JoinHorizontal(lipgloss.Left, title, trueColorNode, palettedNode) 31 | } 32 | ``` -------------------------------------------------------------------------------- /palette/view.go: -------------------------------------------------------------------------------- ```go 1 | package palette 2 | 3 | import ( 4 | "image/color" 5 | "math" 6 | 7 | "github.com/charmbracelet/lipgloss" 8 | "github.com/lucasb-eyer/go-colorful" 9 | ) 10 | 11 | func Palette(palette color.Palette, w, h int) string { 12 | runes := make([]string, len(palette)/2+1) 13 | rows := make([]string, 0, h) 14 | for idx := 0; idx < len(palette); idx += 2 { 15 | var fg, bg colorful.Color 16 | var lipFg, lipBg lipgloss.Color 17 | 18 | fg, _ = colorful.MakeColor(palette[idx]) 19 | lipFg = lipgloss.Color(fg.Hex()) 20 | style := lipgloss.NewStyle().Foreground(lipFg) 21 | 22 | if idx+1 < len(palette) { 23 | bg, _ = colorful.MakeColor(palette[idx+1]) 24 | lipBg = lipgloss.Color(bg.Hex()) 25 | style = style.Copy().Background(lipBg) 26 | } 27 | runes[idx/2] = style.Render(string('▀')) 28 | } 29 | for i := 0; i < h; i++ { 30 | start := w * i 31 | if start >= len(runes) { 32 | break 33 | } 34 | stop := int(math.Min(float64(w*(i+1)), float64(len(runes)))) 35 | rows = append(rows, "") 36 | rows[i] = lipgloss.JoinHorizontal(lipgloss.Left, runes[start:stop]...) 37 | } 38 | return lipgloss.JoinVertical(lipgloss.Left, rows...) 39 | } 40 | ``` -------------------------------------------------------------------------------- /controls/export/destination/view.go: -------------------------------------------------------------------------------- ```go 1 | package destination 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/charmbracelet/lipgloss" 8 | 9 | "github.com/Zebbeni/ansizalizer/style" 10 | ) 11 | 12 | func (m Model) drawSelected() string { 13 | title := style.DimmedTitle.Copy().Render("Selected") 14 | 15 | valueStyle := style.DimmedTitle.Copy() 16 | 17 | if Input == m.focus { 18 | if m.IsActive { 19 | valueStyle = style.SelectedTitle.Copy() 20 | } else { 21 | valueStyle = style.NormalTitle.Copy() 22 | } 23 | } 24 | valueStyle.Padding(0, 0, 1, 0) 25 | 26 | path := m.Browser.SelectedDir 27 | 28 | parent := filepath.Base(filepath.Dir(path)) 29 | selected := filepath.Base(path) 30 | value := fmt.Sprintf("%s/%s", parent, selected) 31 | 32 | valueRunes := []rune(value) 33 | if len(valueRunes) > m.width { 34 | value = string(valueRunes[len(valueRunes)-m.width:]) 35 | } 36 | 37 | valueContent := valueStyle.Render(value) 38 | 39 | valueWidth := m.width 40 | widthStyle := lipgloss.NewStyle().Width(valueWidth).AlignHorizontal(lipgloss.Center) 41 | content := lipgloss.JoinVertical(lipgloss.Center, title, valueContent) 42 | 43 | return widthStyle.Render(content) 44 | } 45 | 46 | func drawBrowserTitle() string { 47 | return style.DimmedTitle.Copy().Padding(0, 2, 1, 2).Render("Select a directory") 48 | } 49 | ``` -------------------------------------------------------------------------------- /controls/export/destination/update.go: -------------------------------------------------------------------------------- ```go 1 | package destination 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/key" 5 | tea "github.com/charmbracelet/bubbletea" 6 | 7 | "github.com/Zebbeni/ansizalizer/event" 8 | ) 9 | 10 | type Direction int 11 | 12 | const ( 13 | Up Direction = iota 14 | Down 15 | ) 16 | 17 | var ( 18 | navMap = map[Direction]map[State]State{ 19 | Down: {Input: Browser}, 20 | Up: {Browser: Input}, 21 | } 22 | ) 23 | 24 | func (m Model) handleEsc() (Model, tea.Cmd) { 25 | m.ShouldClose = true 26 | m.IsActive = false 27 | return m, nil 28 | } 29 | 30 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { 31 | switch { 32 | case key.Matches(msg, event.KeyMap.Down): 33 | if next, hasNext := navMap[Down][m.focus]; hasNext { 34 | m.focus = next 35 | } 36 | case key.Matches(msg, event.KeyMap.Up): 37 | if next, hasNext := navMap[Up][m.focus]; hasNext { 38 | m.focus = next 39 | } else { 40 | m.ShouldClose = true 41 | } 42 | } 43 | return m, nil 44 | } 45 | 46 | func (m Model) handleEnter() (Model, tea.Cmd) { 47 | switch m.focus { 48 | case Input: 49 | m.focus = Browser 50 | } 51 | return m, nil 52 | } 53 | 54 | func (m Model) handleDstBrowserUpdate(msg tea.Msg) (Model, tea.Cmd) { 55 | var cmd tea.Cmd 56 | m.Browser, cmd = m.Browser.Update(msg) 57 | m.selectedDir = m.Browser.SelectedDir 58 | 59 | if m.Browser.ShouldClose { 60 | m.focus = Input 61 | m.Browser.ShouldClose = false 62 | } 63 | return m, cmd 64 | } 65 | ``` -------------------------------------------------------------------------------- /app/process/image.go: -------------------------------------------------------------------------------- ```go 1 | package process 2 | 3 | import ( 4 | "bufio" 5 | "image" 6 | "os" 7 | 8 | "github.com/lucasb-eyer/go-colorful" 9 | 10 | "github.com/Zebbeni/ansizalizer/controls/settings" 11 | "github.com/Zebbeni/ansizalizer/controls/settings/characters" 12 | ) 13 | 14 | var ( 15 | black = colorful.Color{} 16 | ) 17 | 18 | func RenderImageFile(s settings.Model, imgFilePath string) string { 19 | if imgFilePath == "" { 20 | return "Browse an image to render" 21 | } 22 | 23 | var img image.Image 24 | imgFile, err := os.Open(imgFilePath) 25 | if err != nil { 26 | return "Could not open image " + imgFilePath 27 | } 28 | defer imgFile.Close() 29 | imageReader := bufio.NewReader(imgFile) 30 | img, _, err = image.Decode(imageReader) 31 | if err != nil { 32 | return "Could not decode image " + imgFilePath 33 | } 34 | 35 | renderer := New(s) 36 | imgString := renderer.process(img) 37 | return imgString 38 | } 39 | 40 | func (m Renderer) process(input image.Image) string { 41 | isTrueColor, _, palette := m.Settings.Colors.GetSelected() 42 | if !isTrueColor && len(palette.Colors()) == 0 { 43 | return "Choose a color palette" 44 | } 45 | mode, _, _, _ := m.Settings.Characters.Selected() 46 | switch mode { 47 | case characters.Ascii: 48 | return m.processAscii(input) 49 | case characters.Unicode: 50 | return m.processUnicode(input) 51 | case characters.Custom: 52 | return m.processCustom(input) 53 | } 54 | return "Choose a character type" 55 | } 56 | ``` -------------------------------------------------------------------------------- /controls/settings/palettes/lospec/list.go: -------------------------------------------------------------------------------- ```go 1 | package lospec 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/lipgloss" 6 | 7 | "github.com/Zebbeni/ansizalizer/style" 8 | ) 9 | 10 | func CreateList(items []list.Item, w int) list.Model { 11 | newList := list.New(items, NewDelegate(), w, 22) 12 | 13 | newList.KeyMap.ForceQuit.Unbind() 14 | newList.KeyMap.Quit.Unbind() 15 | newList.SetShowHelp(false) 16 | newList.SetShowStatusBar(false) 17 | newList.SetShowTitle(false) 18 | newList.SetFilteringEnabled(false) 19 | 20 | return newList 21 | } 22 | 23 | func NewDelegate() list.DefaultDelegate { 24 | delegate := list.NewDefaultDelegate() 25 | delegate.SetSpacing(0) 26 | delegate.ShowDescription = true 27 | delegate.Styles = ItemStyles() 28 | return delegate 29 | } 30 | 31 | func ItemStyles() (s list.DefaultItemStyles) { 32 | s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2) 33 | s.NormalDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2) 34 | 35 | s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1). 36 | Border(lipgloss.NormalBorder(), false, false, false, true). 37 | BorderForeground(style.SelectedColor1) 38 | s.SelectedDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2) 39 | 40 | s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0) 41 | s.DimmedDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2) 42 | 43 | return s 44 | } 45 | ``` -------------------------------------------------------------------------------- /event/keymap.go: -------------------------------------------------------------------------------- ```go 1 | package event 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/key" 5 | ) 6 | 7 | type Map struct { 8 | Enter key.Binding 9 | Nav key.Binding 10 | Right key.Binding 11 | Left key.Binding 12 | Up key.Binding 13 | Down key.Binding 14 | Copy key.Binding 15 | Save key.Binding 16 | Esc key.Binding 17 | } 18 | 19 | var KeyMap Map 20 | 21 | func InitKeyMap() { 22 | KeyMap = Map{ 23 | Enter: key.NewBinding( 24 | key.WithKeys("return", "enter"), 25 | key.WithHelp("↲/enter", "select/focus menu"), 26 | ), 27 | Nav: key.NewBinding( 28 | key.WithKeys("up", "down", "right", "left"), 29 | key.WithHelp("↕/↔", "navigate"), 30 | ), 31 | Right: key.NewBinding( 32 | key.WithKeys("right"), 33 | ), 34 | Left: key.NewBinding( 35 | key.WithKeys("left"), 36 | ), 37 | Up: key.NewBinding( 38 | key.WithKeys("up"), 39 | ), 40 | Down: key.NewBinding( 41 | key.WithKeys("down"), 42 | ), 43 | Copy: key.NewBinding( 44 | key.WithKeys("ctrl+c"), 45 | key.WithHelp("ctrl+c", "copy to clipboard")), 46 | Save: key.NewBinding( 47 | key.WithKeys("ctrl+s"), 48 | key.WithHelp("ctrl+s", "save to file")), 49 | Esc: key.NewBinding( 50 | key.WithKeys("esc"), 51 | key.WithHelp("esc", "back/exit menu"), 52 | ), 53 | } 54 | } 55 | 56 | func (k Map) ShortHelp() []key.Binding { 57 | return []key.Binding{k.Nav, k.Enter, k.Esc, k.Copy, k.Save} 58 | } 59 | 60 | func (k Map) FullHelp() [][]key.Binding { 61 | return [][]key.Binding{{k.Nav, k.Enter, k.Esc, k.Copy, k.Save}} 62 | } 63 | ``` -------------------------------------------------------------------------------- /controls/settings/advanced/sampling/model.go: -------------------------------------------------------------------------------- ```go 1 | package sampling 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/key" 5 | "github.com/charmbracelet/bubbles/list" 6 | tea "github.com/charmbracelet/bubbletea" 7 | "github.com/charmbracelet/lipgloss" 8 | "github.com/nfnt/resize" 9 | 10 | "github.com/Zebbeni/ansizalizer/event" 11 | "github.com/Zebbeni/ansizalizer/style" 12 | ) 13 | 14 | type Model struct { 15 | Function resize.InterpolationFunction 16 | 17 | list list.Model 18 | 19 | IsActive bool 20 | ShouldClose bool 21 | } 22 | 23 | func New(w int) Model { 24 | items := menuItems() 25 | selected := items[0].(item) 26 | menu := newMenu(items, w, len(items)) 27 | 28 | return Model{ 29 | Function: selected.Function, 30 | list: menu, 31 | IsActive: false, 32 | ShouldClose: false, 33 | } 34 | } 35 | 36 | func (m Model) Init() tea.Cmd { 37 | return nil 38 | } 39 | 40 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 41 | switch msg := msg.(type) { 42 | case tea.KeyMsg: 43 | switch { 44 | case key.Matches(msg, event.KeyMap.Esc): 45 | return m.handleEsc() 46 | case key.Matches(msg, event.KeyMap.Enter): 47 | return m.handleEnter() 48 | case key.Matches(msg, event.KeyMap.Nav): 49 | return m.handleNav(msg) 50 | } 51 | } 52 | return m, nil 53 | } 54 | 55 | func (m Model) View() string { 56 | prompt := style.DimmedTitle.Copy().Render("Select Method") 57 | menu := m.list.View() 58 | content := lipgloss.JoinVertical(lipgloss.Left, prompt, menu) 59 | return lipgloss.NewStyle().Padding(0, 1).Render(content) 60 | } 61 | ``` -------------------------------------------------------------------------------- /app/resize.go: -------------------------------------------------------------------------------- ```go 1 | package app 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "golang.org/x/term" 8 | 9 | tea "github.com/charmbracelet/bubbletea" 10 | ) 11 | 12 | // There is (currently) no support on Windows for detecting resize events, so 13 | // we instead poll at regular intervals to check if the terminal size changed. 14 | // If a resize is detected in this way, we send a WindowSizeMsg with the new 15 | // dimensions to bubbletea, and handle it in the Model event handler 16 | type checkSizeMsg int 17 | 18 | const ( 19 | resizeCheckDuration = time.Second / 4 20 | ) 21 | 22 | func (m Model) handleSizeMsg(msg tea.WindowSizeMsg) (Model, tea.Cmd) { 23 | w, h := msg.Width, msg.Height 24 | m.w, m.h = w, h 25 | m.display = m.display.SetWidth(m.rPanelWidth()) 26 | 27 | tea.ClearScreen() 28 | return m, nil 29 | } 30 | 31 | func (m Model) handleCheckSizeMsg() (Model, tea.Cmd) { 32 | w, h, _ := term.GetSize(int(os.Stdout.Fd())) 33 | if w == m.w && h == m.h { 34 | return m, pollForSizeChange 35 | } 36 | updateSizeCmd := func() tea.Msg { 37 | return tea.WindowSizeMsg{Width: w, Height: h} 38 | } 39 | return m, tea.Batch(pollForSizeChange, updateSizeCmd) 40 | } 41 | 42 | func pollForSizeChange() tea.Msg { 43 | time.Sleep(resizeCheckDuration) 44 | return checkSizeMsg(1) 45 | } 46 | 47 | func (m Model) leftPanelHeight() int { 48 | return m.h - helpHeight 49 | } 50 | 51 | func (m Model) rPanelWidth() int { 52 | return m.w - controlsWidth 53 | } 54 | 55 | func (m Model) rPanelHeight() int { 56 | return m.h - helpHeight 57 | } 58 | ``` -------------------------------------------------------------------------------- /controls/export/view.go: -------------------------------------------------------------------------------- ```go 1 | package export 2 | 3 | import ( 4 | "github.com/charmbracelet/lipgloss" 5 | 6 | "github.com/Zebbeni/ansizalizer/style" 7 | ) 8 | 9 | var ( 10 | activeColor = lipgloss.Color("#aaaaaa") 11 | focusColor = lipgloss.Color("#ffffff") 12 | normalColor = lipgloss.Color("#555555") 13 | ) 14 | 15 | func (m Model) renderWithBorder(content string, state State) string { 16 | renderColor := normalColor 17 | if m.active == state { 18 | renderColor = activeColor 19 | } else if m.focus == state { 20 | renderColor = focusColor 21 | } 22 | 23 | textStyle := lipgloss.NewStyle(). 24 | AlignHorizontal(lipgloss.Center). 25 | Padding(0, 1, 0, 1). 26 | Foreground(renderColor) 27 | borderStyle := lipgloss.NewStyle(). 28 | Border(lipgloss.RoundedBorder()). 29 | BorderForeground(renderColor) 30 | 31 | renderer := style.BoxWithLabel{ 32 | BoxStyle: borderStyle, 33 | LabelStyle: textStyle, 34 | } 35 | 36 | return renderer.Render(stateTitles[state], content, m.width-2) 37 | } 38 | 39 | func (m Model) drawProcessButton() string { 40 | buttonStyle := style.NormalButton 41 | if m.focus == Process { 42 | buttonStyle = style.FocusButton 43 | } 44 | 45 | centerStyle := lipgloss.NewStyle().AlignHorizontal(lipgloss.Center) 46 | 47 | internalStyle := centerStyle.Copy().Width(m.width - 2) 48 | title := internalStyle.Render(stateTitles[Process]) 49 | button := buttonStyle.Render(title) 50 | 51 | return centerStyle.Copy().Width(m.width).AlignHorizontal(lipgloss.Center).Render(button) 52 | } 53 | ``` -------------------------------------------------------------------------------- /controls/settings/palettes/loader/view.go: -------------------------------------------------------------------------------- ```go 1 | package loader 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/lipgloss" 6 | 7 | "github.com/Zebbeni/ansizalizer/style" 8 | ) 9 | 10 | const ( 11 | maxWidth = 30 12 | maxNormalHeight = 1 13 | maxSelectedHeight = 2 14 | ) 15 | 16 | // NewItemStyles returns style definitions for a default item. 17 | // DefaultItemView for when these come into play. 18 | func NewItemStyles() (s list.DefaultItemStyles) { 19 | 20 | s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2) 21 | s.NormalDesc = style.DimmedParagraph.Copy().MaxHeight(maxNormalHeight).Padding(0, 0, 0, 2) 22 | 23 | s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1). 24 | Border(lipgloss.NormalBorder(), false, false, false, true). 25 | BorderForeground(style.SelectedColor1) 26 | s.SelectedDesc = style.SelectedTitle.Copy().MaxHeight(maxSelectedHeight).Padding(0, 0, 0, 1). 27 | Border(lipgloss.NormalBorder(), false, false, false, true). 28 | BorderForeground(style.SelectedColor1) 29 | 30 | s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0) 31 | s.DimmedDesc = style.DimmedParagraph.Copy().MaxHeight(maxNormalHeight).Padding(0, 0, 0, 2) 32 | 33 | return s 34 | } 35 | 36 | func (m Model) drawTitle() string { 37 | title := style.DimmedTitle.Copy().Italic(true).Render("Load from .hex file") 38 | return lipgloss.NewStyle().Width(m.width).PaddingBottom(1).AlignHorizontal(lipgloss.Center).Render(title) 39 | } 40 | ``` -------------------------------------------------------------------------------- /controls/menu/model.go: -------------------------------------------------------------------------------- ```go 1 | package menu 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/lipgloss" 6 | 7 | "github.com/Zebbeni/ansizalizer/style" 8 | ) 9 | 10 | func New(items []list.Item, w int) list.Model { 11 | newList := list.New(items, NewDelegate(), w, 18) 12 | 13 | newList.KeyMap.ForceQuit.Unbind() 14 | newList.KeyMap.Quit.Unbind() 15 | newList.SetShowHelp(false) 16 | newList.SetShowStatusBar(false) 17 | newList.SetShowTitle(false) 18 | newList.SetFilteringEnabled(false) 19 | 20 | return newList 21 | } 22 | 23 | func NewDelegate() list.DefaultDelegate { 24 | delegate := list.NewDefaultDelegate() 25 | delegate.SetSpacing(0) 26 | delegate.ShowDescription = false 27 | delegate.Styles = ItemStyles() 28 | return delegate 29 | } 30 | 31 | func ItemStyles() (s list.DefaultItemStyles) { 32 | s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2) 33 | s.NormalDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2) 34 | 35 | s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1). 36 | Border(lipgloss.NormalBorder(), false, false, false, true). 37 | BorderForeground(style.SelectedColor1) 38 | s.NormalDesc = style.SelectedTitle.Copy().MaxHeight(1).Padding(0, 0, 0, 2). 39 | Border(lipgloss.NormalBorder(), false, false, false, true). 40 | BorderForeground(style.SelectedColor1) 41 | 42 | s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0) 43 | s.DimmedDesc = style.DimmedParagraph.Copy().MaxHeight(1).Padding(0, 0, 0, 2) 44 | 45 | return s 46 | } 47 | ``` -------------------------------------------------------------------------------- /controls/settings/palettes/view.go: -------------------------------------------------------------------------------- ```go 1 | package palettes 2 | 3 | import "github.com/charmbracelet/lipgloss" 4 | 5 | var ( 6 | stateOrder = []State{Load, Adapt, Lospec} 7 | stateNames = map[State]string{ 8 | Load: "Load", 9 | Adapt: "Sample", 10 | Lospec: "Lospec", 11 | } 12 | 13 | activeStyle = lipgloss.NewStyle(). 14 | BorderStyle(lipgloss.RoundedBorder()). 15 | BorderForeground(lipgloss.Color("#aaaaaa")). 16 | Foreground(lipgloss.Color("#aaaaaa")) 17 | focusStyle = lipgloss.NewStyle(). 18 | BorderStyle(lipgloss.RoundedBorder()). 19 | BorderForeground(lipgloss.Color("#ffffff")). 20 | Foreground(lipgloss.Color("#ffffff")) 21 | normalStyle = lipgloss.NewStyle(). 22 | BorderStyle(lipgloss.RoundedBorder()). 23 | BorderForeground(lipgloss.Color("#555555")). 24 | Foreground(lipgloss.Color("#555555")) 25 | titleStyle = lipgloss.NewStyle(). 26 | Foreground(lipgloss.Color("#888888")) 27 | ) 28 | 29 | func (m Model) drawTitle() string { 30 | return titleStyle.Copy().Italic(true).Width(m.width).Align(lipgloss.Center).Render("Colors") 31 | } 32 | 33 | func (m Model) drawButtons() string { 34 | buttons := make([]string, len(stateOrder)) 35 | for i, state := range stateOrder { 36 | style := normalStyle 37 | if m.IsActive && state == m.focus { 38 | style = focusStyle 39 | } else if state == m.selected { 40 | style = activeStyle 41 | } 42 | buttons[i] = style.Copy().AlignHorizontal(lipgloss.Center).Padding(0, 1).Render(stateNames[state]) 43 | } 44 | return lipgloss.JoinHorizontal(lipgloss.Left, buttons...) 45 | } 46 | ``` -------------------------------------------------------------------------------- /controls/browser/item.go: -------------------------------------------------------------------------------- ```go 1 | package browser 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/charmbracelet/bubbles/list" 9 | ) 10 | 11 | type item struct { 12 | name string 13 | path string 14 | isDir bool 15 | isTop bool 16 | } 17 | 18 | func (i item) FilterValue() string { 19 | return i.name 20 | } 21 | 22 | func (i item) Title() string { 23 | if i.isTop { 24 | return "↑" 25 | } 26 | if i.isDir { 27 | return fmt.Sprintf("%s/", i.name) 28 | } 29 | return i.name 30 | } 31 | 32 | func (i item) Description() string { 33 | if i.isDir { 34 | return "directory" 35 | } 36 | return "file" 37 | } 38 | 39 | func getItems(extensions map[string]bool, dir string) []list.Item { 40 | entries, err := os.ReadDir(dir) 41 | if err != nil { 42 | fmt.Println("Error reading directory entries:", err) 43 | os.Exit(1) 44 | } 45 | 46 | parentPath := filepath.Dir(dir) 47 | parentName := filepath.Base(parentPath) 48 | parentItem := item{name: parentName, path: parentPath, isDir: true, isTop: true} 49 | 50 | dirItems := []list.Item{parentItem} 51 | fileItems := make([]list.Item, 0) 52 | 53 | for _, e := range entries { 54 | path := fmt.Sprintf("%s/%s", dir, e.Name()) 55 | 56 | if e.IsDir() { 57 | name := e.Name() 58 | dirItem := item{name: name, path: path, isDir: true, isTop: false} 59 | dirItems = append(dirItems, dirItem) 60 | continue 61 | } 62 | 63 | ext := filepath.Ext(e.Name()) 64 | if _, ok := extensions[ext]; ok { 65 | fileItem := item{name: e.Name(), path: path, isDir: false, isTop: false} 66 | fileItems = append(fileItems, fileItem) 67 | } 68 | } 69 | 70 | return append(dirItems, fileItems...) 71 | } 72 | ``` -------------------------------------------------------------------------------- /controls/export/model.go: -------------------------------------------------------------------------------- ```go 1 | package export 2 | 3 | import ( 4 | tea "github.com/charmbracelet/bubbletea" 5 | "github.com/charmbracelet/lipgloss" 6 | 7 | "github.com/Zebbeni/ansizalizer/controls/export/destination" 8 | "github.com/Zebbeni/ansizalizer/controls/export/source" 9 | ) 10 | 11 | type State int 12 | 13 | const ( 14 | None State = iota 15 | Source 16 | Destination 17 | Process 18 | ) 19 | 20 | var ( 21 | stateTitles = map[State]string{ 22 | Source: "Source", 23 | Destination: "Destination", 24 | Process: "Process", 25 | } 26 | ) 27 | 28 | type Model struct { 29 | active State 30 | focus State 31 | 32 | Source source.Model 33 | Destination destination.Model 34 | 35 | ShouldClose bool 36 | ShouldUnfocus bool 37 | 38 | width int 39 | } 40 | 41 | func New(w int) Model { 42 | return Model{ 43 | focus: Source, 44 | active: None, 45 | Source: source.New(w - 2), 46 | Destination: destination.New(w - 2), 47 | ShouldClose: false, 48 | ShouldUnfocus: false, 49 | width: w, 50 | } 51 | } 52 | 53 | func (m Model) Init() tea.Cmd { 54 | return nil 55 | } 56 | 57 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 58 | switch m.active { 59 | case Source: 60 | return m.handleSourceUpdate(msg) 61 | case Destination: 62 | return m.handleDestinationUpdate(msg) 63 | } 64 | 65 | keyMsg, ok := msg.(tea.KeyMsg) 66 | if !ok { 67 | return m, nil 68 | } 69 | 70 | return m.handleKeyMsg(keyMsg) 71 | } 72 | 73 | func (m Model) View() string { 74 | src := m.renderWithBorder(m.Source.View(), Source) 75 | dst := m.renderWithBorder(m.Destination.View(), Destination) 76 | process := m.drawProcessButton() 77 | return lipgloss.JoinVertical(lipgloss.Left, src, dst, process) 78 | } 79 | ``` -------------------------------------------------------------------------------- /controls/settings/advanced/dithering/model.go: -------------------------------------------------------------------------------- ```go 1 | package dithering 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | tea "github.com/charmbracelet/bubbletea" 6 | "github.com/charmbracelet/lipgloss" 7 | "github.com/makeworld-the-better-one/dither/v2" 8 | ) 9 | 10 | type State int 11 | 12 | const ( 13 | DitherOn State = iota 14 | DitherOff 15 | SerpentineOn 16 | SerpentineOff 17 | Matrix 18 | ) 19 | 20 | type Model struct { 21 | focus State 22 | 23 | doDithering bool 24 | doSerpentine bool 25 | matrix dither.ErrorDiffusionMatrix 26 | 27 | list list.Model 28 | 29 | IsActive bool 30 | ShouldClose bool 31 | 32 | width int 33 | } 34 | 35 | func New(w int) Model { 36 | return Model{ 37 | focus: DitherOff, 38 | doDithering: false, 39 | doSerpentine: false, 40 | matrix: dither.FloydSteinberg, 41 | list: newMatrixMenu(w), 42 | ShouldClose: false, 43 | IsActive: false, 44 | width: w, 45 | } 46 | } 47 | 48 | func (m Model) Init() tea.Cmd { 49 | return nil 50 | } 51 | 52 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 53 | if m.focus == Matrix { 54 | return m.handleMatrixListUpdate(msg) 55 | } 56 | 57 | if keyMsg, ok := msg.(tea.KeyMsg); ok { 58 | return m.handleKeyMsg(keyMsg) 59 | } 60 | return m, nil 61 | } 62 | 63 | func (m Model) View() string { 64 | ditheringOpts := m.drawDitheringOptions() 65 | serpentineOpts := m.drawSerpentineOptions() 66 | matrixList := m.drawMatrix() 67 | content := lipgloss.JoinVertical(lipgloss.Left, ditheringOpts, serpentineOpts, matrixList) 68 | return lipgloss.NewStyle().Padding(0, 1).Render(content) 69 | } 70 | 71 | func (m Model) Settings() (bool, bool, dither.ErrorDiffusionMatrix) { 72 | return m.doDithering, m.doSerpentine, m.matrix 73 | } 74 | ``` -------------------------------------------------------------------------------- /controls/browser/model.go: -------------------------------------------------------------------------------- ```go 1 | package browser 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/charmbracelet/bubbles/key" 9 | "github.com/charmbracelet/bubbles/list" 10 | tea "github.com/charmbracelet/bubbletea" 11 | "github.com/charmbracelet/lipgloss" 12 | 13 | "github.com/Zebbeni/ansizalizer/event" 14 | ) 15 | 16 | type Model struct { 17 | SelectedDir string 18 | SelectedFile string 19 | ActiveDir string 20 | ActiveFile string 21 | 22 | lists []list.Model 23 | fileExtensions map[string]bool 24 | 25 | ShouldClose bool 26 | 27 | width int 28 | } 29 | 30 | func New(exts map[string]bool, w int) Model { 31 | dir, err := os.Getwd() 32 | if err != nil { 33 | fmt.Println("Error getting starting directory:", err) 34 | os.Exit(1) 35 | } 36 | 37 | m := Model{ 38 | width: w, 39 | fileExtensions: exts, 40 | } 41 | m = m.addListForDirectory(dir) 42 | 43 | return m 44 | } 45 | 46 | func (m Model) Init() tea.Cmd { 47 | return nil 48 | } 49 | 50 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 51 | switch msg := msg.(type) { 52 | case tea.KeyMsg: 53 | switch { 54 | case key.Matches(msg, event.KeyMap.Esc): 55 | return m.handleEsc() 56 | case key.Matches(msg, event.KeyMap.Nav): 57 | return m.handleNav(msg) 58 | case key.Matches(msg, event.KeyMap.Enter): 59 | return m.handleEnter() 60 | } 61 | } 62 | return m, nil 63 | } 64 | 65 | func (m Model) currentList() list.Model { 66 | return m.lists[m.listIndex()] 67 | } 68 | 69 | func (m Model) listIndex() int { 70 | return len(m.lists) - 1 71 | } 72 | 73 | func (m Model) View() string { 74 | browser := m.currentList().View() 75 | return lipgloss.JoinVertical(lipgloss.Left, browser) 76 | } 77 | 78 | func (m Model) ActiveFilename() string { 79 | return filepath.Base(m.ActiveFile) 80 | } 81 | ``` -------------------------------------------------------------------------------- /controls/export/destination/model.go: -------------------------------------------------------------------------------- ```go 1 | package destination 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/charmbracelet/bubbles/key" 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/charmbracelet/lipgloss" 9 | 10 | "github.com/Zebbeni/ansizalizer/controls/browser" 11 | "github.com/Zebbeni/ansizalizer/event" 12 | ) 13 | 14 | type State int 15 | 16 | const ( 17 | Input State = iota 18 | Browser 19 | ) 20 | 21 | type Model struct { 22 | focus State 23 | 24 | Browser browser.Model 25 | 26 | selectedDir string 27 | 28 | ShouldClose bool 29 | ShouldUnfocus bool 30 | 31 | IsActive bool 32 | 33 | width int 34 | } 35 | 36 | func New(w int) Model { 37 | filepath, _ := os.Getwd() 38 | 39 | return Model{ 40 | focus: Input, 41 | 42 | Browser: browser.New(nil, w-2), 43 | 44 | selectedDir: filepath, 45 | 46 | width: w, 47 | ShouldClose: false, 48 | } 49 | } 50 | 51 | func (m Model) Init() tea.Cmd { 52 | return nil 53 | } 54 | 55 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 56 | var cmd tea.Cmd 57 | switch m.focus { 58 | case Browser: 59 | return m.handleDstBrowserUpdate(msg) 60 | } 61 | 62 | switch msg := msg.(type) { 63 | case tea.KeyMsg: 64 | switch { 65 | case key.Matches(msg, event.KeyMap.Esc): 66 | return m.handleEsc() 67 | case key.Matches(msg, event.KeyMap.Nav): 68 | return m.handleNav(msg) 69 | case key.Matches(msg, event.KeyMap.Enter): 70 | return m.handleEnter() 71 | } 72 | } 73 | return m, cmd 74 | } 75 | 76 | func (m Model) View() string { 77 | content := make([]string, 0, 5) 78 | 79 | selected := lipgloss.NewStyle().PaddingTop(1).Render(m.drawSelected()) 80 | content = append(content, selected) 81 | 82 | if m.focus == Browser { 83 | content = append(content, m.Browser.View()) 84 | } 85 | 86 | return lipgloss.JoinVertical(lipgloss.Left, content...) 87 | } 88 | 89 | func (m Model) GetSelected() string { 90 | return m.selectedDir 91 | } 92 | ``` -------------------------------------------------------------------------------- /controls/settings/model.go: -------------------------------------------------------------------------------- ```go 1 | package settings 2 | 3 | import ( 4 | tea "github.com/charmbracelet/bubbletea" 5 | "github.com/charmbracelet/lipgloss" 6 | 7 | "github.com/Zebbeni/ansizalizer/controls/settings/advanced" 8 | "github.com/Zebbeni/ansizalizer/controls/settings/characters" 9 | "github.com/Zebbeni/ansizalizer/controls/settings/colors" 10 | "github.com/Zebbeni/ansizalizer/controls/settings/size" 11 | ) 12 | 13 | type Model struct { 14 | active State 15 | focus State 16 | 17 | Colors colors.Model 18 | Characters characters.Model 19 | Size size.Model 20 | Advanced advanced.Model 21 | 22 | ShouldUnfocus bool 23 | ShouldClose bool 24 | 25 | width int 26 | } 27 | 28 | func New(w int) Model { 29 | return Model{ 30 | active: None, 31 | focus: Colors, 32 | 33 | Colors: colors.New(w), 34 | Characters: characters.New(w - 2), 35 | Size: size.New(), 36 | Advanced: advanced.New(w - 2), 37 | 38 | ShouldUnfocus: false, 39 | ShouldClose: false, 40 | 41 | width: w, 42 | } 43 | } 44 | 45 | func (m Model) Init() tea.Cmd { 46 | return nil 47 | } 48 | 49 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 50 | switch m.active { 51 | case Colors: 52 | return m.handleColorsUpdate(msg) 53 | case Characters: 54 | return m.handleCharactersUpdate(msg) 55 | case Size: 56 | return m.handleSizeUpdate(msg) 57 | case Advanced: 58 | return m.handleAdvancedUpdate(msg) 59 | } 60 | 61 | keyMsg, ok := msg.(tea.KeyMsg) 62 | if !ok { 63 | return m, nil 64 | } 65 | 66 | return m.handleKeyMsg(keyMsg) 67 | } 68 | 69 | func (m Model) View() string { 70 | colorCtrls := m.Colors.View() 71 | charCtrls := m.Characters.View() 72 | sizeCtrls := m.Size.View() 73 | sampCtrls := m.Advanced.View() 74 | 75 | col := m.renderWithBorder(colorCtrls, Colors) 76 | char := m.renderWithBorder(charCtrls, Characters) 77 | siz := m.renderWithBorder(sizeCtrls, Size) 78 | sam := m.renderWithBorder(sampCtrls, Advanced) 79 | 80 | return lipgloss.JoinVertical(lipgloss.Top, col, char, siz, sam) 81 | } 82 | ``` -------------------------------------------------------------------------------- /controls/settings/advanced/model.go: -------------------------------------------------------------------------------- ```go 1 | package advanced 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/key" 5 | tea "github.com/charmbracelet/bubbletea" 6 | "github.com/makeworld-the-better-one/dither/v2" 7 | "github.com/nfnt/resize" 8 | 9 | "github.com/Zebbeni/ansizalizer/controls/settings/advanced/dithering" 10 | "github.com/Zebbeni/ansizalizer/controls/settings/advanced/sampling" 11 | "github.com/Zebbeni/ansizalizer/event" 12 | ) 13 | 14 | type State int 15 | 16 | const ( 17 | Menu State = iota 18 | Sampling 19 | Dithering 20 | SamplingControls 21 | DitheringControls 22 | ) 23 | 24 | type Model struct { 25 | focus State 26 | active State 27 | activeTab State 28 | sampling sampling.Model 29 | dithering dithering.Model 30 | ShouldClose bool 31 | IsActive bool 32 | width int 33 | } 34 | 35 | func New(w int) Model { 36 | return Model{ 37 | focus: Sampling, 38 | active: Menu, 39 | activeTab: Sampling, 40 | sampling: sampling.New(w - 2), 41 | dithering: dithering.New(w - 2), 42 | ShouldClose: false, 43 | IsActive: false, 44 | width: w, 45 | } 46 | } 47 | 48 | func (m Model) Init() tea.Cmd { 49 | return nil 50 | } 51 | 52 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 53 | switch m.active { 54 | case SamplingControls: 55 | return m.handleSamplingUpdate(msg) 56 | case DitheringControls: 57 | return m.handleDitheringUpdate(msg) 58 | } 59 | 60 | switch msg := msg.(type) { 61 | case tea.KeyMsg: 62 | switch { 63 | case key.Matches(msg, event.KeyMap.Enter): 64 | return m.handleEnter() 65 | case key.Matches(msg, event.KeyMap.Nav): 66 | return m.handleNav(msg) 67 | case key.Matches(msg, event.KeyMap.Esc): 68 | return m.handleEsc() 69 | } 70 | } 71 | return m, nil 72 | } 73 | 74 | func (m Model) View() string { 75 | return m.drawTabs() 76 | } 77 | 78 | func (m Model) SamplingFunction() resize.InterpolationFunction { 79 | return m.sampling.Function 80 | } 81 | 82 | func (m Model) Dithering() (bool, bool, dither.ErrorDiffusionMatrix) { 83 | return m.dithering.Settings() 84 | } 85 | ``` -------------------------------------------------------------------------------- /controls/settings/advanced/dithering/view.go: -------------------------------------------------------------------------------- ```go 1 | package dithering 2 | 3 | import ( 4 | "github.com/charmbracelet/lipgloss" 5 | 6 | "github.com/Zebbeni/ansizalizer/style" 7 | ) 8 | 9 | func (m Model) drawDitheringOptions() string { 10 | prompt := style.DimmedTitle.Render("Use Dithering:") 11 | prompt = lipgloss.NewStyle().Width(15).Render(prompt) 12 | 13 | nodeStyle := style.NormalButtonNode 14 | if m.IsActive && m.focus == DitherOn { 15 | nodeStyle = style.FocusButtonNode 16 | } else if m.doDithering { 17 | nodeStyle = style.ActiveButtonNode 18 | } 19 | onNode := lipgloss.NewStyle().Width(4).Render(nodeStyle.Copy().Render("On")) 20 | 21 | nodeStyle = style.NormalButtonNode 22 | if m.IsActive && m.focus == DitherOff { 23 | nodeStyle = style.FocusButtonNode 24 | } else if !m.doDithering { 25 | nodeStyle = style.ActiveButtonNode 26 | } 27 | offNode := nodeStyle.Copy().Render("Off") 28 | 29 | return lipgloss.JoinHorizontal(lipgloss.Left, prompt, onNode, offNode) 30 | } 31 | 32 | func (m Model) drawSerpentineOptions() string { 33 | prompt := style.DimmedTitle.Render("Do Serpentine:") 34 | prompt = lipgloss.NewStyle().Width(15).Render(prompt) 35 | 36 | nodeStyle := style.NormalButtonNode 37 | if m.IsActive && m.focus == SerpentineOn { 38 | nodeStyle = style.FocusButtonNode 39 | } else if m.doSerpentine { 40 | nodeStyle = style.ActiveButtonNode 41 | } 42 | onNode := lipgloss.NewStyle().Width(4).Render(nodeStyle.Copy().Render("On")) 43 | 44 | nodeStyle = style.NormalButtonNode 45 | if m.IsActive && m.focus == SerpentineOff { 46 | nodeStyle = style.FocusButtonNode 47 | } else if !m.doSerpentine { 48 | nodeStyle = style.ActiveButtonNode 49 | } 50 | offNode := nodeStyle.Copy().Render("Off") 51 | 52 | return lipgloss.JoinHorizontal(lipgloss.Left, prompt, onNode, offNode) 53 | } 54 | 55 | func (m Model) drawMatrix() string { 56 | prompt := style.DimmedTitle.Copy().PaddingTop(1).Render("Select Matrix") 57 | return lipgloss.JoinVertical(lipgloss.Left, prompt, m.list.View()) 58 | } 59 | ``` -------------------------------------------------------------------------------- /controls/update.go: -------------------------------------------------------------------------------- ```go 1 | package controls 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/charmbracelet/bubbles/key" 7 | tea "github.com/charmbracelet/bubbletea" 8 | 9 | "github.com/Zebbeni/ansizalizer/event" 10 | ) 11 | 12 | type Direction int 13 | 14 | const ( 15 | Left Direction = iota 16 | Right 17 | Up 18 | Down 19 | ) 20 | 21 | var navMap = map[Direction]map[State]State{ 22 | Right: {Browse: Settings, Settings: Export}, 23 | Left: {Export: Settings, Settings: Browse}, 24 | } 25 | 26 | func (m Model) handleOpenUpdate(msg tea.Msg) (Model, tea.Cmd) { 27 | var cmd tea.Cmd 28 | m.FileBrowser, cmd = m.FileBrowser.Update(msg) 29 | 30 | if m.FileBrowser.ShouldClose { 31 | m.FileBrowser.ShouldClose = false 32 | m.active = Menu 33 | } 34 | return m, cmd 35 | } 36 | 37 | func (m Model) handleSettingsUpdate(msg tea.Msg) (Model, tea.Cmd) { 38 | var cmd tea.Cmd 39 | m.Settings, cmd = m.Settings.Update(msg) 40 | 41 | if m.Settings.ShouldClose { 42 | m.Settings.ShouldClose = false 43 | m.active = Menu 44 | } 45 | 46 | return m, cmd 47 | } 48 | 49 | func (m Model) handleExportUpdate(msg tea.Msg) (Model, tea.Cmd) { 50 | var cmd tea.Cmd 51 | m.Export, cmd = m.Export.Update(msg) 52 | 53 | if m.Export.ShouldClose { 54 | m.Export.ShouldClose = false 55 | m.active = Menu 56 | } 57 | 58 | return m, cmd 59 | } 60 | 61 | func (m Model) handleMenuUpdate(msg tea.Msg) (Model, tea.Cmd) { 62 | m.active = Menu 63 | switch msg := msg.(type) { 64 | case tea.KeyMsg: 65 | switch { 66 | case key.Matches(msg, event.KeyMap.Enter): 67 | m.active = m.focus 68 | 69 | case key.Matches(msg, event.KeyMap.Nav): 70 | switch { 71 | case key.Matches(msg, event.KeyMap.Right): 72 | if next, hasNext := navMap[Right][m.focus]; hasNext { 73 | m.focus = next 74 | } 75 | case key.Matches(msg, event.KeyMap.Left): 76 | if next, hasNext := navMap[Left][m.focus]; hasNext { 77 | m.focus = next 78 | } 79 | } 80 | 81 | case key.Matches(msg, event.KeyMap.Esc): 82 | // Quit program if top-level menu is active and escape pressed 83 | tea.Quit() 84 | os.Exit(0) 85 | } 86 | } 87 | return m, nil 88 | } 89 | ``` -------------------------------------------------------------------------------- /controls/model.go: -------------------------------------------------------------------------------- ```go 1 | package controls 2 | 3 | import ( 4 | tea "github.com/charmbracelet/bubbletea" 5 | "github.com/charmbracelet/lipgloss" 6 | 7 | "github.com/Zebbeni/ansizalizer/controls/browser" 8 | "github.com/Zebbeni/ansizalizer/controls/export" 9 | "github.com/Zebbeni/ansizalizer/controls/settings" 10 | "github.com/Zebbeni/ansizalizer/global" 11 | ) 12 | 13 | type State int 14 | 15 | const ( 16 | Menu State = iota 17 | Browse 18 | Settings 19 | Export 20 | 21 | numButtons = 3 22 | ) 23 | 24 | var ( 25 | stateOrder = []State{Browse, Settings, Export} 26 | stateNames = map[State]string{ 27 | Browse: "Browse", 28 | Settings: "Settings", 29 | Export: "Export", 30 | } 31 | ) 32 | 33 | type Model struct { 34 | active State 35 | focus State 36 | 37 | FileBrowser browser.Model 38 | Settings settings.Model 39 | Export export.Model 40 | 41 | width int 42 | } 43 | 44 | func New(w int) Model { 45 | return Model{ 46 | active: Menu, 47 | focus: Browse, 48 | 49 | FileBrowser: browser.New(global.ImgExtensions, w), 50 | Settings: settings.New(w), 51 | Export: export.New(w), 52 | 53 | width: w, 54 | } 55 | } 56 | 57 | func (m Model) Init() tea.Cmd { 58 | return nil 59 | } 60 | 61 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 62 | switch m.active { 63 | case Browse: 64 | return m.handleOpenUpdate(msg) 65 | case Settings: 66 | return m.handleSettingsUpdate(msg) 67 | case Export: 68 | return m.handleExportUpdate(msg) 69 | } 70 | return m.handleMenuUpdate(msg) 71 | } 72 | 73 | // View displays a row of 3 buttons above 1 of 3 control panels: 74 | // Browse | Settings | Export 75 | func (m Model) View() string { 76 | title := m.drawTitle() 77 | 78 | // draw the top three buttons 79 | buttons := m.drawButtons() 80 | var controls string 81 | 82 | switch m.active { 83 | case Browse: 84 | browserTitle := m.drawBrowserTitle() 85 | controls = lipgloss.JoinVertical(lipgloss.Left, browserTitle, m.FileBrowser.View()) 86 | case Settings: 87 | controls = m.Settings.View() 88 | case Export: 89 | controls = m.Export.View() 90 | } 91 | 92 | return lipgloss.JoinVertical(lipgloss.Top, title, buttons, controls) 93 | } 94 | ``` -------------------------------------------------------------------------------- /controls/settings/advanced/sampling/item.go: -------------------------------------------------------------------------------- ```go 1 | package sampling 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/lipgloss" 6 | "github.com/nfnt/resize" 7 | 8 | "github.com/Zebbeni/ansizalizer/style" 9 | ) 10 | 11 | type item struct { 12 | name string 13 | Function resize.InterpolationFunction 14 | } 15 | 16 | func (i item) FilterValue() string { 17 | return i.name 18 | } 19 | 20 | func (i item) Title() string { 21 | return i.name 22 | } 23 | 24 | func (i item) Description() string { 25 | return "" 26 | } 27 | 28 | func menuItems() []list.Item { 29 | items := make([]list.Item, len(nameMap)) 30 | for i, f := range Functions { 31 | items[i] = item{name: nameMap[f], Function: f} 32 | } 33 | return items 34 | } 35 | 36 | func newMenu(items []list.Item, width, height int) list.Model { 37 | l := list.New(items, NewDelegate(false), width, height) 38 | l.SetShowHelp(false) 39 | l.SetFilteringEnabled(false) 40 | l.SetShowTitle(false) 41 | l.SetShowPagination(false) 42 | l.SetShowStatusBar(false) 43 | 44 | l.KeyMap.ForceQuit.Unbind() 45 | l.KeyMap.Quit.Unbind() 46 | 47 | return l 48 | } 49 | 50 | func NewDelegate(isActive bool) list.DefaultDelegate { 51 | delegate := list.NewDefaultDelegate() 52 | delegate.SetSpacing(0) 53 | delegate.ShowDescription = false 54 | if isActive { 55 | delegate.Styles = ItemStylesActive() 56 | } else { 57 | delegate.Styles = ItemStylesInactive() 58 | } 59 | return delegate 60 | } 61 | 62 | func ItemStylesActive() (s list.DefaultItemStyles) { 63 | s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2) 64 | s.SelectedTitle = style.SelectedTitle.Copy().Padding(0, 1, 0, 1). 65 | Border(lipgloss.NormalBorder(), false, false, false, true). 66 | BorderForeground(style.SelectedColor1) 67 | s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0) 68 | return s 69 | } 70 | 71 | func ItemStylesInactive() (s list.DefaultItemStyles) { 72 | s.NormalTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 2) 73 | s.SelectedTitle = style.NormalTitle.Copy().Padding(0, 1, 0, 2) 74 | s.DimmedTitle = style.DimmedTitle.Copy().Padding(0, 1, 0, 0) 75 | return s 76 | } 77 | ``` -------------------------------------------------------------------------------- /app/view.go: -------------------------------------------------------------------------------- ```go 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/charmbracelet/bubbles/help" 7 | "github.com/charmbracelet/bubbles/viewport" 8 | "github.com/charmbracelet/lipgloss" 9 | 10 | "github.com/Zebbeni/ansizalizer/event" 11 | "github.com/Zebbeni/ansizalizer/style" 12 | ) 13 | 14 | const ( 15 | displayHeight = 3 16 | helpHeight = 1 17 | 18 | controlsWidth = 30 19 | ) 20 | 21 | func (m Model) renderControls() string { 22 | viewport := viewport.New(controlsWidth, m.leftPanelHeight()) 23 | 24 | leftContent := m.controls.View() 25 | 26 | viewport.SetContent(lipgloss.NewStyle(). 27 | Width(controlsWidth). 28 | Height(m.leftPanelHeight()). 29 | Render(leftContent)) 30 | return viewport.View() 31 | } 32 | 33 | func (m Model) renderViewer() string { 34 | imgString := m.viewer.View() 35 | imgWidth, imgHeight := lipgloss.Size(imgString) 36 | 37 | imgViewer := imgString 38 | 39 | // only render box label border around content if big enough. 40 | if imgHeight > 1 && imgWidth > 4 { 41 | boxLabelRenderer := style.BoxWithLabel{ 42 | BoxStyle: lipgloss.NewStyle().BorderForeground(style.ExtraDimColor).Border(lipgloss.RoundedBorder()), 43 | LabelStyle: lipgloss.NewStyle().Foreground(style.ExtraDimColor).AlignHorizontal(lipgloss.Center).AlignVertical(lipgloss.Bottom), 44 | } 45 | imgViewer = boxLabelRenderer.Render(fmt.Sprintf("%dx%d", imgWidth, imgHeight), imgString, imgWidth) 46 | } 47 | 48 | renderViewport := viewport.New(m.rPanelWidth()-2, m.rPanelHeight()-displayHeight-2) 49 | 50 | vpRightStyle := lipgloss.NewStyle().Align(lipgloss.Center).AlignVertical(lipgloss.Center) 51 | rightContent := vpRightStyle.Copy().Width(m.rPanelWidth() - 2).Height(m.rPanelHeight() - 4).Render(imgViewer) 52 | renderViewport.SetContent(rightContent) 53 | 54 | content := renderViewport.View() 55 | 56 | return style.NormalButton.Copy().BorderForeground(style.DimmedColor1).Render(content) 57 | } 58 | 59 | func (m Model) renderHelp() string { 60 | helpBar := help.New() 61 | helpContent := helpBar.View(event.KeyMap) 62 | return lipgloss.NewStyle().PaddingLeft(1).Render(helpContent) 63 | } 64 | ``` -------------------------------------------------------------------------------- /style/color.go: -------------------------------------------------------------------------------- ```go 1 | package style 2 | 3 | import "github.com/charmbracelet/lipgloss" 4 | 5 | var ( 6 | NormalColor1 = lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#aaaaaa"} 7 | NormalColor2 = lipgloss.AdaptiveColor{Light: "#3a3a3a", Dark: "#888888"} 8 | SelectedColor1 = lipgloss.AdaptiveColor{Light: "#444444", Dark: "#ffffff"} 9 | SelectedColor2 = lipgloss.AdaptiveColor{Light: "#666666", Dark: "#dddddd"} 10 | ExtraDimColor = lipgloss.AdaptiveColor{Light: "#bbbbbb", Dark: "#444444"} 11 | DimmedColor1 = lipgloss.AdaptiveColor{Light: "#999999", Dark: "#777777"} 12 | DimmedColor2 = lipgloss.AdaptiveColor{Light: "#aaaaaa", Dark: "#666666"} 13 | 14 | NormalTitle = lipgloss.NewStyle().Foreground(NormalColor1) 15 | NormalParagraph = lipgloss.NewStyle().Foreground(NormalColor2) 16 | 17 | SelectedTitle = lipgloss.NewStyle().Foreground(SelectedColor1) 18 | SelectedParagraph = lipgloss.NewStyle().Foreground(SelectedColor2) 19 | 20 | DimmedTitle = lipgloss.NewStyle().Foreground(DimmedColor1) 21 | ExtraDimTitle = lipgloss.NewStyle().Foreground(ExtraDimColor) 22 | DimmedParagraph = lipgloss.NewStyle().Foreground(DimmedColor2) 23 | 24 | ActiveButton = lipgloss.NewStyle(). 25 | BorderStyle(lipgloss.RoundedBorder()). 26 | BorderForeground(NormalColor1). 27 | Foreground(NormalColor1) 28 | FocusButton = lipgloss.NewStyle(). 29 | BorderStyle(lipgloss.RoundedBorder()). 30 | BorderForeground(SelectedColor1). 31 | Foreground(SelectedColor1) 32 | NormalButton = lipgloss.NewStyle(). 33 | BorderStyle(lipgloss.RoundedBorder()). 34 | BorderForeground(DimmedColor1). 35 | Foreground(DimmedColor1) 36 | 37 | ActiveButtonNode = lipgloss.NewStyle(). 38 | PaddingLeft(1). 39 | Foreground(NormalColor1) 40 | FocusButtonNode = lipgloss.NewStyle(). 41 | Border(lipgloss.RoundedBorder(), false, false, false, true). 42 | BorderForeground(SelectedColor1). 43 | Foreground(SelectedColor1). 44 | Padding(0) 45 | NormalButtonNode = lipgloss.NewStyle(). 46 | PaddingLeft(1). 47 | Foreground(DimmedColor1) 48 | ) 49 | ``` -------------------------------------------------------------------------------- /palette/model.go: -------------------------------------------------------------------------------- ```go 1 | package palette 2 | 3 | import ( 4 | "image/color" 5 | "math" 6 | 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/charmbracelet/lipgloss" 9 | "github.com/lucasb-eyer/go-colorful" 10 | 11 | "github.com/Zebbeni/ansizalizer/style" 12 | ) 13 | 14 | type Model struct { 15 | name string 16 | colors color.Palette 17 | width int 18 | height int 19 | } 20 | 21 | func New(name string, colors color.Palette, w, h int) Model { 22 | return Model{ 23 | name: name, 24 | colors: colors, 25 | width: w, 26 | height: h, 27 | } 28 | } 29 | 30 | func (m Model) Init() tea.Cmd { 31 | return nil 32 | } 33 | 34 | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 35 | return m, nil 36 | } 37 | 38 | func (m Model) View() string { 39 | title := style.SelectedTitle.Render(m.name) 40 | description := m.Description() 41 | 42 | return lipgloss.JoinVertical(lipgloss.Top, title, description) 43 | } 44 | 45 | func (m Model) FilterValue() string { 46 | return m.name 47 | } 48 | 49 | func (m Model) Title() string { 50 | return m.name 51 | } 52 | 53 | func (m Model) Description() string { 54 | runes := make([]string, len(m.colors)/2+1) 55 | rows := make([]string, 0, m.height) 56 | for idx := 0; idx < len(m.colors); idx += 2 { 57 | var fg, bg colorful.Color 58 | var lipFg, lipBg lipgloss.Color 59 | 60 | fg, _ = colorful.MakeColor(m.colors[idx]) 61 | lipFg = lipgloss.Color(fg.Hex()) 62 | blockStyle := lipgloss.NewStyle().Foreground(lipFg) 63 | 64 | if idx+1 < len(m.colors) { 65 | bg, _ = colorful.MakeColor(m.colors[idx+1]) 66 | lipBg = lipgloss.Color(bg.Hex()) 67 | blockStyle = blockStyle.Copy().Background(lipBg) 68 | } 69 | runes[idx/2] = blockStyle.Render(string('▀')) 70 | } 71 | for i := 0; i < m.height; i++ { 72 | start := m.width * i 73 | if start >= len(runes) { 74 | break 75 | } 76 | stop := int(math.Min(float64(m.width*(i+1)), float64(len(runes)))) 77 | rows = append(rows, "") 78 | rows[i] = lipgloss.JoinHorizontal(lipgloss.Left, runes[start:stop]...) 79 | } 80 | 81 | return lipgloss.JoinVertical(lipgloss.Left, rows...) 82 | } 83 | 84 | func (m Model) Name() string { 85 | return m.name 86 | } 87 | 88 | func (m Model) Colors() color.Palette { 89 | colorsCopy := make([]color.Color, len(m.colors)) 90 | copy(colorsCopy, m.colors) 91 | return colorsCopy 92 | } 93 | ``` -------------------------------------------------------------------------------- /controls/settings/colors/model.go: -------------------------------------------------------------------------------- ```go 1 | package colors 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/key" 5 | tea "github.com/charmbracelet/bubbletea" 6 | "github.com/charmbracelet/lipgloss" 7 | 8 | "github.com/Zebbeni/ansizalizer/controls/settings/palettes" 9 | "github.com/Zebbeni/ansizalizer/event" 10 | "github.com/Zebbeni/ansizalizer/palette" 11 | ) 12 | 13 | type State int 14 | 15 | const ( 16 | UsePalette State = iota 17 | UseTrueColor 18 | Palette 19 | ) 20 | 21 | type Model struct { 22 | focus State 23 | mode State 24 | width int 25 | PaletteControls palettes.Model 26 | 27 | IsActive bool 28 | ShouldClose bool 29 | } 30 | 31 | func New(w int) Model { 32 | return Model{ 33 | focus: UseTrueColor, 34 | mode: UseTrueColor, 35 | width: w, 36 | PaletteControls: palettes.New(w), 37 | IsActive: false, 38 | ShouldClose: false, 39 | } 40 | } 41 | 42 | func (m Model) Init() tea.Cmd { 43 | return nil 44 | } 45 | 46 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 47 | switch m.focus { 48 | case Palette: 49 | return m.handlePaletteUpdate(msg) 50 | } 51 | 52 | switch msg := msg.(type) { 53 | case tea.KeyMsg: 54 | switch { 55 | case key.Matches(msg, event.KeyMap.Enter): 56 | return m.handleEnter() 57 | case key.Matches(msg, event.KeyMap.Nav): 58 | return m.handleNav(msg) 59 | case key.Matches(msg, event.KeyMap.Esc): 60 | return m.handleEsc() 61 | } 62 | } 63 | return m, nil 64 | } 65 | 66 | func (m Model) View() string { 67 | paletteToggles := m.drawPaletteToggles() 68 | if m.mode == UseTrueColor { 69 | return paletteToggles 70 | } 71 | 72 | paletteTabs := m.PaletteControls.View() 73 | return lipgloss.JoinVertical(lipgloss.Left, paletteToggles, paletteTabs) 74 | } 75 | 76 | // GetSelected returns isPaletted, isAdaptive, and the palette (if applicable) 77 | func (m Model) GetSelected() (bool, bool, palette.Model) { 78 | colorPalette := m.PaletteControls.GetCurrentPalette() 79 | 80 | if m.mode == UseTrueColor { 81 | return true, false, colorPalette 82 | } 83 | 84 | return false, m.PaletteControls.IsAdaptive(), colorPalette 85 | } 86 | 87 | func (m Model) GetCurrentPalette() palette.Model { 88 | return m.PaletteControls.GetCurrentPalette() 89 | } 90 | 91 | func (m Model) IsLimited() bool { 92 | return m.mode == UsePalette 93 | } 94 | ``` -------------------------------------------------------------------------------- /controls/settings/colors/update.go: -------------------------------------------------------------------------------- ```go 1 | package colors 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/key" 5 | tea "github.com/charmbracelet/bubbletea" 6 | 7 | "github.com/Zebbeni/ansizalizer/event" 8 | ) 9 | 10 | type Direction int 11 | 12 | const ( 13 | Left Direction = iota 14 | Right 15 | Up 16 | Down 17 | ) 18 | 19 | var navMap = map[Direction]map[State]State{ 20 | Right: { 21 | UseTrueColor: UsePalette, 22 | }, 23 | Left: { 24 | UsePalette: UseTrueColor, 25 | }, 26 | Up: { 27 | Palette: UsePalette, 28 | }, 29 | Down: { 30 | UseTrueColor: Palette, 31 | UsePalette: Palette, 32 | }, 33 | } 34 | 35 | func (m Model) handlePaletteUpdate(msg tea.Msg) (Model, tea.Cmd) { 36 | var cmd tea.Cmd 37 | m.PaletteControls, cmd = m.PaletteControls.Update(msg) 38 | 39 | if m.PaletteControls.ShouldClose { 40 | m.PaletteControls.IsActive = false 41 | m.PaletteControls.ShouldClose = false 42 | m.focus = UsePalette 43 | } 44 | return m, cmd 45 | } 46 | 47 | func (m Model) handleEnter() (Model, tea.Cmd) { 48 | switch m.focus { 49 | case UsePalette: 50 | m.mode = UsePalette 51 | case UseTrueColor: 52 | m.mode = UseTrueColor 53 | } 54 | return m, nil 55 | } 56 | 57 | func (m Model) handleEsc() (Model, tea.Cmd) { 58 | return m, nil 59 | } 60 | 61 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { 62 | var cmd tea.Cmd 63 | switch { 64 | case key.Matches(msg, event.KeyMap.Right): 65 | if next, hasNext := navMap[Right][m.focus]; hasNext { 66 | return m.setFocus(next) 67 | } 68 | case key.Matches(msg, event.KeyMap.Left): 69 | if next, hasNext := navMap[Left][m.focus]; hasNext { 70 | return m.setFocus(next) 71 | } 72 | case key.Matches(msg, event.KeyMap.Up): 73 | if next, hasNext := navMap[Up][m.focus]; hasNext { 74 | return m.setFocus(next) 75 | } else { 76 | m.IsActive = false 77 | m.ShouldClose = true 78 | } 79 | case key.Matches(msg, event.KeyMap.Down): 80 | if next, hasNext := navMap[Down][m.focus]; hasNext { 81 | return m.setFocus(next) 82 | } else { 83 | m.IsActive = false 84 | m.ShouldClose = true 85 | } 86 | } 87 | return m, cmd 88 | } 89 | 90 | func (m Model) setFocus(focus State) (Model, tea.Cmd) { 91 | if m.mode == UseTrueColor && focus == Palette { 92 | return m, nil 93 | } 94 | 95 | m.focus = focus 96 | switch m.focus { 97 | case Palette: 98 | m.PaletteControls.IsActive = true 99 | } 100 | 101 | return m, nil 102 | } 103 | ``` -------------------------------------------------------------------------------- /controls/export/source/model.go: -------------------------------------------------------------------------------- ```go 1 | package source 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/key" 5 | tea "github.com/charmbracelet/bubbletea" 6 | "github.com/charmbracelet/lipgloss" 7 | 8 | "github.com/Zebbeni/ansizalizer/controls/browser" 9 | "github.com/Zebbeni/ansizalizer/event" 10 | ) 11 | 12 | type State int 13 | 14 | const ( 15 | ExpFile State = iota 16 | ExpDirectory 17 | Input 18 | Browser 19 | SubDirsYes 20 | SubDirsNo 21 | ) 22 | 23 | type Model struct { 24 | focus State 25 | 26 | doExportDirectory bool 27 | includeSubdirectories bool 28 | 29 | Browser browser.Model 30 | selectedDir string 31 | selectedFile string 32 | 33 | ShouldClose bool 34 | ShouldUnfocus bool 35 | 36 | IsActive bool 37 | 38 | width int 39 | } 40 | 41 | func New(w int) Model { 42 | browserModel := browser.New(nil, w-2) 43 | 44 | return Model{ 45 | focus: ExpDirectory, 46 | 47 | Browser: browserModel, 48 | 49 | doExportDirectory: true, 50 | includeSubdirectories: false, 51 | 52 | selectedDir: "", 53 | selectedFile: "", 54 | 55 | width: w, 56 | ShouldClose: false, 57 | } 58 | } 59 | 60 | func (m Model) Init() tea.Cmd { 61 | return nil 62 | } 63 | 64 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 65 | var cmd tea.Cmd 66 | switch m.focus { 67 | case Browser: 68 | return m.handleSrcBrowserUpdate(msg) 69 | } 70 | 71 | switch msg := msg.(type) { 72 | case tea.KeyMsg: 73 | switch { 74 | case key.Matches(msg, event.KeyMap.Esc): 75 | return m.handleEsc() 76 | case key.Matches(msg, event.KeyMap.Nav): 77 | return m.handleNav(msg) 78 | case key.Matches(msg, event.KeyMap.Enter): 79 | return m.handleEnter() 80 | } 81 | } 82 | return m, cmd 83 | } 84 | 85 | func (m Model) View() string { 86 | content := make([]string, 0, 5) 87 | content = append(content, m.drawExportTypeOptions()) 88 | 89 | selected := lipgloss.NewStyle().PaddingTop(1).Render(m.drawSelected()) 90 | content = append(content, selected) 91 | 92 | if m.focus == Browser { 93 | content = append(content, m.Browser.View()) 94 | } 95 | 96 | if m.doExportDirectory { 97 | content = append(content, m.drawSubDirOptions()) 98 | } 99 | 100 | return lipgloss.JoinVertical(lipgloss.Left, content...) 101 | } 102 | 103 | func (m Model) GetSelected() (path string, isDir, useSubDirs bool) { 104 | if m.doExportDirectory { 105 | isDir = true 106 | path = m.selectedDir 107 | useSubDirs = m.includeSubdirectories 108 | } else { 109 | path = m.selectedFile 110 | isDir = false 111 | useSubDirs = false 112 | } 113 | return 114 | } 115 | ``` -------------------------------------------------------------------------------- /event/command.go: -------------------------------------------------------------------------------- ```go 1 | package event 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | 7 | tea "github.com/charmbracelet/bubbletea" 8 | ) 9 | 10 | type StartRenderToViewMsg bool 11 | 12 | func StartRenderToViewCmd() tea.Msg { 13 | return StartRenderToViewMsg(true) 14 | } 15 | 16 | type FinishRenderToViewMsg struct { 17 | FilePath string 18 | ImgString string 19 | ColorsString string 20 | } 21 | 22 | type StartRenderToExportMsg bool 23 | 24 | func StartRenderToExportCmd() tea.Msg { 25 | return StartRenderToExportMsg(true) 26 | } 27 | 28 | type FinishRenderToExportMsg struct { 29 | FilePath string 30 | ImgString string 31 | ColorsString string 32 | } 33 | 34 | func BuildFinishRenderToExportCmd(msg FinishRenderToExportMsg) tea.Cmd { 35 | return func() tea.Msg { return msg } 36 | } 37 | 38 | type StartAdaptingMsg bool 39 | 40 | func StartAdaptingCmd() tea.Msg { 41 | return StartAdaptingMsg(true) 42 | } 43 | 44 | type FinishAdaptingMsg struct { 45 | Name string 46 | Colors color.Palette 47 | } 48 | 49 | type StartExportMsg struct { 50 | SourcePath string 51 | DestinationPath string 52 | IsDir bool 53 | UseSubDirs bool 54 | } 55 | 56 | func BuildStartExportCmd(msg StartExportMsg) tea.Cmd { 57 | return func() tea.Msg { return msg } 58 | } 59 | 60 | type FinishExportMsg bool 61 | 62 | func FinishExportingCmd() tea.Msg { 63 | return FinishExportMsg(true) 64 | } 65 | 66 | // DisplayMsg could eventually contain a type 67 | // that indicates what style to use (warning, error, etc.) 68 | type DisplayMsg string 69 | 70 | func BuildDisplayCmd(msg string) tea.Cmd { 71 | return func() tea.Msg { return DisplayMsg(msg) } 72 | } 73 | 74 | func ClearDisplayCmd() tea.Msg { 75 | return DisplayMsg("") 76 | } 77 | 78 | // LospecRequestMsg is a url request used to get a list of 79 | type LospecRequestMsg struct { 80 | ID int 81 | Page int 82 | URL string 83 | } 84 | 85 | func BuildLospecRequestCmd(msg LospecRequestMsg) tea.Cmd { 86 | display := fmt.Sprintf("loading palettes") 87 | return tea.Batch(func() tea.Msg { return msg }, BuildDisplayCmd(display)) 88 | } 89 | 90 | type LospecData struct { 91 | Palettes []struct { 92 | Colors []string `json:"colors"` 93 | Title string `json:"title"` 94 | } `json:"palettes"` 95 | TotalCount int `json:"totalCount"` 96 | } 97 | 98 | type LospecResponseMsg struct { 99 | ID int 100 | Page int 101 | Data LospecData 102 | } 103 | 104 | func BuildLospecResponseCmd(msg LospecResponseMsg) tea.Cmd { 105 | return tea.Batch(func() tea.Msg { return msg }, ClearDisplayCmd) 106 | } 107 | ``` -------------------------------------------------------------------------------- /controls/settings/characters/model.go: -------------------------------------------------------------------------------- ```go 1 | package characters 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/key" 5 | "github.com/charmbracelet/bubbles/textinput" 6 | tea "github.com/charmbracelet/bubbletea" 7 | "github.com/charmbracelet/lipgloss" 8 | 9 | "github.com/Zebbeni/ansizalizer/event" 10 | ) 11 | 12 | type State int 13 | 14 | const ( 15 | Ascii State = iota 16 | Unicode 17 | Custom 18 | AsciiAz 19 | AsciiNums 20 | AsciiSpec 21 | AsciiAll 22 | UnicodeFull 23 | UnicodeHalf 24 | UnicodeQuart 25 | UnicodeShadeLight 26 | UnicodeShadeMed 27 | UnicodeShadeHeavy 28 | SymbolsForm 29 | OneColor 30 | TwoColor 31 | ) 32 | 33 | type Model struct { 34 | focus State 35 | active State 36 | mode State 37 | charControls State 38 | unicodeMode State 39 | asciiMode State 40 | useFgBg State 41 | customInput textinput.Model 42 | ShouldClose bool 43 | IsActive bool 44 | width int 45 | } 46 | 47 | func New(w int) Model { 48 | return Model{ 49 | focus: Unicode, 50 | active: Unicode, 51 | mode: Unicode, 52 | charControls: Unicode, 53 | asciiMode: AsciiAz, 54 | unicodeMode: UnicodeHalf, 55 | useFgBg: TwoColor, 56 | customInput: newInput("Symbols", "/%A"), 57 | ShouldClose: false, 58 | IsActive: false, 59 | width: w, 60 | } 61 | } 62 | 63 | func (m Model) Init() tea.Cmd { 64 | return nil 65 | } 66 | 67 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 68 | switch m.active { 69 | case SymbolsForm: 70 | if m.customInput.Focused() { 71 | return m.handleSymbolsFormUpdate(msg) 72 | } 73 | } 74 | 75 | switch msg := msg.(type) { 76 | case tea.KeyMsg: 77 | switch { 78 | case key.Matches(msg, event.KeyMap.Enter): 79 | return m.handleEnter() 80 | case key.Matches(msg, event.KeyMap.Nav): 81 | return m.handleNav(msg) 82 | case key.Matches(msg, event.KeyMap.Esc): 83 | return m.handleEsc() 84 | } 85 | } 86 | return m, nil 87 | } 88 | 89 | func (m Model) View() string { 90 | colorsButtons := m.drawColorsButtons() 91 | charTabs := m.drawCharTabs() 92 | return lipgloss.JoinVertical(lipgloss.Top, colorsButtons, charTabs) 93 | } 94 | 95 | // Selected returns the mode, charMode, whether to use two colors, and the 96 | // current set of custom-defined characters 97 | func (m Model) Selected() (State, State, State, []rune) { 98 | var charMode State 99 | 100 | switch m.mode { 101 | case Unicode: 102 | charMode = m.unicodeMode 103 | case Ascii: 104 | charMode = m.asciiMode 105 | case Custom: 106 | charMode = Custom 107 | } 108 | 109 | return m.mode, charMode, m.useFgBg, []rune(m.customInput.Value()) 110 | } 111 | ``` -------------------------------------------------------------------------------- /controls/export/source/update.go: -------------------------------------------------------------------------------- ```go 1 | package source 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/key" 5 | tea "github.com/charmbracelet/bubbletea" 6 | 7 | "github.com/Zebbeni/ansizalizer/controls/browser" 8 | "github.com/Zebbeni/ansizalizer/event" 9 | "github.com/Zebbeni/ansizalizer/global" 10 | ) 11 | 12 | type Direction int 13 | 14 | const ( 15 | Left Direction = iota 16 | Right 17 | Up 18 | Down 19 | ) 20 | 21 | var ( 22 | navMap = map[Direction]map[State]State{ 23 | Right: {ExpFile: ExpDirectory, SubDirsYes: SubDirsNo}, 24 | Left: {ExpDirectory: ExpFile, SubDirsNo: SubDirsYes}, 25 | Down: {ExpFile: Input, ExpDirectory: Input, Input: SubDirsYes}, 26 | Up: {Input: ExpFile, SubDirsYes: Input, SubDirsNo: Input}, 27 | } 28 | ) 29 | 30 | func (m Model) handleEsc() (Model, tea.Cmd) { 31 | m.ShouldClose = true 32 | m.IsActive = false 33 | return m, nil 34 | } 35 | 36 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { 37 | switch { 38 | case key.Matches(msg, event.KeyMap.Right): 39 | if next, hasNext := navMap[Right][m.focus]; hasNext { 40 | m.focus = next 41 | } 42 | case key.Matches(msg, event.KeyMap.Left): 43 | if next, hasNext := navMap[Left][m.focus]; hasNext { 44 | m.focus = next 45 | } 46 | case key.Matches(msg, event.KeyMap.Down): 47 | if next, hasNext := navMap[Down][m.focus]; hasNext { 48 | m.focus = next 49 | } else { 50 | m.ShouldClose = true 51 | } 52 | case key.Matches(msg, event.KeyMap.Up): 53 | if next, hasNext := navMap[Up][m.focus]; hasNext { 54 | m.focus = next 55 | } else { 56 | m.ShouldClose = true 57 | } 58 | } 59 | return m, nil 60 | } 61 | 62 | func (m Model) handleEnter() (Model, tea.Cmd) { 63 | switch m.focus { 64 | case ExpFile: 65 | m.focus = Browser 66 | m.doExportDirectory = false 67 | m.Browser = browser.New(global.ImgExtensions, m.width) 68 | case ExpDirectory: 69 | m.focus = Browser 70 | m.doExportDirectory = true 71 | m.Browser = browser.New(nil, m.width) 72 | case Input: 73 | m.focus = Browser 74 | case SubDirsYes: 75 | m.includeSubdirectories = true 76 | case SubDirsNo: 77 | m.includeSubdirectories = false 78 | } 79 | return m, nil 80 | } 81 | 82 | func (m Model) handleSrcBrowserUpdate(msg tea.Msg) (Model, tea.Cmd) { 83 | var cmd tea.Cmd 84 | m.Browser, cmd = m.Browser.Update(msg) 85 | if m.doExportDirectory { 86 | m.selectedDir = m.Browser.SelectedDir 87 | } else { 88 | m.selectedFile = m.Browser.SelectedFile 89 | } 90 | 91 | if m.Browser.ShouldClose { 92 | m.focus = Input 93 | m.Browser.ShouldClose = false 94 | } 95 | return m, cmd 96 | } 97 | 98 | func (m Model) handleIncludeSubdirectories(shouldInclude bool) (Model, tea.Cmd) { 99 | m.includeSubdirectories = shouldInclude 100 | return m, nil 101 | } 102 | ``` -------------------------------------------------------------------------------- /controls/settings/palettes/adaptive/model.go: -------------------------------------------------------------------------------- ```go 1 | package adaptive 2 | 3 | import ( 4 | "image/color" 5 | "strconv" 6 | 7 | "github.com/charmbracelet/bubbles/key" 8 | "github.com/charmbracelet/bubbles/textinput" 9 | tea "github.com/charmbracelet/bubbletea" 10 | "github.com/charmbracelet/lipgloss" 11 | 12 | "github.com/Zebbeni/ansizalizer/event" 13 | "github.com/Zebbeni/ansizalizer/palette" 14 | ) 15 | 16 | type State int 17 | 18 | const ( 19 | CountForm State = iota 20 | IterForm 21 | Generate 22 | Save 23 | ) 24 | 25 | type Model struct { 26 | focus State 27 | active State 28 | 29 | palette palette.Model 30 | 31 | countInput textinput.Model 32 | iterInput textinput.Model 33 | 34 | width, height int 35 | 36 | ShouldClose bool 37 | ShouldUnfocus bool 38 | IsActive bool 39 | IsSelected bool // true if we've selected something (ie. render w/ adaptive) 40 | } 41 | 42 | func New(w int) Model { 43 | return Model{ 44 | focus: CountForm, 45 | 46 | countInput: newInput(CountForm), 47 | iterInput: newInput(IterForm), 48 | 49 | ShouldUnfocus: false, 50 | IsActive: false, 51 | IsSelected: false, 52 | 53 | width: w, 54 | } 55 | } 56 | 57 | func (m Model) Init() tea.Cmd { 58 | return nil 59 | } 60 | 61 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 62 | switch m.active { 63 | case CountForm: 64 | if m.countInput.Focused() { 65 | return m.handleCountUpdate(msg) 66 | } 67 | case IterForm: 68 | if m.iterInput.Focused() { 69 | return m.handleIterUpdate(msg) 70 | } 71 | } 72 | 73 | switch msg := msg.(type) { 74 | case tea.KeyMsg: 75 | switch { 76 | case key.Matches(msg, event.KeyMap.Enter): 77 | return m.handleEnter() 78 | case key.Matches(msg, event.KeyMap.Nav): 79 | return m.handleNav(msg) 80 | case key.Matches(msg, event.KeyMap.Esc): 81 | return m.handleEsc() 82 | } 83 | } 84 | return m, nil 85 | } 86 | 87 | func (m Model) View() string { 88 | title := m.drawTitle() 89 | inputs := m.drawInputs() 90 | generate := m.drawGenerateButton() 91 | if len(m.palette.Colors()) == 0 { 92 | return lipgloss.JoinVertical(lipgloss.Top, title, inputs, generate) 93 | } 94 | 95 | palette := lipgloss.NewStyle().Padding(0, 1, 0, 1).Render(m.palette.View()) 96 | saveButton := m.drawSaveButton() 97 | content := lipgloss.JoinVertical(lipgloss.Top, title, inputs, generate, palette, saveButton) 98 | return content 99 | } 100 | 101 | func (m Model) Info() (int, int) { 102 | var count, iterations int 103 | count, _ = strconv.Atoi(m.countInput.Value()) 104 | iterations, _ = strconv.Atoi(m.iterInput.Value()) 105 | return count, iterations 106 | } 107 | 108 | func (m Model) GetCurrent() palette.Model { 109 | return m.palette 110 | } 111 | 112 | func (m Model) SetPalette(colors color.Palette, name string) Model { 113 | m.palette = palette.New(name, colors, m.width-4, 3) 114 | return m 115 | } 116 | ``` -------------------------------------------------------------------------------- /controls/settings/advanced/update.go: -------------------------------------------------------------------------------- ```go 1 | package advanced 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/key" 5 | tea "github.com/charmbracelet/bubbletea" 6 | 7 | "github.com/Zebbeni/ansizalizer/event" 8 | ) 9 | 10 | type Direction int 11 | 12 | const ( 13 | Left Direction = iota 14 | Right 15 | Up 16 | Down 17 | ) 18 | 19 | var navMap = map[Direction]map[State]State{ 20 | Right: { 21 | Sampling: Dithering, 22 | }, 23 | Left: { 24 | Dithering: Sampling, 25 | }, 26 | Down: { 27 | Sampling: SamplingControls, 28 | Dithering: DitheringControls, 29 | }, 30 | Up: { 31 | SamplingControls: Sampling, 32 | DitheringControls: Dithering, 33 | }, 34 | } 35 | 36 | func (m Model) handleSamplingUpdate(msg tea.Msg) (Model, tea.Cmd) { 37 | var cmd tea.Cmd 38 | m.sampling, cmd = m.sampling.Update(msg) 39 | 40 | if m.sampling.ShouldClose { 41 | m.active = Menu 42 | m.focus = Sampling 43 | m.sampling.ShouldClose = false 44 | m.sampling.IsActive = false 45 | } 46 | return m, cmd 47 | } 48 | 49 | func (m Model) handleDitheringUpdate(msg tea.Msg) (Model, tea.Cmd) { 50 | var cmd tea.Cmd 51 | m.dithering, cmd = m.dithering.Update(msg) 52 | 53 | if m.dithering.ShouldClose { 54 | m.active = Menu 55 | m.focus = Dithering 56 | m.dithering.ShouldClose = false 57 | m.dithering.IsActive = false 58 | } 59 | return m, cmd 60 | } 61 | 62 | func (m Model) handleEsc() (Model, tea.Cmd) { 63 | m.ShouldClose = true 64 | return m, nil 65 | } 66 | 67 | func (m Model) handleEnter() (Model, tea.Cmd) { 68 | m.active = m.focus 69 | return m, nil 70 | } 71 | 72 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { 73 | var cmd tea.Cmd 74 | switch { 75 | case key.Matches(msg, event.KeyMap.Right): 76 | if next, hasNext := navMap[Right][m.focus]; hasNext { 77 | return m.setFocus(next) 78 | } 79 | case key.Matches(msg, event.KeyMap.Left): 80 | if next, hasNext := navMap[Left][m.focus]; hasNext { 81 | return m.setFocus(next) 82 | } 83 | case key.Matches(msg, event.KeyMap.Up): 84 | if next, hasNext := navMap[Up][m.focus]; hasNext { 85 | return m.setFocus(next) 86 | } else { 87 | m.IsActive = false 88 | m.ShouldClose = true 89 | } 90 | case key.Matches(msg, event.KeyMap.Down): 91 | if next, hasNext := navMap[Down][m.focus]; hasNext { 92 | return m.setFocus(next) 93 | } else { 94 | m.IsActive = false 95 | m.ShouldClose = true 96 | } 97 | } 98 | return m, cmd 99 | } 100 | 101 | func (m Model) setFocus(focus State) (Model, tea.Cmd) { 102 | m.focus = focus 103 | switch m.focus { 104 | case Sampling: 105 | m.activeTab = Sampling 106 | case Dithering: 107 | m.activeTab = Dithering 108 | case SamplingControls: 109 | m.active = SamplingControls 110 | m.sampling.IsActive = true 111 | case DitheringControls: 112 | m.active = DitheringControls 113 | m.dithering.IsActive = true 114 | } 115 | return m, nil 116 | } 117 | ``` -------------------------------------------------------------------------------- /controls/settings/size/model.go: -------------------------------------------------------------------------------- ```go 1 | package size 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/charmbracelet/bubbles/key" 7 | "github.com/charmbracelet/bubbles/textinput" 8 | tea "github.com/charmbracelet/bubbletea" 9 | "github.com/charmbracelet/lipgloss" 10 | 11 | "github.com/Zebbeni/ansizalizer/event" 12 | ) 13 | 14 | const DEFAULT_CHAR_W_TO_H_RATIO = 0.5 15 | 16 | type State int 17 | type Mode int 18 | 19 | const ( 20 | Fit Mode = iota 21 | Stretch 22 | ) 23 | 24 | const ( 25 | FitButton State = iota 26 | StretchButton 27 | WidthForm 28 | HeightForm 29 | CharRatioForm 30 | None 31 | ) 32 | 33 | type Model struct { 34 | focus State 35 | active State 36 | mode Mode 37 | 38 | widthInput textinput.Model 39 | heightInput textinput.Model 40 | charRatioInput textinput.Model 41 | 42 | ShouldUnfocus bool 43 | ShouldClose bool 44 | IsActive bool 45 | } 46 | 47 | func New() Model { 48 | return Model{ 49 | focus: FitButton, 50 | active: None, 51 | mode: Fit, 52 | widthInput: newInput(WidthForm, 50), 53 | heightInput: newInput(HeightForm, 40), 54 | charRatioInput: newFloatInput(CharRatioForm, DEFAULT_CHAR_W_TO_H_RATIO), 55 | 56 | ShouldUnfocus: false, 57 | ShouldClose: false, 58 | IsActive: false, 59 | } 60 | } 61 | 62 | func (m Model) Init() tea.Cmd { 63 | return nil 64 | } 65 | 66 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 67 | var cmd1, cmd2 tea.Cmd 68 | newM := m 69 | 70 | switch m.active { 71 | case WidthForm: 72 | if m.widthInput.Focused() { 73 | newM, cmd1 = newM.handleWidthUpdate(msg) 74 | } 75 | case HeightForm: 76 | if m.heightInput.Focused() { 77 | newM, cmd1 = newM.handleHeightUpdate(msg) 78 | } 79 | case CharRatioForm: 80 | if m.charRatioInput.Focused() { 81 | newM, cmd1 = newM.handleCharRatioUpdate(msg) 82 | } 83 | } 84 | 85 | switch msg := msg.(type) { 86 | case tea.KeyMsg: 87 | switch { 88 | case key.Matches(msg, event.KeyMap.Enter): 89 | newM, cmd2 = newM.handleEnter() 90 | case key.Matches(msg, event.KeyMap.Nav): 91 | newM, cmd2 = newM.handleNav(msg) 92 | case key.Matches(msg, event.KeyMap.Esc): 93 | newM, cmd2 = newM.handleEsc() 94 | } 95 | } 96 | return newM, tea.Batch(cmd1, cmd2) 97 | } 98 | 99 | func (m Model) View() string { 100 | buttonRow := m.drawButtons() 101 | forms := m.drawSizeForms() 102 | ratioForm := m.drawCharRatioForm() 103 | return lipgloss.JoinVertical(lipgloss.Left, buttonRow, forms, ratioForm) 104 | } 105 | 106 | func (m Model) Info() (Mode, int, int, float64) { 107 | var width, height int 108 | width, _ = strconv.Atoi(m.widthInput.Value()) 109 | height, _ = strconv.Atoi(m.heightInput.Value()) 110 | charRatio, err := strconv.ParseFloat(m.charRatioInput.Value(), 64) 111 | if err != nil { 112 | charRatio = DEFAULT_CHAR_W_TO_H_RATIO 113 | } 114 | return m.mode, width, height, charRatio 115 | } 116 | ``` -------------------------------------------------------------------------------- /controls/browser/update.go: -------------------------------------------------------------------------------- ```go 1 | package browser 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/key" 5 | tea "github.com/charmbracelet/bubbletea" 6 | 7 | "github.com/Zebbeni/ansizalizer/controls/menu" 8 | "github.com/Zebbeni/ansizalizer/event" 9 | ) 10 | 11 | func (m Model) handleEnter() (Model, tea.Cmd) { 12 | return m.updateSelected() 13 | } 14 | 15 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { 16 | if m.currentList().Index() == 0 && key.Matches(msg, event.KeyMap.Up) { 17 | m.ShouldClose = true 18 | return m, nil 19 | } 20 | 21 | cmds := make([]tea.Cmd, 2) 22 | m.lists[m.listIndex()], cmds[0] = m.currentList().Update(msg) 23 | m, cmds[1] = m.updateActive() 24 | return m, tea.Batch(cmds...) 25 | } 26 | 27 | func (m Model) handleEsc() (Model, tea.Cmd) { 28 | // remove last list if possible (go back to previous) 29 | if len(m.lists) > 1 { 30 | m.lists = m.lists[:m.listIndex()] 31 | return m, nil 32 | } 33 | 34 | m.ShouldClose = true 35 | return m, nil 36 | } 37 | 38 | func (m Model) updateActive() (Model, tea.Cmd) { 39 | itm, ok := m.currentList().SelectedItem().(item) 40 | if !ok { 41 | panic("Unexpected list item type") 42 | } 43 | 44 | if itm.isDir && m.ActiveDir != itm.path { 45 | m.ActiveDir = itm.path 46 | return m, nil 47 | } 48 | 49 | if itm.isDir == false && m.ActiveFile != itm.path { 50 | m.ActiveFile = itm.path 51 | return m, event.StartRenderToViewCmd 52 | } 53 | 54 | return m, nil 55 | } 56 | 57 | func (m Model) updateSelected() (Model, tea.Cmd) { 58 | itm, ok := m.currentList().SelectedItem().(item) 59 | if !ok { 60 | panic("Unexpected list item type") 61 | } 62 | 63 | if itm.isDir { 64 | m.SelectedDir = itm.path 65 | m = m.addListForDirectory(itm.path) 66 | } else { 67 | m.SelectedFile = itm.path 68 | m.ShouldClose = true 69 | } 70 | 71 | return m, nil 72 | } 73 | 74 | func (m Model) addListForDirectory(dir string) Model { 75 | newList := menu.New(getItems(m.fileExtensions, dir), m.width) 76 | 77 | newList.SetShowTitle(false) 78 | 79 | //title := filepath.Join(filepath.Base(filepath.Dir(dir)), filepath.Base(dir)) 80 | 81 | //newList.Title = fitString(title, m.width-10) 82 | //newList.Styles.Title = newList.Styles.Title.Copy().Foreground(style.DimmedColor2).UnsetBackground() 83 | //newList.Styles.TitleBar = newList.Styles.TitleBar.Copy().Padding(0).Height(2) 84 | newList.SetShowStatusBar(false) 85 | newList.SetFilteringEnabled(false) 86 | newList.SetShowFilter(false) 87 | newList.SetWidth(m.width) 88 | 89 | m.lists = append(m.lists, newList) 90 | m.SelectedDir = dir 91 | 92 | return m 93 | } 94 | 95 | func fitString(value string, width int) string { 96 | valueRunes := []rune(value) 97 | 98 | start := len(valueRunes) - width - 2 99 | if start < 0 { 100 | start = 0 101 | } 102 | 103 | if len(valueRunes) > width { 104 | value = "\n.." + string(valueRunes[start:]) 105 | } 106 | 107 | return value 108 | } 109 | ``` -------------------------------------------------------------------------------- /controls/export/update.go: -------------------------------------------------------------------------------- ```go 1 | package export 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/key" 5 | tea "github.com/charmbracelet/bubbletea" 6 | 7 | "github.com/Zebbeni/ansizalizer/event" 8 | ) 9 | 10 | type Direction int 11 | 12 | const ( 13 | Down Direction = iota 14 | Up 15 | ) 16 | 17 | var navMap = map[Direction]map[State]State{ 18 | Down: {Source: Destination, Destination: Process}, 19 | Up: {Destination: Source, Process: Destination}, 20 | } 21 | 22 | func (m Model) handleSourceUpdate(msg tea.Msg) (Model, tea.Cmd) { 23 | var cmd tea.Cmd 24 | m.Source, cmd = m.Source.Update(msg) 25 | 26 | if m.Source.ShouldClose { 27 | m.active = None 28 | m.Source.ShouldClose = false 29 | } 30 | if m.Source.ShouldUnfocus { 31 | return m.handleMenuUpdate(msg) 32 | } 33 | return m, cmd 34 | } 35 | 36 | func (m Model) handleDestinationUpdate(msg tea.Msg) (Model, tea.Cmd) { 37 | var cmd tea.Cmd 38 | m.Destination, cmd = m.Destination.Update(msg) 39 | 40 | if m.Destination.ShouldClose { 41 | m.active = None 42 | m.Destination.ShouldClose = false 43 | } 44 | return m, cmd 45 | } 46 | 47 | func (m Model) handleEnter() (Model, tea.Cmd) { 48 | m.active = m.focus 49 | switch m.active { 50 | case Source: 51 | m.Source.IsActive = true 52 | case Destination: 53 | m.Destination.IsActive = true 54 | case Process: 55 | return m.handleProcess() 56 | } 57 | return m, nil 58 | } 59 | 60 | func (m Model) handleEsc() (Model, tea.Cmd) { 61 | m.ShouldClose = true 62 | return m, nil 63 | } 64 | 65 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { 66 | switch { 67 | case key.Matches(msg, event.KeyMap.Down): 68 | if next, hasNext := navMap[Down][m.focus]; hasNext { 69 | m.focus = next 70 | } else { 71 | m.ShouldClose = true 72 | } 73 | case key.Matches(msg, event.KeyMap.Up): 74 | if next, hasNext := navMap[Up][m.focus]; hasNext { 75 | m.focus = next 76 | } else { 77 | m.ShouldClose = true 78 | } 79 | } 80 | return m, nil 81 | } 82 | 83 | func (m Model) handleMenuUpdate(msg tea.Msg) (Model, tea.Cmd) { 84 | if keyMsg, ok := msg.(tea.KeyMsg); ok { 85 | return m.handleKeyMsg(keyMsg) 86 | } 87 | return m, nil 88 | } 89 | 90 | func (m Model) handleKeyMsg(msg tea.KeyMsg) (Model, tea.Cmd) { 91 | var cmd tea.Cmd 92 | switch { 93 | case key.Matches(msg, event.KeyMap.Enter): 94 | return m.handleEnter() 95 | case key.Matches(msg, event.KeyMap.Nav): 96 | return m.handleNav(msg) 97 | case key.Matches(msg, event.KeyMap.Esc): 98 | return m.handleEsc() 99 | } 100 | return m, cmd 101 | } 102 | 103 | func (m Model) handleProcess() (Model, tea.Cmd) { 104 | sourcePath, isDir, useSubDirs := m.Source.GetSelected() 105 | destinationPath := m.Destination.GetSelected() 106 | return m, event.BuildStartExportCmd(event.StartExportMsg{ 107 | SourcePath: sourcePath, 108 | DestinationPath: destinationPath, 109 | IsDir: isDir, 110 | UseSubDirs: useSubDirs, 111 | }) 112 | } 113 | 114 | func (m Model) GetDestination() (path string) { 115 | return m.Destination.GetSelected() 116 | } 117 | ``` -------------------------------------------------------------------------------- /controls/settings/palettes/loader/model.go: -------------------------------------------------------------------------------- ```go 1 | package loader 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "image/color" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | tea "github.com/charmbracelet/bubbletea" 12 | "github.com/charmbracelet/lipgloss" 13 | "github.com/lucasb-eyer/go-colorful" 14 | 15 | "github.com/Zebbeni/ansizalizer/controls/browser" 16 | "github.com/Zebbeni/ansizalizer/event" 17 | "github.com/Zebbeni/ansizalizer/palette" 18 | "github.com/Zebbeni/ansizalizer/style" 19 | ) 20 | 21 | var ( 22 | paletteExtensions = map[string]bool{".hex": true} 23 | ) 24 | 25 | type Model struct { 26 | FileBrowser browser.Model 27 | 28 | paletteFilepath string 29 | palette palette.Model 30 | 31 | IsSelected bool // true if we've selected something (ie. render w/ loader) 32 | ShouldUnfocus bool 33 | 34 | width int 35 | } 36 | 37 | func New(w int) Model { 38 | fileBrowser := browser.New(paletteExtensions, w-2) 39 | 40 | return Model{ 41 | FileBrowser: fileBrowser, 42 | IsSelected: false, 43 | ShouldUnfocus: false, 44 | width: w, 45 | } 46 | } 47 | 48 | func (m Model) Init() tea.Cmd { 49 | return nil 50 | } 51 | 52 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 53 | var cmd tea.Cmd 54 | 55 | m.FileBrowser, cmd = m.FileBrowser.Update(msg) 56 | 57 | if m.FileBrowser.ActiveFile != m.paletteFilepath { 58 | m.paletteFilepath = m.FileBrowser.ActiveFile 59 | 60 | name := strings.Split(filepath.Base(m.paletteFilepath), ".hex")[0] 61 | colors, err := parsePaletteFile(m.paletteFilepath) 62 | if err != nil { 63 | return m, tea.Batch(cmd, event.BuildDisplayCmd("error parsing paletteFilepath file")) 64 | } 65 | m.palette = palette.New(name, colors, m.width-5, 3) 66 | 67 | m.IsSelected = true 68 | return m, tea.Batch(cmd, event.StartRenderToViewCmd) 69 | } 70 | 71 | if m.FileBrowser.ShouldClose { 72 | m.IsSelected = false 73 | m.FileBrowser.ShouldClose = false 74 | m.ShouldUnfocus = true 75 | } 76 | 77 | return m, cmd 78 | } 79 | 80 | func (m Model) View() string { 81 | activePreview := style.DimmedTitle.Render("No palette selected") 82 | if len(m.palette.Colors()) != 0 { 83 | activePreview = m.palette.View() 84 | } 85 | activePreview = lipgloss.NewStyle().Padding(0, 0, 1, 2).Render(activePreview) 86 | 87 | title := m.drawTitle() 88 | browser := m.FileBrowser.View() 89 | return lipgloss.JoinVertical(lipgloss.Top, title, browser, activePreview) 90 | } 91 | 92 | func (m Model) GetCurrent() palette.Model { 93 | return m.palette 94 | } 95 | 96 | func parsePaletteFile(filepath string) (color.Palette, error) { 97 | readFile, err := os.Open(filepath) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | fileScanner := bufio.NewScanner(readFile) 103 | fileScanner.Split(bufio.ScanLines) 104 | 105 | var col colorful.Color 106 | p := make(color.Palette, 0, 256) 107 | 108 | for fileScanner.Scan() { 109 | col, err = colorful.Hex(fmt.Sprintf("#%s", fileScanner.Text())) 110 | if err != nil { 111 | return nil, err 112 | } 113 | p = append(p, col) 114 | } 115 | 116 | return p, nil 117 | } 118 | ``` -------------------------------------------------------------------------------- /controls/settings/palettes/model.go: -------------------------------------------------------------------------------- ```go 1 | package palettes 2 | 3 | import ( 4 | tea "github.com/charmbracelet/bubbletea" 5 | "github.com/charmbracelet/lipgloss" 6 | 7 | "github.com/Zebbeni/ansizalizer/controls/settings/palettes/adaptive" 8 | "github.com/Zebbeni/ansizalizer/controls/settings/palettes/loader" 9 | "github.com/Zebbeni/ansizalizer/controls/settings/palettes/lospec" 10 | "github.com/Zebbeni/ansizalizer/palette" 11 | ) 12 | 13 | type State int 14 | 15 | // None consists of a few different components that are shown or hidden 16 | // depending on which toggles have been set on / off. The Model state indicates 17 | // which component is currently focused. From top to bottom the components are: 18 | 19 | // 1) Limited (on/off) 20 | // 2) Loader (Name) (if Limited) -> [Enter] displays Loader menu 21 | // 3) Dithering (on/off) (if Limited) 22 | // 4) Serpentine (on/off) (if Dithering) 23 | // 5) Matrix (Name) (if Dithering) -> [Enter] displays to Matrix menu 24 | 25 | // These can all be part of a single list, but we need to onSelect the list items 26 | 27 | const ( 28 | Adapt State = iota 29 | Load 30 | Lospec 31 | AdaptiveControls 32 | LoadControls 33 | LospecControls 34 | ) 35 | 36 | type Model struct { 37 | selected State 38 | focus State // the component taking input 39 | controls State 40 | 41 | Adapter adaptive.Model 42 | Loader loader.Model 43 | Lospec lospec.Model 44 | 45 | ShouldClose bool 46 | 47 | IsActive bool 48 | 49 | width int 50 | } 51 | 52 | func New(w int) Model { 53 | m := Model{ 54 | selected: Load, 55 | focus: Load, 56 | controls: Load, 57 | Adapter: adaptive.New(w), 58 | Loader: loader.New(w), 59 | Lospec: lospec.New(w), 60 | ShouldClose: false, 61 | IsActive: false, 62 | width: w, 63 | } 64 | return m 65 | } 66 | 67 | func (m Model) Init() tea.Cmd { 68 | return nil 69 | } 70 | 71 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 72 | switch m.focus { 73 | case AdaptiveControls: 74 | return m.handleAdaptiveUpdate(msg) 75 | case LoadControls: 76 | return m.handleLoaderUpdate(msg) 77 | case LospecControls: 78 | return m.handleLospecUpdate(msg) 79 | } 80 | return m.handleMenuUpdate(msg) 81 | } 82 | 83 | func (m Model) View() string { 84 | buttons := m.drawButtons() 85 | if m.IsActive == false { 86 | return buttons 87 | } 88 | 89 | var controls string 90 | switch m.controls { 91 | case Adapt: 92 | controls = m.Adapter.View() 93 | case Load: 94 | controls = m.Loader.View() 95 | case Lospec: 96 | controls = m.Lospec.View() 97 | } 98 | if len(controls) == 0 { 99 | return buttons 100 | } 101 | 102 | return lipgloss.JoinVertical(lipgloss.Top, buttons, controls) 103 | } 104 | 105 | func (m Model) IsAdaptive() bool { 106 | return m.selected == Adapt 107 | } 108 | 109 | func (m Model) IsPaletted() bool { 110 | return m.selected == Load 111 | } 112 | 113 | func (m Model) GetCurrentPalette() palette.Model { 114 | switch m.selected { 115 | case Load: 116 | return m.Loader.GetCurrent() 117 | case Adapt: 118 | return m.Adapter.GetCurrent() 119 | case Lospec: 120 | return m.Lospec.GetCurrent() 121 | } 122 | return palette.Model{} 123 | } 124 | ``` -------------------------------------------------------------------------------- /controls/settings/update.go: -------------------------------------------------------------------------------- ```go 1 | package settings 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/key" 5 | tea "github.com/charmbracelet/bubbletea" 6 | 7 | "github.com/Zebbeni/ansizalizer/event" 8 | ) 9 | 10 | type Direction int 11 | 12 | const ( 13 | Down Direction = iota 14 | Up 15 | ) 16 | 17 | var navMap = map[Direction]map[State]State{ 18 | Down: {Colors: Characters, Characters: Size, Size: Advanced}, 19 | Up: {Advanced: Size, Size: Characters, Characters: Colors}, 20 | } 21 | 22 | func (m Model) handleSettingsUpdate(msg tea.Msg) (Model, tea.Cmd) { 23 | if keyMsg, ok := msg.(tea.KeyMsg); ok { 24 | return m.handleKeyMsg(keyMsg) 25 | } 26 | return m, nil 27 | } 28 | 29 | func (m Model) handleColorsUpdate(msg tea.Msg) (Model, tea.Cmd) { 30 | var cmd tea.Cmd 31 | m.Colors, cmd = m.Colors.Update(msg) 32 | 33 | if m.Colors.ShouldClose { 34 | m.active = None 35 | m.Colors.IsActive = false 36 | m.Colors.ShouldClose = false 37 | } 38 | return m, cmd 39 | } 40 | 41 | func (m Model) handleCharactersUpdate(msg tea.Msg) (Model, tea.Cmd) { 42 | var cmd tea.Cmd 43 | m.Characters, cmd = m.Characters.Update(msg) 44 | 45 | if m.Characters.ShouldClose { 46 | m.active = None 47 | m.Characters.IsActive = false 48 | m.Characters.ShouldClose = false 49 | } 50 | return m, cmd 51 | } 52 | 53 | func (m Model) handleSizeUpdate(msg tea.Msg) (Model, tea.Cmd) { 54 | var cmd tea.Cmd 55 | m.Size, cmd = m.Size.Update(msg) 56 | if m.Size.ShouldClose { 57 | m.active = None 58 | m.Size.IsActive = false 59 | m.Size.ShouldClose = false 60 | } 61 | if m.Size.ShouldUnfocus { 62 | return m.handleSettingsUpdate(msg) 63 | } 64 | return m, cmd 65 | } 66 | 67 | func (m Model) handleAdvancedUpdate(msg tea.Msg) (Model, tea.Cmd) { 68 | var cmd tea.Cmd 69 | m.Advanced, cmd = m.Advanced.Update(msg) 70 | 71 | if m.Advanced.ShouldClose { 72 | m.active = None 73 | m.Advanced.ShouldClose = false 74 | } 75 | return m, cmd 76 | } 77 | 78 | func (m Model) handleEnter() (Model, tea.Cmd) { 79 | m.active = m.focus 80 | switch m.active { 81 | case Colors: 82 | m.Colors.IsActive = true 83 | case Characters: 84 | m.Characters.IsActive = true 85 | case Size: 86 | m.Size.IsActive = true 87 | case Advanced: 88 | m.Advanced.IsActive = true 89 | } 90 | return m, nil 91 | } 92 | 93 | func (m Model) handleEsc() (Model, tea.Cmd) { 94 | m.ShouldClose = true 95 | return m, nil 96 | } 97 | 98 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { 99 | switch { 100 | case key.Matches(msg, event.KeyMap.Down): 101 | if next, hasNext := navMap[Down][m.focus]; hasNext { 102 | m.focus = next 103 | } 104 | case key.Matches(msg, event.KeyMap.Up): 105 | if next, hasNext := navMap[Up][m.focus]; hasNext { 106 | m.focus = next 107 | } else { 108 | m.ShouldClose = true 109 | } 110 | } 111 | return m, nil 112 | } 113 | 114 | func (m Model) handleKeyMsg(msg tea.KeyMsg) (Model, tea.Cmd) { 115 | var cmd tea.Cmd 116 | switch { 117 | case key.Matches(msg, event.KeyMap.Enter): 118 | return m.handleEnter() 119 | case key.Matches(msg, event.KeyMap.Nav): 120 | return m.handleNav(msg) 121 | case key.Matches(msg, event.KeyMap.Esc): 122 | return m.handleEsc() 123 | } 124 | return m, cmd 125 | } 126 | ``` -------------------------------------------------------------------------------- /style/box.go: -------------------------------------------------------------------------------- ```go 1 | package style 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/charmbracelet/lipgloss" 7 | ) 8 | 9 | type BoxWithLabel struct { 10 | BoxStyle lipgloss.Style 11 | LabelStyle lipgloss.Style 12 | } 13 | 14 | func NewDefaultBoxWithLabel() BoxWithLabel { 15 | return BoxWithLabel{ 16 | BoxStyle: lipgloss.NewStyle(). 17 | Border(lipgloss.RoundedBorder()). 18 | BorderForeground(lipgloss.Color("63")), 19 | 20 | // You could, of course, also set background and foreground colors here 21 | // as well. 22 | LabelStyle: lipgloss.NewStyle(). 23 | AlignHorizontal(lipgloss.Center). 24 | PaddingTop(0). 25 | PaddingBottom(0), 26 | } 27 | } 28 | 29 | func (b BoxWithLabel) Render(label, content string, width int) string { 30 | var ( 31 | // Query the box style for some of its border properties so we can 32 | // essentially take the top border apart and put it around the label. 33 | border lipgloss.Border = b.BoxStyle.GetBorderStyle() 34 | topBorderStyler func(string) string = lipgloss.NewStyle().Foreground(b.BoxStyle.GetBorderTopForeground()).Render 35 | bottomBorderStyler func(string) string = lipgloss.NewStyle().Foreground(b.BoxStyle.GetBorderBottomForeground()).Render 36 | topLeft string = topBorderStyler(border.TopLeft) 37 | topRight string = topBorderStyler(border.TopRight) 38 | botLeft string = bottomBorderStyler(border.BottomLeft) 39 | botRight string = bottomBorderStyler(border.BottomRight) 40 | 41 | renderedLabel string = b.LabelStyle.Render(label) 42 | ) 43 | 44 | // Render top row with the label 45 | borderWidth := b.BoxStyle.GetHorizontalBorderSize() 46 | cellsShort := max(0, width+borderWidth-lipgloss.Width(topLeft+topRight+renderedLabel)) 47 | 48 | gap := strings.Repeat(border.Top, cellsShort) 49 | var gapLeft, gapRight string 50 | switch b.LabelStyle.GetAlignHorizontal() { 51 | case lipgloss.Left: 52 | gapRight = gap 53 | case lipgloss.Right: 54 | gapLeft = gap 55 | case lipgloss.Center: 56 | gapLeft = strings.Repeat(border.Top, cellsShort/2) 57 | gapRight = strings.Repeat(border.Top, cellsShort-(cellsShort/2)) 58 | } 59 | 60 | var top, bottom string 61 | 62 | switch b.LabelStyle.GetAlignVertical() { 63 | case lipgloss.Top: 64 | strings.Repeat(border.Top, cellsShort) 65 | top = topLeft + topBorderStyler(gapLeft) + renderedLabel + topBorderStyler(gapRight) + topRight 66 | bottom = b.BoxStyle.Copy(). 67 | BorderTop(false). 68 | Width(width). 69 | Render(content) 70 | case lipgloss.Bottom: 71 | strings.Repeat(border.Bottom, cellsShort) 72 | bottom = botLeft + bottomBorderStyler(gapLeft) + renderedLabel + bottomBorderStyler(gapRight) + botRight 73 | top = b.BoxStyle.Copy(). 74 | BorderBottom(false). 75 | Width(width). 76 | Render(content) 77 | } 78 | 79 | // Stack the pieces 80 | return top + "\n" + bottom 81 | } 82 | 83 | func max(a, b int) int { 84 | if a > b { 85 | return a 86 | } 87 | return b 88 | } 89 | ``` -------------------------------------------------------------------------------- /controls/settings/advanced/dithering/update.go: -------------------------------------------------------------------------------- ```go 1 | package dithering 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/key" 5 | tea "github.com/charmbracelet/bubbletea" 6 | 7 | "github.com/Zebbeni/ansizalizer/event" 8 | ) 9 | 10 | type Direction int 11 | 12 | const ( 13 | Left Direction = iota 14 | Right 15 | Up 16 | Down 17 | ) 18 | 19 | var navMap = map[Direction]map[State]State{ 20 | Right: { 21 | DitherOn: DitherOff, 22 | SerpentineOn: SerpentineOff, 23 | }, 24 | Left: { 25 | DitherOff: DitherOn, 26 | SerpentineOff: SerpentineOn, 27 | }, 28 | Down: { 29 | DitherOn: SerpentineOn, 30 | DitherOff: SerpentineOff, 31 | SerpentineOn: Matrix, 32 | SerpentineOff: Matrix, 33 | }, 34 | Up: { 35 | SerpentineOn: DitherOn, 36 | SerpentineOff: DitherOff, 37 | Matrix: SerpentineOn, 38 | }, 39 | } 40 | 41 | func (m Model) handleMatrixListUpdate(msg tea.Msg) (Model, tea.Cmd) { 42 | if keyMsg, ok := msg.(tea.KeyMsg); ok { 43 | switch { 44 | case key.Matches(keyMsg, event.KeyMap.Up) && m.list.Index() == 0: 45 | return m.handleNav(keyMsg) 46 | case key.Matches(keyMsg, event.KeyMap.Esc): 47 | case key.Matches(keyMsg, event.KeyMap.Enter): 48 | var cmd tea.Cmd 49 | m, cmd = m.setFocus(navMap[Up][Matrix]) 50 | return m, tea.Batch(cmd, event.StartRenderToViewCmd) 51 | } 52 | } 53 | 54 | var cmd tea.Cmd 55 | m.list, cmd = m.list.Update(msg) 56 | return m, cmd 57 | } 58 | 59 | func (m Model) handleEsc() (Model, tea.Cmd) { 60 | m.ShouldClose = true 61 | return m, nil 62 | } 63 | 64 | func (m Model) handleEnter() (Model, tea.Cmd) { 65 | switch m.focus { 66 | case DitherOn: 67 | m.doDithering = true 68 | case DitherOff: 69 | m.doDithering = false 70 | case SerpentineOn: 71 | m.doSerpentine = true 72 | case SerpentineOff: 73 | m.doSerpentine = false 74 | } 75 | return m, event.StartRenderToViewCmd 76 | } 77 | 78 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { 79 | var cmd tea.Cmd 80 | switch { 81 | case key.Matches(msg, event.KeyMap.Right): 82 | if next, hasNext := navMap[Right][m.focus]; hasNext { 83 | return m.setFocus(next) 84 | } 85 | case key.Matches(msg, event.KeyMap.Left): 86 | if next, hasNext := navMap[Left][m.focus]; hasNext { 87 | return m.setFocus(next) 88 | } 89 | case key.Matches(msg, event.KeyMap.Up): 90 | if next, hasNext := navMap[Up][m.focus]; hasNext { 91 | return m.setFocus(next) 92 | } else { 93 | m.ShouldClose = true 94 | } 95 | case key.Matches(msg, event.KeyMap.Down): 96 | if next, hasNext := navMap[Down][m.focus]; hasNext { 97 | return m.setFocus(next) 98 | } else { 99 | m.ShouldClose = true 100 | } 101 | } 102 | return m, cmd 103 | } 104 | 105 | func (m Model) handleKeyMsg(msg tea.KeyMsg) (Model, tea.Cmd) { 106 | var cmd tea.Cmd 107 | switch { 108 | case key.Matches(msg, event.KeyMap.Enter): 109 | return m.handleEnter() 110 | case key.Matches(msg, event.KeyMap.Nav): 111 | return m.handleNav(msg) 112 | case key.Matches(msg, event.KeyMap.Esc): 113 | return m.handleEsc() 114 | } 115 | return m, cmd 116 | } 117 | 118 | func (m Model) setFocus(focus State) (Model, tea.Cmd) { 119 | m.focus = focus 120 | if focus != Matrix { 121 | m.list.SetDelegate(NewDelegate(false)) 122 | } else { 123 | m.list.SetDelegate(NewDelegate(true)) 124 | } 125 | 126 | return m, nil 127 | } 128 | ``` -------------------------------------------------------------------------------- /app/export.go: -------------------------------------------------------------------------------- ```go 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/Zebbeni/ansizalizer/global" 10 | ) 11 | 12 | const ( 13 | maxExportJobs = 1000 14 | ) 15 | 16 | type exportJob struct { 17 | sourcePath string 18 | destinationPath string 19 | } 20 | 21 | type MaxExportQueueError struct { 22 | count int 23 | } 24 | 25 | func (r *MaxExportQueueError) Error() string { 26 | return fmt.Sprintf("%d+ export jobs exceed %d max", r.count, maxExportJobs) 27 | } 28 | 29 | // this process may get more complicated if we want to do animated gifs, 30 | // since each gif will require multiple image exports. 31 | func buildExportQueue(dirPath, destPath string, useSubDirs bool) ([]exportJob, error) { 32 | // for each image file found in the dirPath, append an exportJob object 33 | // with the source filepath and its corresponding .ansi destination filepath 34 | entries, err := os.ReadDir(dirPath) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | exportJobs := make([]exportJob, 0, len(entries)) 40 | subDirs := make([]string, 0, len(entries)) 41 | 42 | for _, e := range entries { 43 | sourcePath := filepath.Join(dirPath, e.Name()) 44 | 45 | if e.IsDir() { 46 | subDirs = append(subDirs, sourcePath) 47 | continue 48 | } 49 | 50 | ext := filepath.Ext(e.Name()) 51 | if _, ok := global.ImgExtensions[ext]; ok { 52 | nameWithoutExt := strings.Split(filepath.Base(sourcePath), ".")[0] 53 | nameWithExt := fmt.Sprintf("%s.ansi", nameWithoutExt) 54 | destFilePath := filepath.Join(destPath, nameWithExt) 55 | exportJobs = append(exportJobs, exportJob{ 56 | sourcePath: sourcePath, 57 | destinationPath: destFilePath, 58 | }) 59 | } 60 | } 61 | 62 | if useSubDirs { 63 | // call buildExportQueue on each subdirectory in dirPath, creating 64 | // subdirectories in the destination path to mimic the source directory 65 | // structure, and providing these subdirectory paths to the build call as well 66 | for _, subDir := range subDirs { 67 | 68 | subDirName := filepath.Base(subDir) 69 | subDestPath := filepath.Join(destPath, subDirName) 70 | 71 | var subDirExportJobs []exportJob 72 | subDirExportJobs, err = buildExportQueue(subDir, subDestPath, true) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | // append resulting exportJob lists to the main list 78 | exportJobs = append(exportJobs, subDirExportJobs...) 79 | if len(exportJobs) > maxExportJobs { 80 | return nil, &MaxExportQueueError{count: len(exportJobs)} 81 | } 82 | 83 | // skip creating mirrored subdirectories if no files found there 84 | if len(subDirExportJobs) == 0 { 85 | continue 86 | } 87 | 88 | // create the destination folder if it doesn't already exist 89 | // do this after the recursive call to buildExportQueue. Otherwise, 90 | // we can hit an infinite loop where our newly created directories 91 | // get picked up by subsequent buildExportQueue calls, forever. 92 | if _, err = os.Stat(subDestPath); os.IsNotExist(err) { 93 | err = os.MkdirAll(subDestPath, os.ModeDir) 94 | if err != nil { 95 | return nil, err 96 | } 97 | } 98 | } 99 | } 100 | 101 | return exportJobs, nil 102 | } 103 | ``` -------------------------------------------------------------------------------- /app/process/custom.go: -------------------------------------------------------------------------------- ```go 1 | package process 2 | 3 | import ( 4 | "image" 5 | "math" 6 | 7 | "github.com/charmbracelet/lipgloss" 8 | "github.com/lucasb-eyer/go-colorful" 9 | "github.com/makeworld-the-better-one/dither/v2" 10 | "github.com/nfnt/resize" 11 | 12 | "github.com/Zebbeni/ansizalizer/controls/settings/characters" 13 | "github.com/Zebbeni/ansizalizer/controls/settings/size" 14 | ) 15 | 16 | func (m Renderer) processCustom(input image.Image) string { 17 | imgW, imgH := float32(input.Bounds().Dx()), float32(input.Bounds().Dy()) 18 | 19 | dimensionType, width, height, charRatio := m.Settings.Size.Info() 20 | if dimensionType == size.Fit { 21 | fitHeight := float32(width) * (imgH / imgW) * float32(charRatio) 22 | fitWidth := (float32(height) * (imgW / imgH)) / float32(charRatio) 23 | if fitHeight > float32(height) { 24 | width = int(fitWidth) 25 | } else { 26 | height = int(fitHeight) 27 | } 28 | } 29 | 30 | resizeFunc := m.Settings.Advanced.SamplingFunction() 31 | refImg := resize.Resize(uint(width)*2, uint(height)*2, input, resizeFunc) 32 | 33 | isTrueColor, _, palette := m.Settings.Colors.GetSelected() 34 | isPaletted := !isTrueColor 35 | 36 | doDither, doSerpentine, matrix := m.Settings.Advanced.Dithering() 37 | if doDither && isPaletted { 38 | ditherer := dither.NewDitherer(palette.Colors()) 39 | ditherer.Matrix = matrix 40 | if doSerpentine { 41 | ditherer.Serpentine = true 42 | } 43 | refImg = ditherer.Dither(refImg) 44 | } 45 | 46 | _, _, useFgBg, chars := m.Settings.Characters.Selected() 47 | if len(chars) == 0 { 48 | return "Enter at least one custom character" 49 | } 50 | 51 | content := "" 52 | rows := make([]string, height) 53 | row := make([]string, width) 54 | 55 | for y := 0; y < height*2; y += 2 { 56 | for x := 0; x < width*2; x += 2 { 57 | r1, _ := colorful.MakeColor(refImg.At(x, y)) 58 | r2, _ := colorful.MakeColor(refImg.At(x+1, y)) 59 | r3, _ := colorful.MakeColor(refImg.At(x, y+1)) 60 | r4, _ := colorful.MakeColor(refImg.At(x+1, y+1)) 61 | 62 | if useFgBg == characters.TwoColor { 63 | fg, bg, brightness := m.fgBgBrightness(r1, r2, r3, r4) 64 | 65 | lipFg := lipgloss.Color(fg.Hex()) 66 | lipBg := lipgloss.Color(bg.Hex()) 67 | style := lipgloss.NewStyle().Foreground(lipFg).Background(lipBg).Bold(true) 68 | 69 | index := min(int(brightness*float64(len(chars))), len(chars)-1) 70 | char := chars[index] 71 | charString := string(char) 72 | 73 | row[x/2] = style.Render(charString) 74 | } else { 75 | fg := m.avgColTrue(r1, r2, r3, r4) 76 | brightness := math.Min(1.0, math.Abs(fg.DistanceLuv(black))) 77 | if isPaletted { 78 | fg, _ = colorful.MakeColor(palette.Colors().Convert(fg)) 79 | } 80 | lipFg := lipgloss.Color(fg.Hex()) 81 | style := lipgloss.NewStyle().Foreground(lipFg).Bold(true) 82 | index := min(int(brightness*float64(len(chars))), len(chars)-1) 83 | char := chars[index] 84 | charString := string(char) 85 | row[x/2] = style.Render(charString) 86 | } 87 | } 88 | rows[y/2] = lipgloss.JoinHorizontal(lipgloss.Top, row...) 89 | } 90 | content += lipgloss.JoinVertical(lipgloss.Left, rows...) 91 | return content 92 | } 93 | 94 | func min(a, b int) int { 95 | if a < b { 96 | return a 97 | } 98 | return b 99 | } 100 | ``` -------------------------------------------------------------------------------- /controls/settings/characters/tabs.go: -------------------------------------------------------------------------------- ```go 1 | package characters 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/charmbracelet/lipgloss" 7 | 8 | "github.com/Zebbeni/ansizalizer/style" 9 | ) 10 | 11 | var ( 12 | inactiveTabBorder = tabBorderWithBottom("┴", "─", "┴") 13 | activeTabBorder = tabBorderWithBottom("┘", " ", "└") 14 | docStyle = lipgloss.NewStyle().Padding(0) 15 | inactiveTabStyle = lipgloss.NewStyle().Border(inactiveTabBorder, true) 16 | activeTabStyle = lipgloss.NewStyle().Border(activeTabBorder, true) 17 | focusTabStyle = activeTabStyle.Copy().BorderForeground(style.SelectedColor1) 18 | windowStyle = lipgloss.NewStyle().Align(lipgloss.Center).Border(lipgloss.NormalBorder()).UnsetBorderTop().Padding(1, 0) 19 | ) 20 | 21 | func (m Model) drawCharTabs() string { 22 | doc := strings.Builder{} 23 | var renderedTabs []string 24 | tabs := []State{Ascii, Unicode, Custom} 25 | 26 | borderColor := style.DimmedColor2 27 | if m.IsActive { 28 | borderColor = style.NormalColor1 29 | } 30 | 31 | for i, t := range tabs { 32 | var tabStyle lipgloss.Style 33 | 34 | isFirst := i == 0 35 | isLast := i == len(tabs)-1 36 | isActive := m.focus == t 37 | showControls := m.charControls == t 38 | 39 | fgColor := style.DimmedColor2 40 | if m.IsActive { 41 | if isActive { 42 | fgColor = style.SelectedColor1 43 | } else { 44 | fgColor = style.DimmedColor1 45 | } 46 | } else { 47 | if isActive { 48 | fgColor = style.NormalColor2 49 | } 50 | } 51 | 52 | if showControls { 53 | tabStyle = activeTabStyle.Copy() 54 | } else { 55 | tabStyle = inactiveTabStyle.Copy() 56 | } 57 | 58 | border, _, _, _, _ := tabStyle.GetBorder() 59 | if isFirst && showControls { 60 | border.BottomLeft = "│" 61 | } else if isFirst && !showControls { 62 | border.BottomLeft = "├" 63 | } else if isLast && showControls { 64 | border.BottomRight = "└" 65 | } else if isLast && !showControls { 66 | border.BottomRight = "┴" 67 | } 68 | 69 | tabStyle = tabStyle.Border(border).BorderForeground(borderColor).Foreground(fgColor) 70 | renderedTabs = append(renderedTabs, tabStyle.Render(stateNames[t])) 71 | } 72 | 73 | tabBlock := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...) 74 | extW, extH := max(m.width-lipgloss.Width(tabBlock)-2, 0), 1 75 | 76 | border := lipgloss.Border{BottomLeft: "─", Bottom: "─", BottomRight: "┐"} 77 | 78 | extendedStyle := windowStyle.Copy().Border(border).BorderForeground(borderColor).Padding(0) 79 | extended := extendedStyle.Copy().Width(extW).Height(extH).Render("") 80 | renderedTabs = append(renderedTabs, extended) 81 | 82 | row := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...) 83 | doc.WriteString(row) 84 | doc.WriteString("\n") 85 | 86 | charButtons := m.drawCharControls() 87 | doc.WriteString(windowStyle.Copy().BorderForeground(borderColor).Width(lipgloss.Width(row) - windowStyle.GetHorizontalFrameSize()).Render(charButtons)) 88 | return docStyle.Render(doc.String()) 89 | } 90 | 91 | func max(a, b int) int { 92 | if a > b { 93 | return a 94 | } 95 | return b 96 | } 97 | 98 | func min(a, b int) int { 99 | if a < b { 100 | return a 101 | } 102 | return b 103 | } 104 | 105 | func tabBorderWithBottom(left, middle, right string) lipgloss.Border { 106 | border := lipgloss.RoundedBorder() 107 | border.BottomLeft = left 108 | border.Bottom = middle 109 | border.BottomRight = right 110 | return border 111 | } 112 | ``` -------------------------------------------------------------------------------- /controls/settings/size/view.go: -------------------------------------------------------------------------------- ```go 1 | package size 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/cursor" 5 | "github.com/charmbracelet/lipgloss" 6 | ) 7 | 8 | var ( 9 | stateOrder = []State{FitButton, StretchButton} 10 | stateNames = map[State]string{ 11 | FitButton: "Fit", 12 | StretchButton: "Stretch", 13 | WidthForm: "Width", 14 | HeightForm: "Height", 15 | CharRatioForm: "Char Size Ratio (Width/Height)", 16 | } 17 | 18 | inputStyle = lipgloss.NewStyle().Width(14).AlignHorizontal(lipgloss.Left) 19 | 20 | activeColor = lipgloss.Color("#aaaaaa") 21 | focusColor = lipgloss.Color("#ffffff") 22 | normalColor = lipgloss.Color("#555555") 23 | titleStyle = lipgloss.NewStyle(). 24 | Foreground(lipgloss.Color("#888888")) 25 | ) 26 | 27 | func (m Model) drawButtons() string { 28 | buttons := make([]string, len(stateOrder)) 29 | for i, state := range stateOrder { 30 | styleColor := normalColor 31 | if m.IsActive { 32 | if state == m.focus { 33 | styleColor = focusColor 34 | } else if state == m.active { 35 | styleColor = activeColor 36 | } 37 | } 38 | style := lipgloss.NewStyle(). 39 | BorderStyle(lipgloss.RoundedBorder()). 40 | BorderForeground(styleColor). 41 | Foreground(styleColor) 42 | buttons[i] = style.Copy().Width(12).AlignHorizontal(lipgloss.Center).Render(stateNames[state]) 43 | } 44 | return lipgloss.JoinHorizontal(lipgloss.Left, buttons...) 45 | } 46 | 47 | func (m Model) drawSizeForms() string { 48 | prompt, text := m.getInputColors(WidthForm) 49 | m.widthInput.Width = 3 50 | m.widthInput.PromptStyle = m.widthInput.PromptStyle.Copy().Foreground(prompt) 51 | m.heightInput.TextStyle = m.heightInput.TextStyle.Copy().Foreground(text) 52 | if m.widthInput.Focused() { 53 | m.widthInput.Cursor.SetMode(cursor.CursorBlink) 54 | } else { 55 | m.widthInput.Cursor.SetMode(cursor.CursorHide) 56 | } 57 | 58 | prompt, text = m.getInputColors(HeightForm) 59 | m.heightInput.PromptStyle = m.heightInput.PromptStyle.Copy().Foreground(prompt) 60 | m.heightInput.TextStyle = m.heightInput.TextStyle.Copy().Foreground(text) 61 | if m.heightInput.Focused() { 62 | m.heightInput.Cursor.SetMode(cursor.CursorBlink) 63 | } else { 64 | m.heightInput.Cursor.SetMode(cursor.CursorHide) 65 | } 66 | 67 | width := inputStyle.Render(m.widthInput.View()) 68 | height := inputStyle.Render(m.heightInput.View()) 69 | 70 | return lipgloss.JoinHorizontal(lipgloss.Top, width, height) 71 | } 72 | 73 | func (m Model) drawCharRatioForm() string { 74 | prompt, text := m.getInputColors(CharRatioForm) 75 | m.charRatioInput.Width = 30 76 | m.charRatioInput.PromptStyle = m.charRatioInput.PromptStyle.Copy().Width(20).Foreground(prompt) 77 | m.charRatioInput.TextStyle = m.charRatioInput.TextStyle.Copy().Foreground(text) 78 | if m.charRatioInput.Focused() { 79 | m.charRatioInput.Cursor.SetMode(cursor.CursorBlink) 80 | } else { 81 | m.charRatioInput.Cursor.SetMode(cursor.CursorHide) 82 | } 83 | 84 | return inputStyle.Copy().Width(28).AlignHorizontal(lipgloss.Left).PaddingTop(1).Render(m.charRatioInput.View()) 85 | } 86 | 87 | func (m Model) getInputColors(state State) (lipgloss.Color, lipgloss.Color) { 88 | if m.focus == state { 89 | if m.active == state { 90 | return activeColor, focusColor 91 | } else { 92 | return focusColor, activeColor 93 | } 94 | } 95 | return normalColor, normalColor 96 | } 97 | ``` -------------------------------------------------------------------------------- /controls/settings/palettes/adaptive/view.go: -------------------------------------------------------------------------------- ```go 1 | package adaptive 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/cursor" 5 | "github.com/charmbracelet/lipgloss" 6 | 7 | "github.com/Zebbeni/ansizalizer/style" 8 | ) 9 | 10 | var ( 11 | stateOrder = []State{CountForm, IterForm} 12 | stateNames = map[State]string{ 13 | CountForm: "Colors", 14 | IterForm: "Passes", 15 | } 16 | 17 | inputStyle = lipgloss.NewStyle().Width(13).AlignHorizontal(lipgloss.Left) 18 | 19 | activeColor = lipgloss.Color("#aaaaaa") 20 | focusColor = lipgloss.Color("#ffffff") 21 | normalColor = lipgloss.Color("#555555") 22 | titleStyle = lipgloss.NewStyle(). 23 | Foreground(lipgloss.Color("#888888")) 24 | ) 25 | 26 | func (m Model) drawTitle() string { 27 | title := style.DimmedTitle.Copy().Italic(true).Render("Create palette From image") 28 | return lipgloss.NewStyle().Width(m.width).PaddingBottom(1).AlignHorizontal(lipgloss.Center).Render(title) 29 | } 30 | 31 | func (m Model) drawInputs() string { 32 | prompt, placeholder := m.getInputColors(CountForm) 33 | 34 | m.countInput.PromptStyle = m.countInput.PromptStyle.Copy().Foreground(prompt) 35 | m.countInput.PlaceholderStyle = m.countInput.PlaceholderStyle.Copy().Foreground(placeholder) 36 | if m.countInput.Focused() { 37 | m.countInput.Cursor.SetMode(cursor.CursorBlink) 38 | } else { 39 | m.countInput.Cursor.SetMode(cursor.CursorHide) 40 | } 41 | 42 | prompt, placeholder = m.getInputColors(IterForm) 43 | m.iterInput.PromptStyle = m.countInput.PromptStyle.Copy().Foreground(prompt) 44 | m.iterInput.PlaceholderStyle = m.countInput.PlaceholderStyle.Copy().Foreground(placeholder) 45 | if m.iterInput.Focused() { 46 | m.iterInput.Cursor.SetMode(cursor.CursorBlink) 47 | } else { 48 | m.iterInput.Cursor.SetMode(cursor.CursorHide) 49 | } 50 | 51 | countInput := inputStyle.Render(m.countInput.View()) 52 | iterInput := inputStyle.Render(m.iterInput.View()) 53 | 54 | return lipgloss.JoinHorizontal(lipgloss.Top, countInput, iterInput) 55 | } 56 | 57 | func (m Model) drawGenerateButton() string { 58 | styleColor := normalColor 59 | if m.IsActive && m.focus == Generate { 60 | styleColor = focusColor 61 | } else if m.active == Generate { 62 | styleColor = activeColor 63 | } 64 | 65 | style := lipgloss.NewStyle(). 66 | Width(m.width - 4). 67 | AlignHorizontal(lipgloss.Center). 68 | BorderStyle(lipgloss.RoundedBorder()). 69 | BorderForeground(styleColor). 70 | Foreground(styleColor) 71 | 72 | button := style.Render("Generate New") 73 | return lipgloss.NewStyle().Width(m.width - 2).AlignHorizontal(lipgloss.Center).Render(button) 74 | } 75 | 76 | // TODO: This is almost the same as drawGenerateButton. See if we can generalize 77 | func (m Model) drawSaveButton() string { 78 | styleColor := normalColor 79 | if m.IsActive && m.focus == Save { 80 | styleColor = focusColor 81 | } else if m.active == Save { 82 | styleColor = activeColor 83 | } 84 | 85 | style := lipgloss.NewStyle(). 86 | Width(m.width - 4). 87 | AlignHorizontal(lipgloss.Center). 88 | PaddingTop(1). 89 | Foreground(styleColor) 90 | 91 | button := style.Render("Save to .hex File") 92 | return lipgloss.NewStyle().Width(m.width - 2).AlignHorizontal(lipgloss.Center).Render(button) 93 | } 94 | 95 | func (m Model) getInputColors(state State) (lipgloss.Color, lipgloss.Color) { 96 | if m.IsActive { 97 | if m.focus == state { 98 | return focusColor, focusColor 99 | } else if m.active == state { 100 | return activeColor, activeColor 101 | } 102 | } 103 | return normalColor, normalColor 104 | } 105 | ``` -------------------------------------------------------------------------------- /controls/settings/advanced/view.go: -------------------------------------------------------------------------------- ```go 1 | package advanced 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/charmbracelet/lipgloss" 7 | 8 | "github.com/Zebbeni/ansizalizer/style" 9 | ) 10 | 11 | var ( 12 | inactiveTabBorder = tabBorderWithBottom("┴", "─", "┴") 13 | activeTabBorder = tabBorderWithBottom("┘", " ", "└") 14 | docStyle = lipgloss.NewStyle().Padding(0) 15 | inactiveTabStyle = lipgloss.NewStyle().Border(inactiveTabBorder, true) 16 | activeTabStyle = lipgloss.NewStyle().Border(activeTabBorder, true) 17 | focusTabStyle = activeTabStyle.Copy().BorderForeground(style.SelectedColor1) 18 | windowStyle = lipgloss.NewStyle().Align(lipgloss.Left).Border(lipgloss.NormalBorder()).UnsetBorderTop().Padding(1, 0) 19 | stateNames = map[State]string{Sampling: "Sampling", Dithering: "Dithering"} 20 | ) 21 | 22 | func (m Model) drawTabs() string { 23 | doc := strings.Builder{} 24 | var renderedTabs []string 25 | tabs := []State{Sampling, Dithering} 26 | 27 | borderColor := style.DimmedColor2 28 | if m.IsActive { 29 | borderColor = style.NormalColor1 30 | } 31 | 32 | for i, t := range tabs { 33 | var tabStyle lipgloss.Style 34 | isFirst, isLast, isActive, isActiveTab := i == 0, i == len(tabs)-1, m.focus == t, m.activeTab == t 35 | 36 | fgColor := style.DimmedColor2 37 | if m.IsActive { 38 | if isActive { 39 | fgColor = style.SelectedColor1 40 | } else { 41 | fgColor = style.DimmedColor1 42 | } 43 | } else { 44 | if isActive { 45 | fgColor = style.NormalColor2 46 | } 47 | } 48 | 49 | if m.activeTab == t { 50 | tabStyle = activeTabStyle.Copy() 51 | } else { 52 | tabStyle = inactiveTabStyle.Copy() 53 | } 54 | 55 | border, _, _, _, _ := tabStyle.GetBorder() 56 | if isFirst && isActiveTab { 57 | border.BottomLeft = "│" 58 | } else if isFirst && !isActiveTab { 59 | border.BottomLeft = "├" 60 | } else if isLast && isActiveTab { 61 | border.BottomRight = "└" 62 | } else if isLast && !isActiveTab { 63 | border.BottomRight = "┴" 64 | } 65 | 66 | tabStyle = tabStyle.Border(border).BorderForeground(borderColor).Foreground(fgColor) 67 | renderedTabs = append(renderedTabs, tabStyle.Render(stateNames[t])) 68 | } 69 | 70 | tabBlock := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...) 71 | extW, extH := max(m.width-lipgloss.Width(tabBlock)-2, 0), 1 72 | 73 | border := lipgloss.Border{BottomLeft: "─", Bottom: "─", BottomRight: "┐"} 74 | 75 | extendedStyle := windowStyle.Copy().Border(border).BorderForeground(borderColor).Padding(0) 76 | extended := extendedStyle.Copy().Width(extW).Height(extH).Render("") 77 | renderedTabs = append(renderedTabs, extended) 78 | 79 | row := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...) 80 | doc.WriteString(row) 81 | doc.WriteString("\n") 82 | 83 | content := m.drawTabContent() 84 | doc.WriteString(windowStyle.Copy().BorderForeground(borderColor).Width(lipgloss.Width(row) - windowStyle.GetHorizontalFrameSize()).Render(content)) 85 | return docStyle.Render(doc.String()) 86 | } 87 | 88 | func (m Model) drawTabContent() string { 89 | switch m.activeTab { 90 | case Sampling: 91 | return m.sampling.View() 92 | case Dithering: 93 | return m.dithering.View() 94 | } 95 | return "" 96 | } 97 | 98 | func max(a, b int) int { 99 | if a > b { 100 | return a 101 | } 102 | return b 103 | } 104 | 105 | func min(a, b int) int { 106 | if a < b { 107 | return a 108 | } 109 | return b 110 | } 111 | 112 | func tabBorderWithBottom(left, middle, right string) lipgloss.Border { 113 | border := lipgloss.RoundedBorder() 114 | border.BottomLeft = left 115 | border.Bottom = middle 116 | border.BottomRight = right 117 | return border 118 | } 119 | ``` -------------------------------------------------------------------------------- /controls/settings/palettes/adaptive/update.go: -------------------------------------------------------------------------------- ```go 1 | package adaptive 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/charmbracelet/bubbles/key" 10 | tea "github.com/charmbracelet/bubbletea" 11 | 12 | "github.com/Zebbeni/ansizalizer/event" 13 | ) 14 | 15 | type Direction int 16 | 17 | const ( 18 | Left Direction = iota 19 | Right 20 | Up 21 | Down 22 | ) 23 | 24 | var navMap = map[Direction]map[State]State{ 25 | Right: {CountForm: IterForm}, 26 | Left: {IterForm: CountForm}, 27 | Up: {Generate: CountForm, Save: Generate}, 28 | Down: {CountForm: Generate, IterForm: Generate, Generate: Save}, 29 | } 30 | 31 | func (m Model) handleEsc() (Model, tea.Cmd) { 32 | m.ShouldClose = true 33 | m.IsSelected = false 34 | return m, nil 35 | } 36 | 37 | func (m Model) handleEnter() (Model, tea.Cmd) { 38 | m.active = m.focus 39 | m.IsSelected = true 40 | switch m.active { 41 | case CountForm: 42 | m.countInput.Focus() 43 | return m, nil 44 | case IterForm: 45 | m.iterInput.Focus() 46 | return m, nil 47 | case Save: 48 | return m.savePaletteFile() 49 | } 50 | return m, event.StartAdaptingCmd 51 | } 52 | 53 | func (m Model) handleNav(msg tea.KeyMsg) (Model, tea.Cmd) { 54 | var cmd tea.Cmd 55 | switch { 56 | case key.Matches(msg, event.KeyMap.Right): 57 | if next, hasNext := navMap[Right][m.focus]; hasNext { 58 | m.focus = next 59 | } 60 | case key.Matches(msg, event.KeyMap.Left): 61 | if next, hasNext := navMap[Left][m.focus]; hasNext { 62 | m.focus = next 63 | } 64 | case key.Matches(msg, event.KeyMap.Down): 65 | if next, hasNext := navMap[Down][m.focus]; hasNext { 66 | m.focus = next 67 | } else { 68 | m.IsSelected = false 69 | m.ShouldUnfocus = true 70 | } 71 | case key.Matches(msg, event.KeyMap.Up): 72 | if next, hasNext := navMap[Up][m.focus]; hasNext { 73 | m.focus = next 74 | } else { 75 | m.IsSelected = false 76 | m.ShouldUnfocus = true 77 | } 78 | } 79 | 80 | return m, cmd 81 | } 82 | 83 | func (m Model) handleCountUpdate(msg tea.Msg) (Model, tea.Cmd) { 84 | if keyMsg, ok := msg.(tea.KeyMsg); ok { 85 | switch { 86 | case key.Matches(keyMsg, event.KeyMap.Enter): 87 | m.IsSelected = true 88 | m.countInput.Blur() 89 | return m, event.StartAdaptingCmd 90 | case key.Matches(keyMsg, event.KeyMap.Esc): 91 | m.countInput.Blur() 92 | } 93 | } 94 | var cmd tea.Cmd 95 | m.countInput, cmd = m.countInput.Update(msg) 96 | return m, cmd 97 | } 98 | 99 | func (m Model) handleIterUpdate(msg tea.Msg) (Model, tea.Cmd) { 100 | if keyMsg, ok := msg.(tea.KeyMsg); ok { 101 | switch { 102 | case key.Matches(keyMsg, event.KeyMap.Enter): 103 | m.IsSelected = true 104 | m.iterInput.Blur() 105 | return m, event.StartAdaptingCmd 106 | case key.Matches(keyMsg, event.KeyMap.Esc): 107 | m.iterInput.Blur() 108 | } 109 | } 110 | var cmd tea.Cmd 111 | m.iterInput, cmd = m.iterInput.Update(msg) 112 | return m, cmd 113 | } 114 | 115 | func (m Model) savePaletteFile() (Model, tea.Cmd) { 116 | filename := fmt.Sprintf("%s.hex", m.palette.Name()) 117 | 118 | f, err := os.Create(filename) 119 | 120 | if err != nil { 121 | return m, event.BuildDisplayCmd("error saving palette file") 122 | } 123 | 124 | defer f.Close() 125 | 126 | var hexStrings string 127 | 128 | for _, c := range m.palette.Colors() { 129 | hexStrings += hexColor(c) + "\n" 130 | 131 | if err != nil { 132 | return m, event.BuildDisplayCmd("error writing to palette file") 133 | } 134 | } 135 | 136 | _, err = f.WriteString(hexStrings) 137 | 138 | dir, _ := os.Getwd() 139 | msg := fmt.Sprintf("saved %s in /%s", filename, filepath.Base(dir)) 140 | return m, event.BuildDisplayCmd(msg) 141 | } 142 | 143 | func hexColor(c color.Color) string { 144 | rgba := color.RGBAModel.Convert(c).(color.RGBA) 145 | return fmt.Sprintf("%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B) 146 | } 147 | ```