@@ -2,10 +2,15 @@ package box
22
33import (
44 "fmt"
5+ "log"
6+ "os"
57 "strings"
68
79 "github.com/gookit/color"
10+ "github.com/huandu/xstrings"
811 "github.com/mattn/go-runewidth"
12+ "github.com/muesli/reflow/wrap"
13+ "golang.org/x/term"
914)
1015
1116const (
@@ -18,28 +23,32 @@ const (
1823 rightAlign = "%[1]s%[2]s%[4]s%[5]s%[3]s%[6]s%[1]s"
1924)
2025
21- // Box struct defines the Box to be made.
26+ // Box defines the design
2227type Box struct {
23- TopRight string // Symbols used for TopRight Corner
24- TopLeft string // Symbols used for TopLeft Corner
25- Vertical string // Symbols used for Vertical Bars
26- BottomRight string // Symbols used for BottomRight Corner
27- BottomLeft string // Symbols used for BottomRight Corner
28- Horizontal string // Symbols used for Horizontal Bars
29- Config // Config for the Box struct
28+ TopRight string // TopRight Corner Symbols
29+ TopLeft string // TopLeft Corner Symbols
30+ Vertical string // Vertical Bar Symbols
31+ BottomRight string // BottomRight Corner Symbols
32+ BottomLeft string // BottomLeft Corner Symbols
33+ Horizontal string // Horizontal Bars Symbols
34+ Config // Box Config
3035}
3136
32- // Config is the configuration for the Box struct
37+ // Config is the configuration needed for the Box to be designed
3338type Config struct {
34- Py int // Horizontal Padding
35- Px int // Vertical Padding
36- ContentAlign string // Content Alignment inside Box
37- Type string // Type of Box
38- TitlePos string // Title Position
39- Color interface {} // Color of Box
39+ Py int // Horizontal Padding
40+ Px int // Vertical Padding
41+ ContentAlign string // Content Alignment inside Box
42+ Type string // Box Type
43+ TitlePos string // Title Position
44+ TitleColor interface {} // Title Color
45+ ContentColor interface {} // Content Color
46+ Color interface {} // Box Color
47+ AllowWrapping bool // Flag to allow custom Content Wrapping
48+ WrappingLimit int // Wrap the Content upto the Limit
4049}
4150
42- // New takes struct Config and returns the specified Box struct.
51+ // New takes Box Config and returns a Box from the given Config
4352func New (config Config ) Box {
4453 // Default Box Type is Single
4554 if config .Type == "" {
@@ -55,10 +64,29 @@ func New(config Config) Box {
5564 panic ("Invalid Box Type provided" )
5665}
5766
58- // String returns the string representation of Box.
67+ // String returns the string representation of Box
5968func (b Box ) String (title , lines string ) string {
6069 var lines2 []string
6170
71+ // Allow Wrapping according to the user
72+ if b .AllowWrapping {
73+ // If limit not provided then use 2*TermWidth/3 as limit else
74+ // use the one provided
75+ if b .WrappingLimit != 0 {
76+ lines = wrap .String (lines , b .WrappingLimit )
77+ } else {
78+ width , _ , err := term .GetSize (int (os .Stdout .Fd ()))
79+ if err != nil {
80+ log .Fatal (err )
81+ }
82+ lines = wrap .String (lines , 2 * width / 3 )
83+ }
84+ }
85+
86+ // Obtain Title and Content color
87+ title = b .obtainTitleColor (title )
88+ lines = b .obtainContentColor (lines )
89+
6290 // Default Position is Inside, no warning for invalid TitlePos as it is done
6391 // in toString() method
6492 if b .TitlePos == "" {
@@ -78,31 +106,40 @@ func (b Box) String(title, lines string) string {
78106 return b .toString (title , lines2 )
79107}
80108
81- // toString is same as String except that it is used for printing Boxes
109+ // toString is an internal method and same as String method except that the main Box generation is done here
82110func (b Box ) toString (title string , lines []string ) string {
83- titleLen := len (strings .Split (title , n1 ))
111+ titleLen := len (strings .Split (color . ClearCode ( title ) , n1 ))
84112 sideMargin := strings .Repeat (" " , b .Px )
85- longestLine , lines2 := longestLine (lines )
113+ _longestLine , lines2 := longestLine (lines )
86114
87115 // Get padding on one side
88116 paddingCount := b .Px
89117
90- n := longestLine + (paddingCount * 2 ) + 2
118+ n := _longestLine + (paddingCount * 2 ) + 2
91119
92- if b .TitlePos != inside && runewidth .StringWidth (title ) > n - 2 {
120+ if b .TitlePos != inside && runewidth .StringWidth (color . ClearCode ( title ) ) > n - 2 {
93121 panic ("Title must be shorter than the Top & Bottom Bars" )
94122 }
95123
96124 // Create Top and Bottom Bars
97125 Bar := strings .Repeat (b .Horizontal , n - 2 )
98126 TopBar := b .TopLeft + Bar + b .TopRight
99127 BottomBar := b .BottomLeft + Bar + b .BottomRight
100- // Check b.TitlePos
128+
129+ var TitleBar string
130+ // If title has tabs then expand them accordingly.
131+ if strings .Contains (title , "\t " ) {
132+ TitleBar = repeatWithString (b .Horizontal , n - 2 , xstrings .ExpandTabs (title , 4 ))
133+ } else {
134+ TitleBar = repeatWithString (b .Horizontal , n - 2 , title )
135+ }
136+
137+ // Check b.TitlePos if it is not Inside
101138 if b .TitlePos != inside {
102- TitleBar := repeatWithString (b .Horizontal , n - 2 , title )
103139 switch b .TitlePos {
104140 case "Top" :
105141 TopBar = b .TopLeft + TitleBar + b .TopRight
142+ //fmt.Println(TopBar)
106143 case "Bottom" :
107144 BottomBar = b .BottomLeft + TitleBar + b .BottomRight
108145 default :
@@ -115,15 +152,20 @@ func (b Box) toString(title string, lines []string) string {
115152 }
116153inside:
117154 // Check type of b.Color then assign the Colors to TopBar and BottomBar accordingly
118- TopBar , BottomBar = b .checkColorType (TopBar , BottomBar )
155+ // If title has tabs then expand them accordingly.
156+ if strings .Contains (title , "\t " ) {
157+ TopBar , BottomBar = b .checkColorType (TopBar , BottomBar , xstrings .ExpandTabs (title , 4 ))
158+ } else {
159+ TopBar , BottomBar = b .checkColorType (TopBar , BottomBar , title )
160+ }
161+
119162 if b .TitlePos == inside && runewidth .StringWidth (TopBar ) != runewidth .StringWidth (BottomBar ) {
120163 panic ("cannot create a Box with different sizes of Top and Bottom Bars" )
121164 }
122165
123166 // Create lines to print
124167 texts := b .addVertPadding (n )
125- texts = b .formatLine (lines2 , longestLine , titleLen , sideMargin , title , texts )
126-
168+ texts = b .formatLine (lines2 , _longestLine , titleLen , sideMargin , title , texts )
127169 vertpadding := b .addVertPadding (n )
128170 texts = append (texts , vertpadding ... )
129171
@@ -141,8 +183,124 @@ inside:
141183 return sb .String ()
142184}
143185
144- // obtainColor obtains the Color from string, uint and [3]uint respectively
145- func (b Box ) obtainColor () string {
186+ // obtainTitleColor obtains TitleColor from types string, uint and [3]uint respectively
187+ func (b Box ) obtainTitleColor (title string ) string {
188+ if b .TitleColor == nil { // if nil then just return the string
189+ return title
190+ }
191+ // Check if type of b.TitleColor is string
192+ if str , ok := b .TitleColor .(string ); ok {
193+ // Hi Intensity Color
194+ if strings .HasPrefix (str , "Hi" ) {
195+ if _ , ok := fgHiColors [str ]; ok {
196+ // If title has newlines in it then splitting would be needed
197+ // as color won't be applied on all
198+ if strings .Contains (title , "\n " ) {
199+ return b .applyColorToAll (title , str , color.RGBColor {}, false )
200+ }
201+ return addStylePreservingOriginalFormat (title , fgHiColors [str ].Sprint )
202+ }
203+ } else if _ , ok := fgColors [str ]; ok {
204+ // If title has newlines in it then splitting would be needed
205+ // as color won't be applied on all
206+ if strings .Contains (title , "\n " ) {
207+ return b .applyColorToAll (title , str , color.RGBColor {}, false )
208+ }
209+ return addStylePreservingOriginalFormat (title , fgColors [str ].Sprint )
210+ }
211+ // Return a warning as TitleColor provided as a string is unknown and
212+ // return without the color effect
213+ errorMsg ("[warning]: invalid value provided to Color, using default" )
214+ return title
215+
216+ // Check if type of b.TitleColor is uint
217+ } else if hex , ok := b .TitleColor .(uint ); ok {
218+ // Break down the hex into R, G and B respectively
219+ hexArray := [3 ]uint {hex >> 16 , hex >> 8 & 0xff , hex & 0xff }
220+ col := color .RGB (uint8 (hexArray [0 ]), uint8 (hexArray [1 ]), uint8 (hexArray [2 ]))
221+
222+ // If title has newlines in it then splitting would be needed
223+ // as color won't be applied on all
224+ if strings .Contains (title , "\n " ) {
225+ return b .applyColorToAll (title , "" , col , true )
226+ }
227+ return addStylePreservingOriginalFormat (title , col .Sprint )
228+
229+ // Check if type of b.TitleColor is [3]uint
230+ } else if rgb , ok := b .TitleColor .([3 ]uint ); ok {
231+ col := color .RGB (uint8 (rgb [0 ]), uint8 (rgb [1 ]), uint8 (rgb [2 ]))
232+
233+ // If title has newlines in it then splitting would be needed
234+ // as color won't be applied on all
235+ if strings .Contains (title , "\n " ) {
236+ return b .applyColorToAll (title , "" , col , true )
237+ }
238+ return b .roundOffTitleColor (col , addStylePreservingOriginalFormat (title , col .Sprint ))
239+ }
240+ // Panic if b.TitleColor is an unexpected type
241+ panic (fmt .Sprintf ("expected string, [3]uint or uint not %T" , b .TitleColor ))
242+ }
243+
244+ // obtainContentColor obtains ContentColor from types string, uint and [3]uint respectively
245+ func (b Box ) obtainContentColor (content string ) string {
246+ if b .ContentColor == nil { // if nil then just return the string
247+ return content
248+ }
249+ // Check if type of b.ContentColor is string
250+ if str , ok := b .ContentColor .(string ); ok {
251+ // Hi Intensity Color
252+ if strings .HasPrefix (str , "Hi" ) {
253+ if _ , ok := fgHiColors [str ]; ok {
254+ // If Content has newlines in it then splitting would be needed
255+ // as color won't be applied on all
256+ if strings .Contains (content , "\n " ) {
257+ return b .applyColorToAll (content , str , color.RGBColor {}, false )
258+ }
259+ return addStylePreservingOriginalFormat (content , fgHiColors [str ].Sprint )
260+ }
261+ } else if _ , ok := fgColors [str ]; ok {
262+ // If Content has newlines in it then splitting would be needed
263+ // as color won't be applied on all
264+ if strings .Contains (content , "\n " ) {
265+ return b .applyColorToAll (content , str , color.RGBColor {}, false )
266+ }
267+ return addStylePreservingOriginalFormat (content , fgColors [str ].Sprint )
268+ }
269+ // Return a warning as ContentColor provided as a string is unknown and
270+ // return without the color effect
271+ errorMsg ("[warning]: invalid value provided to Color, using default" )
272+ return content
273+
274+ // Check if type of b.ContentColor is uint
275+ } else if hex , ok := b .ContentColor .(uint ); ok {
276+ // Break down the hex into R, G and B respectively
277+ hexArray := [3 ]uint {hex >> 16 , hex >> 8 & 0xff , hex & 0xff }
278+ col := color .RGB (uint8 (hexArray [0 ]), uint8 (hexArray [1 ]), uint8 (hexArray [2 ]))
279+
280+ // If content has newlines in it then splitting would be needed
281+ // as color won't be applied on all
282+ if strings .Contains (content , "\n " ) {
283+ return b .applyColorToAll (content , "" , col , true )
284+ }
285+ return b .roundOffTitleColor (col , content )
286+
287+ // Check if type of b.ContentColor is [3]uint
288+ } else if rgb , ok := b .ContentColor .([3 ]uint ); ok {
289+ col := color .RGB (uint8 (rgb [0 ]), uint8 (rgb [1 ]), uint8 (rgb [2 ]))
290+
291+ // If content has newlines in it then splitting would be needed
292+ // as color won't be applied on all
293+ if strings .Contains (content , "\n " ) {
294+ return b .applyColorToAll (content , "" , col , true )
295+ }
296+ return b .roundOffTitleColor (col , content )
297+ }
298+ // Panic if b.ContentColor is an unexpected type
299+ panic (fmt .Sprintf ("expected string, [3]uint or uint not %T" , b .ContentColor ))
300+ }
301+
302+ // obtainColor obtains BoxColor from types string, uint and [3]uint respectively
303+ func (b Box ) obtainBoxColor () string {
146304 if b .Color == nil { // if nil then just return the string
147305 return b .Vertical
148306 }
@@ -179,6 +337,25 @@ func (b Box) obtainColor() string {
179337func (b Box ) Print (title , lines string ) {
180338 var lines2 []string
181339
340+ // Allow Wrapping according to the user
341+ if b .AllowWrapping {
342+ // If limit not provided then use 2*TermWidth/3 as limit else
343+ // use the one provided
344+ if b .WrappingLimit != 0 {
345+ lines = wrap .String (lines , b .WrappingLimit )
346+ } else {
347+ width , _ , err := term .GetSize (int (os .Stdout .Fd ()))
348+ if err != nil {
349+ log .Fatal (err )
350+ }
351+ lines = wrap .String (lines , 2 * width / 3 )
352+ }
353+ }
354+
355+ // Obtain Title and Content color
356+ title = b .obtainTitleColor (title )
357+ lines = b .obtainContentColor (lines )
358+
182359 // Default Position is Inside, if invalid position is given then just raise a warning
183360 // then use Default Position which is Inside
184361 if b .TitlePos == "" {
@@ -201,10 +378,29 @@ func (b Box) Print(title, lines string) {
201378 color .Print (b .toString (title , lines2 ))
202379}
203380
204- // Println adds a newline before and after the Box
381+ // Println adds a newline before and after printing the Box
205382func (b Box ) Println (title , lines string ) {
206383 var lines2 []string
207384
385+ // Allow Wrapping according to the user
386+ if b .AllowWrapping {
387+ // If limit not provided then use 2*TermWidth/3 as limit else
388+ // use the one provided
389+ if b .WrappingLimit != 0 {
390+ lines = wrap .String (lines , b .WrappingLimit )
391+ } else {
392+ width , _ , err := term .GetSize (int (os .Stdout .Fd ()))
393+ if err != nil {
394+ log .Fatal (err )
395+ }
396+ lines = wrap .String (lines , 2 * width / 3 )
397+ }
398+ }
399+
400+ // Obtain Title and Content color
401+ title = b .obtainTitleColor (title )
402+ lines = b .obtainContentColor (lines )
403+
208404 // Default Position is Inside, if invalid position is given then just raise a warning
209405 // then use Default Position which is Inside
210406 if b .TitlePos == "" {
0 commit comments