refactor(color): remove unused ColorCapabilities.Colors, pulseStyle wrapper and exported ReducedColor; document drawClientToast as info-only
- commit
- 34bf70956b09e9132f26c2ae495389db46d299df
- author
- brice · · 2026-10-03 14:04 UTC
- committer
- brice · · 2026-10-03 14:14 UTC
- parent
- 7ac2f1b
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email - refs
- feat/color-capabilities
6 files · +25 -39
M internal/domain/terminalcap/capabilities.go +0 -14
| @@ -34,20 +34,6 @@ func (c ColorCapabilities) Valid() bool { return c.Mode.Valid() && c.Source.Vali | |||
| 34 | 34 | // RGB reports whether the attachment can receive RGB ANSI output. | |
| 35 | 35 | func (c ColorCapabilities) RGB() bool { return c.Mode == TrueColor } | |
| 36 | 36 | ||
| 37 | - | // Colors reports the number of colors the attachment can display. | |
| 38 | - | func (c ColorCapabilities) Colors() int { | |
| 39 | - | switch c.Mode { | |
| 40 | - | case ANSI256: | |
| 41 | - | return 256 | |
| 42 | - | case ANSI16: | |
| 43 | - | return 16 | |
| 44 | - | case Monochrome: | |
| 45 | - | return 0 | |
| 46 | - | default: | |
| 47 | - | return 1 << 24 | |
| 48 | - | } | |
| 49 | - | } | |
| 50 | - | ||
| 51 | 37 | // Source records how confidently a terminal capability was selected. | |
| 52 | 38 | type Source uint8 | |
| 53 | 39 | ||
| @@ -34,20 +34,6 @@ func (c ColorCapabilities) Valid() bool { return c.Mode.Valid() && c.Source.Vali | |||||
| 34 | // RGB reports whether the attachment can receive RGB ANSI output. | 34 | // RGB reports whether the attachment can receive RGB ANSI output. | ||
| 35 | func (c ColorCapabilities) RGB() bool { return c.Mode == TrueColor } | 35 | func (c ColorCapabilities) RGB() bool { return c.Mode == TrueColor } | ||
| 36 | 36 | ||||
| 37 | - | // Colors reports the number of colors the attachment can display. | |||
| 38 | - | func (c ColorCapabilities) Colors() int { | |||
| 39 | - | switch c.Mode { | |||
| 40 | - | case ANSI256: | |||
| 41 | - | return 256 | |||
| 42 | - | case ANSI16: | |||
| 43 | - | return 16 | |||
| 44 | - | case Monochrome: | |||
| 45 | - | return 0 | |||
| 46 | - | default: | |||
| 47 | - | return 1 << 24 | |||
| 48 | - | } | |||
| 49 | - | } | |||
| 50 | - | ||||
| 51 | // Source records how confidently a terminal capability was selected. | 37 | // Source records how confidently a terminal capability was selected. | ||
| 52 | type Source uint8 | 38 | type Source uint8 | ||
| 53 | 39 | ||||
M internal/domain/terminalcap/capabilities_test.go +8 -10
| @@ -8,21 +8,19 @@ import ( | |||
| 8 | 8 | ||
| 9 | 9 | func TestColorCapabilities(t *testing.T) { | |
| 10 | 10 | tests := []struct { | |
| 11 | - | name string | |
| 12 | - | in ColorCapabilities | |
| 13 | - | wantRGB bool | |
| 14 | - | wantColors int | |
| 11 | + | name string | |
| 12 | + | in ColorCapabilities | |
| 13 | + | wantRGB bool | |
| 15 | 14 | }{ | |
| 16 | - | {name: "zero value is truecolor", in: ColorCapabilities{}, wantRGB: true, wantColors: 16777216}, | |
| 17 | - | {name: "truecolor", in: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, wantRGB: true, wantColors: 16777216}, | |
| 18 | - | {name: "ansi256", in: ColorCapabilities{Mode: ANSI256}, wantRGB: false, wantColors: 256}, | |
| 19 | - | {name: "ansi16", in: ColorCapabilities{Mode: ANSI16, Source: SourceForced}, wantRGB: false, wantColors: 16}, | |
| 20 | - | {name: "monochrome", in: ColorCapabilities{Mode: Monochrome, Source: SourceForced}, wantRGB: false, wantColors: 0}, | |
| 15 | + | {name: "zero value is truecolor", in: ColorCapabilities{}, wantRGB: true}, | |
| 16 | + | {name: "truecolor", in: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, wantRGB: true}, | |
| 17 | + | {name: "ansi256", in: ColorCapabilities{Mode: ANSI256}, wantRGB: false}, | |
| 18 | + | {name: "ansi16", in: ColorCapabilities{Mode: ANSI16, Source: SourceForced}, wantRGB: false}, | |
| 19 | + | {name: "monochrome", in: ColorCapabilities{Mode: Monochrome, Source: SourceForced}, wantRGB: false}, | |
| 21 | 20 | } | |
| 22 | 21 | for _, tt := range tests { | |
| 23 | 22 | t.Run(tt.name, func(t *testing.T) { | |
| 24 | 23 | require.Equal(t, tt.wantRGB, tt.in.RGB()) | |
| 25 | - | require.Equal(t, tt.wantColors, tt.in.Colors()) | |
| 26 | 24 | }) | |
| 27 | 25 | } | |
| 28 | 26 | } | |
| @@ -8,21 +8,19 @@ import ( | |||||
| 8 | 8 | ||||
| 9 | func TestColorCapabilities(t *testing.T) { | 9 | func TestColorCapabilities(t *testing.T) { | ||
| 10 | tests := []struct { | 10 | tests := []struct { | ||
| 11 | - | name string | 11 | + | name string |
| 12 | - | in ColorCapabilities | 12 | + | in ColorCapabilities |
| 13 | - | wantRGB bool | 13 | + | wantRGB bool |
| 14 | - | wantColors int | |||
| 15 | }{ | 14 | }{ | ||
| 16 | - | {name: "zero value is truecolor", in: ColorCapabilities{}, wantRGB: true, wantColors: 16777216}, | 15 | + | {name: "zero value is truecolor", in: ColorCapabilities{}, wantRGB: true}, |
| 17 | - | {name: "truecolor", in: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, wantRGB: true, wantColors: 16777216}, | 16 | + | {name: "truecolor", in: ColorCapabilities{Mode: TrueColor, Source: SourceDeclared}, wantRGB: true}, |
| 18 | - | {name: "ansi256", in: ColorCapabilities{Mode: ANSI256}, wantRGB: false, wantColors: 256}, | 17 | + | {name: "ansi256", in: ColorCapabilities{Mode: ANSI256}, wantRGB: false}, |
| 19 | - | {name: "ansi16", in: ColorCapabilities{Mode: ANSI16, Source: SourceForced}, wantRGB: false, wantColors: 16}, | 18 | + | {name: "ansi16", in: ColorCapabilities{Mode: ANSI16, Source: SourceForced}, wantRGB: false}, |
| 20 | - | {name: "monochrome", in: ColorCapabilities{Mode: Monochrome, Source: SourceForced}, wantRGB: false, wantColors: 0}, | 19 | + | {name: "monochrome", in: ColorCapabilities{Mode: Monochrome, Source: SourceForced}, wantRGB: false}, |
| 21 | } | 20 | } | ||
| 22 | for _, tt := range tests { | 21 | for _, tt := range tests { | ||
| 23 | t.Run(tt.name, func(t *testing.T) { | 22 | t.Run(tt.name, func(t *testing.T) { | ||
| 24 | require.Equal(t, tt.wantRGB, tt.in.RGB()) | 23 | require.Equal(t, tt.wantRGB, tt.in.RGB()) | ||
| 25 | - | require.Equal(t, tt.wantColors, tt.in.Colors()) | |||
| 26 | }) | 24 | }) | ||
| 27 | } | 25 | } | ||
| 28 | } | 26 | } | ||
M internal/usecase/client/client_toast.go +6 -0
| @@ -17,6 +17,12 @@ import ( | |||
| 17 | 17 | // reconciliation; input pumps must only publish a request for it. | |
| 18 | 18 | // The anchor places it: transitions stay centered, notices sit top-right so | |
| 19 | 19 | // they never cover the picker list. The border is colored by severity. | |
| 20 | + | // | |
| 21 | + | // The border uses fixed xterm-256 colors and does not follow the attachment | |
| 22 | + | // color profile, so production callers (the transition notice) pass only | |
| 23 | + | // domain.NoticeInfo, which emits no color. Toasts with a severity that must | |
| 24 | + | // honor 16-color or monochrome terminals go through toastBorderSGRFor, as the | |
| 25 | + | // picker does. | |
| 20 | 26 | func drawClientToast(out io.Writer, size domain.Size, message string, anchor domain.Anchor, severity domain.NoticeSeverity) (domain.Rect, error) { | |
| 21 | 27 | bounds := ui.ToastBounds(size, ui.Toast{Message: message, Anchor: anchor}) | |
| 22 | 28 | if bounds.Width <= 0 || bounds.Height <= 0 { | |
| @@ -17,6 +17,12 @@ import ( | |||||
| 17 | // reconciliation; input pumps must only publish a request for it. | 17 | // reconciliation; input pumps must only publish a request for it. | ||
| 18 | // The anchor places it: transitions stay centered, notices sit top-right so | 18 | // The anchor places it: transitions stay centered, notices sit top-right so | ||
| 19 | // they never cover the picker list. The border is colored by severity. | 19 | // they never cover the picker list. The border is colored by severity. | ||
| 20 | + | // | |||
| 21 | + | // The border uses fixed xterm-256 colors and does not follow the attachment | |||
| 22 | + | // color profile, so production callers (the transition notice) pass only | |||
| 23 | + | // domain.NoticeInfo, which emits no color. Toasts with a severity that must | |||
| 24 | + | // honor 16-color or monochrome terminals go through toastBorderSGRFor, as the | |||
| 25 | + | // picker does. | |||
| 20 | func drawClientToast(out io.Writer, size domain.Size, message string, anchor domain.Anchor, severity domain.NoticeSeverity) (domain.Rect, error) { | 26 | func drawClientToast(out io.Writer, size domain.Size, message string, anchor domain.Anchor, severity domain.NoticeSeverity) (domain.Rect, error) { | ||
| 21 | bounds := ui.ToastBounds(size, ui.Toast{Message: message, Anchor: anchor}) | 27 | bounds := ui.ToastBounds(size, ui.Toast{Message: message, Anchor: anchor}) | ||
| 22 | if bounds.Width <= 0 || bounds.Height <= 0 { | 28 | if bounds.Width <= 0 || bounds.Height <= 0 { | ||
M internal/usecase/daemon/status.go +3 -7
| @@ -27,16 +27,12 @@ func pulseVisible(frame int) bool { | |||
| 27 | 27 | return frame%pulseFrameCount != 0 | |
| 28 | 28 | } | |
| 29 | 29 | ||
| 30 | - | // pulseStyle returns the style for the attention bell glyph at frame, built | |
| 30 | + | // pulseStyleFor returns the style for the attention bell glyph at frame, built | |
| 31 | 31 | // on top of base so the bell always keeps the caller's background (the tab's | |
| 32 | 32 | // accent/statusBar, or a faded MRU entry's blended colors) instead of | |
| 33 | 33 | // punching a default-background hole in a themed bar. On the invisible beat | |
| 34 | - | // it returns (base, false) unchanged. | |
| 35 | - | func pulseStyle(frame int, base renderer.Style) (renderer.Style, bool) { | |
| 36 | - | return pulseStyleFor(frame, base, false) | |
| 37 | - | } | |
| 38 | - | ||
| 39 | - | // pulseStyleFor is pulseStyle for chrome built by the reduced-color policy. | |
| 34 | + | // it returns (base, false) unchanged. reduced selects the attribute-only pulse | |
| 35 | + | // for chrome built by the reduced-color policy. | |
| 40 | 36 | func pulseStyleFor(frame int, base renderer.Style, reduced bool) (renderer.Style, bool) { | |
| 41 | 37 | if !pulseVisible(frame) { | |
| 42 | 38 | return base, false | |
| @@ -27,16 +27,12 @@ func pulseVisible(frame int) bool { | |||||
| 27 | return frame%pulseFrameCount != 0 | 27 | return frame%pulseFrameCount != 0 | ||
| 28 | } | 28 | } | ||
| 29 | 29 | ||||
| 30 | - | // pulseStyle returns the style for the attention bell glyph at frame, built | 30 | + | // pulseStyleFor returns the style for the attention bell glyph at frame, built |
| 31 | // on top of base so the bell always keeps the caller's background (the tab's | 31 | // on top of base so the bell always keeps the caller's background (the tab's | ||
| 32 | // accent/statusBar, or a faded MRU entry's blended colors) instead of | 32 | // accent/statusBar, or a faded MRU entry's blended colors) instead of | ||
| 33 | // punching a default-background hole in a themed bar. On the invisible beat | 33 | // punching a default-background hole in a themed bar. On the invisible beat | ||
| 34 | - | // it returns (base, false) unchanged. | 34 | + | // it returns (base, false) unchanged. reduced selects the attribute-only pulse |
| 35 | - | func pulseStyle(frame int, base renderer.Style) (renderer.Style, bool) { | 35 | + | // for chrome built by the reduced-color policy. |
| 36 | - | return pulseStyleFor(frame, base, false) | |||
| 37 | - | } | |||
| 38 | - | ||||
| 39 | - | // pulseStyleFor is pulseStyle for chrome built by the reduced-color policy. | |||
| 40 | func pulseStyleFor(frame int, base renderer.Style, reduced bool) (renderer.Style, bool) { | 36 | func pulseStyleFor(frame int, base renderer.Style, reduced bool) (renderer.Style, bool) { | ||
| 41 | if !pulseVisible(frame) { | 37 | if !pulseVisible(frame) { | ||
| 42 | return base, false | 38 | return base, false | ||
M internal/usecase/daemon/status_test.go +5 -5
| @@ -885,32 +885,32 @@ func TestPulseStyleFadesFromBaseAndHidesAtFrameZero(t *testing.T) { | |||
| 885 | 885 | ||
| 886 | 886 | t.Run("invisible frame returns base unchanged", func(t *testing.T) { | |
| 887 | 887 | for _, base := range []renderer.Style{rgbBase, indexedBase} { | |
| 888 | - | style, visible := pulseStyle(0, base) | |
| 888 | + | style, visible := pulseStyleFor(0, base, false) | |
| 889 | 889 | require.False(t, visible) | |
| 890 | 890 | require.True(t, style.Equal(base)) | |
| 891 | 891 | } | |
| 892 | 892 | }) | |
| 893 | 893 | ||
| 894 | 894 | t.Run("rgb base blends the glyph foreground from the base background to its foreground, keeping the base background", func(t *testing.T) { | |
| 895 | - | low, visible := pulseStyle(1, rgbBase) | |
| 895 | + | low, visible := pulseStyleFor(1, rgbBase, false) | |
| 896 | 896 | require.True(t, visible) | |
| 897 | 897 | require.True(t, low.Bold) | |
| 898 | 898 | require.True(t, low.HasBackgroundRGB) | |
| 899 | 899 | require.Equal(t, rgbBase.BackgroundRGB, low.BackgroundRGB, "bell keeps the caller's background so it never punches a hole in a themed bar") | |
| 900 | 900 | ||
| 901 | - | peak, visible := pulseStyle(pulseFrameCount/2, rgbBase) | |
| 901 | + | peak, visible := pulseStyleFor(pulseFrameCount/2, rgbBase, false) | |
| 902 | 902 | require.True(t, visible) | |
| 903 | 903 | require.Equal(t, rgbBase.ForegroundRGB, peak.ForegroundRGB, "peak intensity reaches the base foreground exactly") | |
| 904 | 904 | require.NotEqual(t, low.ForegroundRGB, peak.ForegroundRGB, "the glyph should ramp, not jump straight to full intensity") | |
| 905 | 905 | }) | |
| 906 | 906 | ||
| 907 | 907 | t.Run("non-RGB base falls back to the indexed grey ramp and preserves other base attributes", func(t *testing.T) { | |
| 908 | - | low, visible := pulseStyle(1, indexedBase) | |
| 908 | + | low, visible := pulseStyleFor(1, indexedBase, false) | |
| 909 | 909 | require.True(t, visible) | |
| 910 | 910 | require.True(t, low.Bold) | |
| 911 | 911 | require.True(t, low.Inverse, "non-color base attributes like inverse must survive") | |
| 912 | 912 | ||
| 913 | - | peak, visible := pulseStyle(pulseFrameCount/2, indexedBase) | |
| 913 | + | peak, visible := pulseStyleFor(pulseFrameCount/2, indexedBase, false) | |
| 914 | 914 | require.True(t, visible) | |
| 915 | 915 | require.Greater(t, peak.Foreground, low.Foreground) | |
| 916 | 916 | }) | |
| @@ -885,32 +885,32 @@ func TestPulseStyleFadesFromBaseAndHidesAtFrameZero(t *testing.T) { | |||||
| 885 | 885 | ||||
| 886 | t.Run("invisible frame returns base unchanged", func(t *testing.T) { | 886 | t.Run("invisible frame returns base unchanged", func(t *testing.T) { | ||
| 887 | for _, base := range []renderer.Style{rgbBase, indexedBase} { | 887 | for _, base := range []renderer.Style{rgbBase, indexedBase} { | ||
| 888 | - | style, visible := pulseStyle(0, base) | 888 | + | style, visible := pulseStyleFor(0, base, false) |
| 889 | require.False(t, visible) | 889 | require.False(t, visible) | ||
| 890 | require.True(t, style.Equal(base)) | 890 | require.True(t, style.Equal(base)) | ||
| 891 | } | 891 | } | ||
| 892 | }) | 892 | }) | ||
| 893 | 893 | ||||
| 894 | t.Run("rgb base blends the glyph foreground from the base background to its foreground, keeping the base background", func(t *testing.T) { | 894 | t.Run("rgb base blends the glyph foreground from the base background to its foreground, keeping the base background", func(t *testing.T) { | ||
| 895 | - | low, visible := pulseStyle(1, rgbBase) | 895 | + | low, visible := pulseStyleFor(1, rgbBase, false) |
| 896 | require.True(t, visible) | 896 | require.True(t, visible) | ||
| 897 | require.True(t, low.Bold) | 897 | require.True(t, low.Bold) | ||
| 898 | require.True(t, low.HasBackgroundRGB) | 898 | require.True(t, low.HasBackgroundRGB) | ||
| 899 | require.Equal(t, rgbBase.BackgroundRGB, low.BackgroundRGB, "bell keeps the caller's background so it never punches a hole in a themed bar") | 899 | require.Equal(t, rgbBase.BackgroundRGB, low.BackgroundRGB, "bell keeps the caller's background so it never punches a hole in a themed bar") | ||
| 900 | 900 | ||||
| 901 | - | peak, visible := pulseStyle(pulseFrameCount/2, rgbBase) | 901 | + | peak, visible := pulseStyleFor(pulseFrameCount/2, rgbBase, false) |
| 902 | require.True(t, visible) | 902 | require.True(t, visible) | ||
| 903 | require.Equal(t, rgbBase.ForegroundRGB, peak.ForegroundRGB, "peak intensity reaches the base foreground exactly") | 903 | require.Equal(t, rgbBase.ForegroundRGB, peak.ForegroundRGB, "peak intensity reaches the base foreground exactly") | ||
| 904 | require.NotEqual(t, low.ForegroundRGB, peak.ForegroundRGB, "the glyph should ramp, not jump straight to full intensity") | 904 | require.NotEqual(t, low.ForegroundRGB, peak.ForegroundRGB, "the glyph should ramp, not jump straight to full intensity") | ||
| 905 | }) | 905 | }) | ||
| 906 | 906 | ||||
| 907 | t.Run("non-RGB base falls back to the indexed grey ramp and preserves other base attributes", func(t *testing.T) { | 907 | t.Run("non-RGB base falls back to the indexed grey ramp and preserves other base attributes", func(t *testing.T) { | ||
| 908 | - | low, visible := pulseStyle(1, indexedBase) | 908 | + | low, visible := pulseStyleFor(1, indexedBase, false) |
| 909 | require.True(t, visible) | 909 | require.True(t, visible) | ||
| 910 | require.True(t, low.Bold) | 910 | require.True(t, low.Bold) | ||
| 911 | require.True(t, low.Inverse, "non-color base attributes like inverse must survive") | 911 | require.True(t, low.Inverse, "non-color base attributes like inverse must survive") | ||
| 912 | 912 | ||||
| 913 | - | peak, visible := pulseStyle(pulseFrameCount/2, indexedBase) | 913 | + | peak, visible := pulseStyleFor(pulseFrameCount/2, indexedBase, false) |
| 914 | require.True(t, visible) | 914 | require.True(t, visible) | ||
| 915 | require.Greater(t, peak.Foreground, low.Foreground) | 915 | require.Greater(t, peak.Foreground, low.Foreground) | ||
| 916 | }) | 916 | }) | ||
M internal/usecase/theme/color_policy.go +3 -3
| @@ -6,10 +6,10 @@ import ( | |||
| 6 | 6 | "github.com/bnema/vev/internal/domain/terminalcap" | |
| 7 | 7 | ) | |
| 8 | 8 | ||
| 9 | - | // ReducedColor reports whether chrome for c must be built from attributes and | |
| 9 | + | // reducedColor reports whether chrome for c must be built from attributes and | |
| 10 | 10 | // terminal palette slots instead of RGB surfaces: 16-color and monochrome | |
| 11 | 11 | // attachments. | |
| 12 | - | func ReducedColor(c terminalcap.ColorCapabilities) bool { | |
| 12 | + | func reducedColor(c terminalcap.ColorCapabilities) bool { | |
| 13 | 13 | return c.Mode == terminalcap.ANSI16 || c.Mode == terminalcap.Monochrome | |
| 14 | 14 | } | |
| 15 | 15 | ||
| @@ -19,7 +19,7 @@ func ReducedColor(c terminalcap.ColorCapabilities) bool { | |||
| 19 | 19 | // and mark the returned Theme so dimming uses the faint attribute instead of | |
| 20 | 20 | // RGB blends. Color is per attachment, so callers resolve once per attachment. | |
| 21 | 21 | func ResolveForColor(t Theme, policy domain.ThemeAccent, color terminalcap.ColorCapabilities) ResolvedTheme { | |
| 22 | - | if !ReducedColor(color) { | |
| 22 | + | if !reducedColor(color) { | |
| 23 | 23 | return Resolve(t, policy) | |
| 24 | 24 | } | |
| 25 | 25 | t.DimByAttribute = true | |
| @@ -6,10 +6,10 @@ import ( | |||||
| 6 | "github.com/bnema/vev/internal/domain/terminalcap" | 6 | "github.com/bnema/vev/internal/domain/terminalcap" | ||
| 7 | ) | 7 | ) | ||
| 8 | 8 | ||||
| 9 | - | // ReducedColor reports whether chrome for c must be built from attributes and | 9 | + | // reducedColor reports whether chrome for c must be built from attributes and |
| 10 | // terminal palette slots instead of RGB surfaces: 16-color and monochrome | 10 | // terminal palette slots instead of RGB surfaces: 16-color and monochrome | ||
| 11 | // attachments. | 11 | // attachments. | ||
| 12 | - | func ReducedColor(c terminalcap.ColorCapabilities) bool { | 12 | + | func reducedColor(c terminalcap.ColorCapabilities) bool { |
| 13 | return c.Mode == terminalcap.ANSI16 || c.Mode == terminalcap.Monochrome | 13 | return c.Mode == terminalcap.ANSI16 || c.Mode == terminalcap.Monochrome | ||
| 14 | } | 14 | } | ||
| 15 | 15 | ||||
| @@ -19,7 +19,7 @@ func ReducedColor(c terminalcap.ColorCapabilities) bool { | |||||
| 19 | // and mark the returned Theme so dimming uses the faint attribute instead of | 19 | // and mark the returned Theme so dimming uses the faint attribute instead of | ||
| 20 | // RGB blends. Color is per attachment, so callers resolve once per attachment. | 20 | // RGB blends. Color is per attachment, so callers resolve once per attachment. | ||
| 21 | func ResolveForColor(t Theme, policy domain.ThemeAccent, color terminalcap.ColorCapabilities) ResolvedTheme { | 21 | func ResolveForColor(t Theme, policy domain.ThemeAccent, color terminalcap.ColorCapabilities) ResolvedTheme { | ||
| 22 | - | if !ReducedColor(color) { | 22 | + | if !reducedColor(color) { |
| 23 | return Resolve(t, policy) | 23 | return Resolve(t, policy) | ||
| 24 | } | 24 | } | ||
| 25 | t.DimByAttribute = true | 25 | t.DimByAttribute = true | ||