feat(config): add palette anchor setting
- commit
- 7fe026fcd8a58541bff2229ae674e1fec976e0ae
- author
- brice · · 2026-07-11 06:37 UTC
- parent
- c15d164
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +97 -4
M internal/adapters/config/config.go +17 -4
| @@ -31,6 +31,7 @@ func Parse(r io.Reader) (domain.Config, []domain.Warning, error) { | |||
| 31 | 31 | var warnings []domain.Warning | |
| 32 | 32 | seenBindingKeys := make(map[string]bool) | |
| 33 | 33 | seenFloatingKeys := make(map[string]bool) | |
| 34 | + | seenPaletteKeys := make(map[string]bool) | |
| 34 | 35 | ||
| 35 | 36 | scanner := bufio.NewScanner(r) | |
| 36 | 37 | lineNo := 0 | |
| @@ -90,8 +91,20 @@ func Parse(r io.Reader) (domain.Config, []domain.Warning, error) { | |||
| 90 | 91 | interval = domain.MinBarInterval | |
| 91 | 92 | } | |
| 92 | 93 | cfg.Bar.Interval = interval | |
| 94 | + | case key == "palette.anchor": | |
| 95 | + | warnings = warnDuplicateKey(warnings, seenPaletteKeys, key, lineNo) | |
| 96 | + | if strings.EqualFold(strings.TrimSpace(value), "auto") { | |
| 97 | + | cfg.Palette = domain.PaletteConfig{} | |
| 98 | + | continue | |
| 99 | + | } | |
| 100 | + | anchor, ok := domain.ParseAnchor(value) | |
| 101 | + | if !ok { | |
| 102 | + | warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid palette.anchor %q", value)}) | |
| 103 | + | continue | |
| 104 | + | } | |
| 105 | + | cfg.Palette = domain.PaletteConfig{Anchor: anchor, AnchorSet: true} | |
| 93 | 106 | case key == "floating.command": | |
| 94 | - | warnings = warnDuplicateFloatingKey(warnings, seenFloatingKeys, key, lineNo) | |
| 107 | + | warnings = warnDuplicateKey(warnings, seenFloatingKeys, key, lineNo) | |
| 95 | 108 | command, ok := parseBarCommand(value) | |
| 96 | 109 | if !ok { | |
| 97 | 110 | warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid floating.command %q", value)}) | |
| @@ -99,7 +112,7 @@ func Parse(r io.Reader) (domain.Config, []domain.Warning, error) { | |||
| 99 | 112 | } | |
| 100 | 113 | cfg.Floating.Command = command | |
| 101 | 114 | case key == "floating.width": | |
| 102 | - | warnings = warnDuplicateFloatingKey(warnings, seenFloatingKeys, key, lineNo) | |
| 115 | + | warnings = warnDuplicateKey(warnings, seenFloatingKeys, key, lineNo) | |
| 103 | 116 | width, ok := parsePercentage(value) | |
| 104 | 117 | if !ok { | |
| 105 | 118 | warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid floating.width %q", value)}) | |
| @@ -107,7 +120,7 @@ func Parse(r io.Reader) (domain.Config, []domain.Warning, error) { | |||
| 107 | 120 | } | |
| 108 | 121 | cfg.Floating.Width = width | |
| 109 | 122 | case key == "floating.height": | |
| 110 | - | warnings = warnDuplicateFloatingKey(warnings, seenFloatingKeys, key, lineNo) | |
| 123 | + | warnings = warnDuplicateKey(warnings, seenFloatingKeys, key, lineNo) | |
| 111 | 124 | height, ok := parsePercentage(value) | |
| 112 | 125 | if !ok { | |
| 113 | 126 | warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid floating.height %q", value)}) | |
| @@ -217,7 +230,7 @@ func fileStamp(path string) (stamp, error) { | |||
| 217 | 230 | return stamp{modTime: st.ModTime(), size: st.Size(), exists: true}, nil | |
| 218 | 231 | } | |
| 219 | 232 | ||
| 220 | - | func warnDuplicateFloatingKey(warnings []domain.Warning, seen map[string]bool, key string, lineNo int) []domain.Warning { | |
| 233 | + | func warnDuplicateKey(warnings []domain.Warning, seen map[string]bool, key string, lineNo int) []domain.Warning { | |
| 221 | 234 | if seen[key] { | |
| 222 | 235 | warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("duplicate key %q", key)}) | |
| 223 | 236 | } | |
| @@ -31,6 +31,7 @@ func Parse(r io.Reader) (domain.Config, []domain.Warning, error) { | |||||
| 31 | var warnings []domain.Warning | 31 | var warnings []domain.Warning | ||
| 32 | seenBindingKeys := make(map[string]bool) | 32 | seenBindingKeys := make(map[string]bool) | ||
| 33 | seenFloatingKeys := make(map[string]bool) | 33 | seenFloatingKeys := make(map[string]bool) | ||
| 34 | + | seenPaletteKeys := make(map[string]bool) | |||
| 34 | 35 | ||||
| 35 | scanner := bufio.NewScanner(r) | 36 | scanner := bufio.NewScanner(r) | ||
| 36 | lineNo := 0 | 37 | lineNo := 0 | ||
| @@ -90,8 +91,20 @@ func Parse(r io.Reader) (domain.Config, []domain.Warning, error) { | |||||
| 90 | interval = domain.MinBarInterval | 91 | interval = domain.MinBarInterval | ||
| 91 | } | 92 | } | ||
| 92 | cfg.Bar.Interval = interval | 93 | cfg.Bar.Interval = interval | ||
| 94 | + | case key == "palette.anchor": | |||
| 95 | + | warnings = warnDuplicateKey(warnings, seenPaletteKeys, key, lineNo) | |||
| 96 | + | if strings.EqualFold(strings.TrimSpace(value), "auto") { | |||
| 97 | + | cfg.Palette = domain.PaletteConfig{} | |||
| 98 | + | continue | |||
| 99 | + | } | |||
| 100 | + | anchor, ok := domain.ParseAnchor(value) | |||
| 101 | + | if !ok { | |||
| 102 | + | warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid palette.anchor %q", value)}) | |||
| 103 | + | continue | |||
| 104 | + | } | |||
| 105 | + | cfg.Palette = domain.PaletteConfig{Anchor: anchor, AnchorSet: true} | |||
| 93 | case key == "floating.command": | 106 | case key == "floating.command": | ||
| 94 | - | warnings = warnDuplicateFloatingKey(warnings, seenFloatingKeys, key, lineNo) | 107 | + | warnings = warnDuplicateKey(warnings, seenFloatingKeys, key, lineNo) |
| 95 | command, ok := parseBarCommand(value) | 108 | command, ok := parseBarCommand(value) | ||
| 96 | if !ok { | 109 | if !ok { | ||
| 97 | warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid floating.command %q", value)}) | 110 | warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid floating.command %q", value)}) | ||
| @@ -99,7 +112,7 @@ func Parse(r io.Reader) (domain.Config, []domain.Warning, error) { | |||||
| 99 | } | 112 | } | ||
| 100 | cfg.Floating.Command = command | 113 | cfg.Floating.Command = command | ||
| 101 | case key == "floating.width": | 114 | case key == "floating.width": | ||
| 102 | - | warnings = warnDuplicateFloatingKey(warnings, seenFloatingKeys, key, lineNo) | 115 | + | warnings = warnDuplicateKey(warnings, seenFloatingKeys, key, lineNo) |
| 103 | width, ok := parsePercentage(value) | 116 | width, ok := parsePercentage(value) | ||
| 104 | if !ok { | 117 | if !ok { | ||
| 105 | warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid floating.width %q", value)}) | 118 | warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid floating.width %q", value)}) | ||
| @@ -107,7 +120,7 @@ func Parse(r io.Reader) (domain.Config, []domain.Warning, error) { | |||||
| 107 | } | 120 | } | ||
| 108 | cfg.Floating.Width = width | 121 | cfg.Floating.Width = width | ||
| 109 | case key == "floating.height": | 122 | case key == "floating.height": | ||
| 110 | - | warnings = warnDuplicateFloatingKey(warnings, seenFloatingKeys, key, lineNo) | 123 | + | warnings = warnDuplicateKey(warnings, seenFloatingKeys, key, lineNo) |
| 111 | height, ok := parsePercentage(value) | 124 | height, ok := parsePercentage(value) | ||
| 112 | if !ok { | 125 | if !ok { | ||
| 113 | warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid floating.height %q", value)}) | 126 | warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid floating.height %q", value)}) | ||
| @@ -217,7 +230,7 @@ func fileStamp(path string) (stamp, error) { | |||||
| 217 | return stamp{modTime: st.ModTime(), size: st.Size(), exists: true}, nil | 230 | return stamp{modTime: st.ModTime(), size: st.Size(), exists: true}, nil | ||
| 218 | } | 231 | } | ||
| 219 | 232 | ||||
| 220 | - | func warnDuplicateFloatingKey(warnings []domain.Warning, seen map[string]bool, key string, lineNo int) []domain.Warning { | 233 | + | func warnDuplicateKey(warnings []domain.Warning, seen map[string]bool, key string, lineNo int) []domain.Warning { |
| 221 | if seen[key] { | 234 | if seen[key] { | ||
| 222 | warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("duplicate key %q", key)}) | 235 | warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("duplicate key %q", key)}) | ||
| 223 | } | 236 | } | ||
A internal/adapters/config/palette_anchor_test.go +72 -0
| @@ -0,0 +1,72 @@ | |||
| 1 | + | package config | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "os" | |
| 6 | + | "path/filepath" | |
| 7 | + | "strings" | |
| 8 | + | "testing" | |
| 9 | + | "time" | |
| 10 | + | ||
| 11 | + | "github.com/bnema/vev/internal/domain" | |
| 12 | + | "github.com/stretchr/testify/require" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | func TestParsePaletteAnchor(t *testing.T) { | |
| 16 | + | t.Parallel() | |
| 17 | + | ||
| 18 | + | tests := []struct { | |
| 19 | + | name string | |
| 20 | + | input string | |
| 21 | + | want domain.PaletteConfig | |
| 22 | + | warnings []domain.Warning | |
| 23 | + | }{ | |
| 24 | + | {name: "default is auto", want: domain.PaletteConfig{}}, | |
| 25 | + | {name: "explicit auto is case insensitive", input: "palette.anchor = AuTo\n", want: domain.PaletteConfig{}}, | |
| 26 | + | {name: "concrete anchors are trimmed and case insensitive", input: "palette.anchor = CENTER\npalette.anchor = top-left\npalette.anchor = Top\npalette.anchor = TOP-RIGHT\npalette.anchor = left\npalette.anchor = RIGHT\npalette.anchor = bottom-left\npalette.anchor = Bottom\npalette.anchor = BOTTOM-RIGHT \n", want: domain.PaletteConfig{Anchor: domain.AnchorBottomRight, AnchorSet: true}, warnings: []domain.Warning{{Line: 2, Msg: "duplicate key \"palette.anchor\""}, {Line: 3, Msg: "duplicate key \"palette.anchor\""}, {Line: 4, Msg: "duplicate key \"palette.anchor\""}, {Line: 5, Msg: "duplicate key \"palette.anchor\""}, {Line: 6, Msg: "duplicate key \"palette.anchor\""}, {Line: 7, Msg: "duplicate key \"palette.anchor\""}, {Line: 8, Msg: "duplicate key \"palette.anchor\""}, {Line: 9, Msg: "duplicate key \"palette.anchor\""}}}, | |
| 27 | + | {name: "invalid first preserves auto", input: "palette.anchor = diagonal\n", want: domain.PaletteConfig{}, warnings: []domain.Warning{{Line: 1, Msg: "invalid palette.anchor \"diagonal\""}}}, | |
| 28 | + | {name: "valid then invalid preserves valid", input: "palette.anchor = top\npalette.anchor = diagonal\n", want: domain.PaletteConfig{Anchor: domain.AnchorTop, AnchorSet: true}, warnings: []domain.Warning{{Line: 2, Msg: "duplicate key \"palette.anchor\""}, {Line: 2, Msg: "invalid palette.anchor \"diagonal\""}}}, | |
| 29 | + | {name: "explicit then auto clears to auto", input: "palette.anchor = left\npalette.anchor = auto\n", want: domain.PaletteConfig{}, warnings: []domain.Warning{{Line: 2, Msg: "duplicate key \"palette.anchor\""}}}, | |
| 30 | + | } | |
| 31 | + | for _, tt := range tests { | |
| 32 | + | t.Run(tt.name, func(t *testing.T) { | |
| 33 | + | cfg, warnings, err := Parse(strings.NewReader(tt.input)) | |
| 34 | + | require.NoError(t, err) | |
| 35 | + | require.Equal(t, tt.want, cfg.Palette) | |
| 36 | + | require.Equal(t, tt.warnings, warnings) | |
| 37 | + | require.Empty(t, cfg.BindingEntries) | |
| 38 | + | }) | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | func TestLoadMissingPaletteAnchorDefaultsToAuto(t *testing.T) { | |
| 43 | + | t.Parallel() | |
| 44 | + | cfg, warnings, err := Load(filepath.Join(t.TempDir(), "missing")) | |
| 45 | + | require.NoError(t, err) | |
| 46 | + | require.Empty(t, warnings) | |
| 47 | + | require.Equal(t, domain.PaletteConfig{}, cfg.Palette) | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | func TestWatchPaletteAnchorRestoresAutoAfterFileRemoval(t *testing.T) { | |
| 51 | + | path := filepath.Join(t.TempDir(), "config") | |
| 52 | + | require.NoError(t, os.WriteFile(path, []byte("palette.anchor = top-right\n"), 0o600)) | |
| 53 | + | clk := newFakeClock(time.Unix(0, 0)) | |
| 54 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 55 | + | defer cancel() | |
| 56 | + | changes := make(chan domain.Config, 2) | |
| 57 | + | done := make(chan error, 1) | |
| 58 | + | go func() { done <- Watch(ctx, clk, path, func(cfg domain.Config, _ []domain.Warning) { changes <- cfg }) }() | |
| 59 | + | clk.waitForTimers(t, 1) | |
| 60 | + | ||
| 61 | + | require.NoError(t, os.WriteFile(path, []byte("palette.anchor = bottom-left\n"), 0o600)) | |
| 62 | + | clk.advance(2 * time.Second) | |
| 63 | + | require.Equal(t, domain.AnchorBottomLeft, (<-changes).Palette.Anchor) | |
| 64 | + | ||
| 65 | + | require.NoError(t, os.Remove(path)) | |
| 66 | + | clk.waitForTimers(t, 1) | |
| 67 | + | clk.advance(2 * time.Second) | |
| 68 | + | got := <-changes | |
| 69 | + | require.Equal(t, domain.PaletteConfig{}, got.Palette) | |
| 70 | + | cancel() | |
| 71 | + | require.ErrorIs(t, <-done, context.Canceled) | |
| 72 | + | } | |
| @@ -0,0 +1,72 @@ | |||||
| 1 | + | package config | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "os" | |||
| 6 | + | "path/filepath" | |||
| 7 | + | "strings" | |||
| 8 | + | "testing" | |||
| 9 | + | "time" | |||
| 10 | + | ||||
| 11 | + | "github.com/bnema/vev/internal/domain" | |||
| 12 | + | "github.com/stretchr/testify/require" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | func TestParsePaletteAnchor(t *testing.T) { | |||
| 16 | + | t.Parallel() | |||
| 17 | + | ||||
| 18 | + | tests := []struct { | |||
| 19 | + | name string | |||
| 20 | + | input string | |||
| 21 | + | want domain.PaletteConfig | |||
| 22 | + | warnings []domain.Warning | |||
| 23 | + | }{ | |||
| 24 | + | {name: "default is auto", want: domain.PaletteConfig{}}, | |||
| 25 | + | {name: "explicit auto is case insensitive", input: "palette.anchor = AuTo\n", want: domain.PaletteConfig{}}, | |||
| 26 | + | {name: "concrete anchors are trimmed and case insensitive", input: "palette.anchor = CENTER\npalette.anchor = top-left\npalette.anchor = Top\npalette.anchor = TOP-RIGHT\npalette.anchor = left\npalette.anchor = RIGHT\npalette.anchor = bottom-left\npalette.anchor = Bottom\npalette.anchor = BOTTOM-RIGHT \n", want: domain.PaletteConfig{Anchor: domain.AnchorBottomRight, AnchorSet: true}, warnings: []domain.Warning{{Line: 2, Msg: "duplicate key \"palette.anchor\""}, {Line: 3, Msg: "duplicate key \"palette.anchor\""}, {Line: 4, Msg: "duplicate key \"palette.anchor\""}, {Line: 5, Msg: "duplicate key \"palette.anchor\""}, {Line: 6, Msg: "duplicate key \"palette.anchor\""}, {Line: 7, Msg: "duplicate key \"palette.anchor\""}, {Line: 8, Msg: "duplicate key \"palette.anchor\""}, {Line: 9, Msg: "duplicate key \"palette.anchor\""}}}, | |||
| 27 | + | {name: "invalid first preserves auto", input: "palette.anchor = diagonal\n", want: domain.PaletteConfig{}, warnings: []domain.Warning{{Line: 1, Msg: "invalid palette.anchor \"diagonal\""}}}, | |||
| 28 | + | {name: "valid then invalid preserves valid", input: "palette.anchor = top\npalette.anchor = diagonal\n", want: domain.PaletteConfig{Anchor: domain.AnchorTop, AnchorSet: true}, warnings: []domain.Warning{{Line: 2, Msg: "duplicate key \"palette.anchor\""}, {Line: 2, Msg: "invalid palette.anchor \"diagonal\""}}}, | |||
| 29 | + | {name: "explicit then auto clears to auto", input: "palette.anchor = left\npalette.anchor = auto\n", want: domain.PaletteConfig{}, warnings: []domain.Warning{{Line: 2, Msg: "duplicate key \"palette.anchor\""}}}, | |||
| 30 | + | } | |||
| 31 | + | for _, tt := range tests { | |||
| 32 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 33 | + | cfg, warnings, err := Parse(strings.NewReader(tt.input)) | |||
| 34 | + | require.NoError(t, err) | |||
| 35 | + | require.Equal(t, tt.want, cfg.Palette) | |||
| 36 | + | require.Equal(t, tt.warnings, warnings) | |||
| 37 | + | require.Empty(t, cfg.BindingEntries) | |||
| 38 | + | }) | |||
| 39 | + | } | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | func TestLoadMissingPaletteAnchorDefaultsToAuto(t *testing.T) { | |||
| 43 | + | t.Parallel() | |||
| 44 | + | cfg, warnings, err := Load(filepath.Join(t.TempDir(), "missing")) | |||
| 45 | + | require.NoError(t, err) | |||
| 46 | + | require.Empty(t, warnings) | |||
| 47 | + | require.Equal(t, domain.PaletteConfig{}, cfg.Palette) | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | func TestWatchPaletteAnchorRestoresAutoAfterFileRemoval(t *testing.T) { | |||
| 51 | + | path := filepath.Join(t.TempDir(), "config") | |||
| 52 | + | require.NoError(t, os.WriteFile(path, []byte("palette.anchor = top-right\n"), 0o600)) | |||
| 53 | + | clk := newFakeClock(time.Unix(0, 0)) | |||
| 54 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 55 | + | defer cancel() | |||
| 56 | + | changes := make(chan domain.Config, 2) | |||
| 57 | + | done := make(chan error, 1) | |||
| 58 | + | go func() { done <- Watch(ctx, clk, path, func(cfg domain.Config, _ []domain.Warning) { changes <- cfg }) }() | |||
| 59 | + | clk.waitForTimers(t, 1) | |||
| 60 | + | ||||
| 61 | + | require.NoError(t, os.WriteFile(path, []byte("palette.anchor = bottom-left\n"), 0o600)) | |||
| 62 | + | clk.advance(2 * time.Second) | |||
| 63 | + | require.Equal(t, domain.AnchorBottomLeft, (<-changes).Palette.Anchor) | |||
| 64 | + | ||||
| 65 | + | require.NoError(t, os.Remove(path)) | |||
| 66 | + | clk.waitForTimers(t, 1) | |||
| 67 | + | clk.advance(2 * time.Second) | |||
| 68 | + | got := <-changes | |||
| 69 | + | require.Equal(t, domain.PaletteConfig{}, got.Palette) | |||
| 70 | + | cancel() | |||
| 71 | + | require.ErrorIs(t, <-done, context.Canceled) | |||
| 72 | + | } | |||
M internal/domain/config.go +8 -0
| @@ -47,6 +47,13 @@ type BarConfig struct { | |||
| 47 | 47 | Interval time.Duration | |
| 48 | 48 | } | |
| 49 | 49 | ||
| 50 | + | // PaletteConfig contains palette placement settings. A false AnchorSet uses the | |
| 51 | + | // adaptive automatic placement. | |
| 52 | + | type PaletteConfig struct { | |
| 53 | + | Anchor Anchor | |
| 54 | + | AnchorSet bool | |
| 55 | + | } | |
| 56 | + | ||
| 50 | 57 | // FloatingConfig contains launch and sizing settings for per-tab floating panes. | |
| 51 | 58 | type FloatingConfig struct { | |
| 52 | 59 | Command string | |
| @@ -63,6 +70,7 @@ type Config struct { | |||
| 63 | 70 | BindingEntries []ConfigEntry | |
| 64 | 71 | Codes map[string]string | |
| 65 | 72 | Snapshot SnapshotConfig | |
| 73 | + | Palette PaletteConfig | |
| 66 | 74 | Floating FloatingConfig | |
| 67 | 75 | } | |
| 68 | 76 | ||
| @@ -47,6 +47,13 @@ type BarConfig struct { | |||||
| 47 | Interval time.Duration | 47 | Interval time.Duration | ||
| 48 | } | 48 | } | ||
| 49 | 49 | ||||
| 50 | + | // PaletteConfig contains palette placement settings. A false AnchorSet uses the | |||
| 51 | + | // adaptive automatic placement. | |||
| 52 | + | type PaletteConfig struct { | |||
| 53 | + | Anchor Anchor | |||
| 54 | + | AnchorSet bool | |||
| 55 | + | } | |||
| 56 | + | ||||
| 50 | // FloatingConfig contains launch and sizing settings for per-tab floating panes. | 57 | // FloatingConfig contains launch and sizing settings for per-tab floating panes. | ||
| 51 | type FloatingConfig struct { | 58 | type FloatingConfig struct { | ||
| 52 | Command string | 59 | Command string | ||
| @@ -63,6 +70,7 @@ type Config struct { | |||||
| 63 | BindingEntries []ConfigEntry | 70 | BindingEntries []ConfigEntry | ||
| 64 | Codes map[string]string | 71 | Codes map[string]string | ||
| 65 | Snapshot SnapshotConfig | 72 | Snapshot SnapshotConfig | ||
| 73 | + | Palette PaletteConfig | |||
| 66 | Floating FloatingConfig | 74 | Floating FloatingConfig | ||
| 67 | } | 75 | } | ||
| 68 | 76 | ||||