1+ package com .github .epochcoder .prettyconsole ;
2+
3+ import com .github .epochcoder .prettyconsole .handlers .ConsoleBoxPasswordHandler ;
4+ import com .google .common .base .Function ;
5+ import com .google .common .base .Joiner ;
6+ import com .google .common .base .Preconditions ;
7+ import com .google .common .base .Splitter ;
8+ import com .google .common .base .Strings ;
9+ import com .google .common .collect .Iterables ;
10+ import java .awt .Font ;
11+ import java .awt .FontMetrics ;
12+ import java .awt .Graphics ;
13+ import java .awt .Graphics2D ;
14+ import java .awt .RenderingHints ;
15+ import java .awt .image .BufferedImage ;
16+ import java .io .PrintStream ;
17+ import java .util .ArrayList ;
18+ import java .util .List ;
19+ import java .util .regex .Pattern ;
20+
21+ /**
22+ * represents a console display box in the system console (or any PrintStream).
23+ * used to display friendly technical information
24+ * @author Willie Scholtz
25+ */
26+ public final class ConsoleBox {
27+
28+ /**
29+ * the character to use in box corners
30+ */
31+ private static final String BOX_CHAR = " + " ;
32+
33+ /**
34+ * the character to use to pad strings with
35+ */
36+ private static final char PAD_CHAR = ' ' ;
37+
38+ /**
39+ * the character used for the box's right and left sides
40+ */
41+ private static final String END_CHAR = " | " ;
42+
43+ /**
44+ * the character used for the box's top, title and bottom sides
45+ */
46+ private static final String TB_CHAR = "-" ;
47+
48+ /**
49+ * the character used for representing black in a color
50+ */
51+ private static final String BLACK_CHAR = " " ;
52+
53+ /**
54+ * the character used for representing white in a color
55+ */
56+ private static final String WHITE_CHAR = "#" ;
57+
58+ /**
59+ * the character used for representing aliasing of fonts.
60+ * actually other colors, but since our image is only black and white
61+ * it will represent a shade, which in this case is aliasing
62+ */
63+ private static final String ALIAS_CHAR = " " ;
64+
65+ /**
66+ * the key value separator for names and values
67+ */
68+ private static final String KEY_VALUE_SEP = " : " ;
69+
70+
71+ private final List <ConsoleBoxKeyHandler > handlers ;
72+ private final StringBuilder builder ;
73+ private boolean content ;
74+ private final int width ;
75+
76+ /**
77+ * creates a new instance of a ConsoleBox
78+ * @param boxWidth the width of the box (in character count)
79+ * @param title the initial title of the box, leave blank for none
80+ */
81+ public ConsoleBox (int boxWidth , String title ) {
82+ this .width = boxWidth ;
83+ this .builder = new StringBuilder ();
84+ this .handlers = new ArrayList <ConsoleBoxKeyHandler >();
85+
86+ // add default password handler
87+ this .handlers .add (new ConsoleBoxPasswordHandler ());
88+
89+ if (!Strings .isNullOrEmpty (title )) {
90+ this .title (title );
91+ }
92+ }
93+
94+ /*
95+ * creates a new instance of a ConsoleBox with no title
96+ * @param boxWidth the width of the box (in character count)
97+ */
98+ public ConsoleBox (int boxWidth ) {
99+ this (boxWidth , null );
100+ }
101+
102+ public ConsoleBox handler (ConsoleBoxKeyHandler handler ) {
103+ this .handlers .add (handler );
104+ return this ;
105+ }
106+
107+ /**
108+ * builds and writes this box to the specified output stream
109+ * @param output
110+ */
111+ public void build (PrintStream output ) {
112+ this .title ("" );
113+ output .println (this .builder .toString ());
114+ }
115+
116+ private String padBoth (String string , String pad , int length ) {
117+ int right = (length - string .length ()) / 2 + string .length ();
118+ String result = Strings .padEnd (string , right , pad .toCharArray ()[0 ]);
119+ return Strings .padStart (result , length , pad .toCharArray ()[0 ]);
120+ }
121+
122+ /**
123+ * adds a title section to the console box
124+ * @param title the title to use
125+ * @return the current box
126+ */
127+ public ConsoleBox title (String title ) {
128+ this .builder .append ("\n " + BOX_CHAR ).append (padBoth (title ,
129+ TB_CHAR , this .width )).append (BOX_CHAR );
130+
131+ return this ;
132+ }
133+
134+ /**
135+ * adds an empty line section to the console box
136+ * @return the current box
137+ */
138+ public ConsoleBox empty () {
139+ this .builder .append ("\n " + BOX_CHAR ).append (
140+ padBoth ("" , " " , this .width )).append (BOX_CHAR );
141+
142+ return this ;
143+ }
144+
145+ /**
146+ * generates and writes the specified text as an ASCII image into this box
147+ * @param text the text to write as ASCII
148+ * @param invert should the ASCII colors be inverted?
149+ * @return the current box
150+ */
151+ public ConsoleBox ascii (String text , boolean invert ) {
152+ final BufferedImage image = new BufferedImage (this .width ,
153+ 32 , BufferedImage .TYPE_INT_RGB );
154+
155+ final Graphics graphics = image .getGraphics ();
156+ final Graphics2D g2d = (Graphics2D ) graphics ;
157+
158+ g2d .setRenderingHint (RenderingHints .KEY_TEXT_ANTIALIASING ,
159+ RenderingHints .VALUE_TEXT_ANTIALIAS_ON );
160+
161+ Font textFont = new Font ("Dialog" , Font .BOLD , 22 );
162+ FontMetrics textMetrics = g2d .getFontMetrics (textFont );
163+ g2d .setFont (textFont );
164+
165+ int tX = (image .getWidth () / 2 ) - (textMetrics .stringWidth (text ) / 2 );
166+ int tY = (image .getHeight () / 2 ) + (textMetrics .getHeight () / 2 ) - 5 ;
167+
168+ g2d .drawString (text , tX , tY );
169+ g2d .drawRenderedImage (image , null );
170+ g2d .dispose ();
171+
172+ final int iHeight = image .getHeight ();
173+ final int iWidth = image .getWidth ();
174+
175+ final String bChar = invert ? WHITE_CHAR : BLACK_CHAR ;
176+ final String wChar = invert ? BLACK_CHAR : WHITE_CHAR ;
177+
178+ for (int y = 0 ; y < iHeight ; y ++) {
179+ final StringBuilder sb = new StringBuilder ();
180+ for (int x = 0 ; x < iWidth ; x ++) {
181+ final int rgbColor = image .getRGB (x , y );
182+ sb .append (rgbColor == -16777216 ? bChar : rgbColor == -1 ? wChar : ALIAS_CHAR );
183+ }
184+
185+ if (sb .toString ().trim ().isEmpty ()) {
186+ continue ;
187+ }
188+
189+ this .builder .append ("\n " + END_CHAR )
190+ .append (sb ).append (END_CHAR );
191+ }
192+
193+ return this ;
194+ }
195+
196+ /**
197+ * adds a informational line to the console box,
198+ * automatically splitting large values
199+ * @param key the name of the value to display
200+ * @param value the value of this line
201+ * @return the current box
202+ */
203+ public ConsoleBox line (String key , String value ) {
204+ key = Strings .isNullOrEmpty (key ) ? "null" : key ;
205+ value = Strings .isNullOrEmpty (value ) ? "" : value ;
206+
207+ // get the key length
208+ final int kL = key .length ();
209+ // calculate remaining box space for the value
210+ final int ths = (this .width - kL - KEY_VALUE_SEP .length ());
211+ Preconditions .checkState (ths > -1 , "key[" + key + "] is to long "
212+ + "for box with a " + width + " width!" );
213+
214+ // \n | the_key_length_in_spaces
215+ final String joinOn = ("\n " + END_CHAR + Strings .padEnd ("" ,
216+ kL + KEY_VALUE_SEP .length (), PAD_CHAR ));
217+
218+ // get key handlers and modify if neccessary
219+ for (ConsoleBoxKeyHandler handler : this .handlers ) {
220+ if (handler .shouldHandle (key )) {
221+ value = handler .handleValue (key , value );
222+ // don't break, possibilitty of multiple handlers
223+ }
224+ }
225+
226+ // if a key handler returns null, a key should be skipped
227+ if (value != null ) {
228+ // split the string on either length or new lines
229+ Iterable <String > splitted = Splitter .on (Pattern
230+ .compile ("(?<=\\ G.{" + ths + "})|\\ n" )).split (value );
231+
232+ // add the value + end characters (multiple lines)
233+ String formatted = Joiner .on (joinOn ).join (
234+ Iterables .transform (splitted , new Function <String , String >() {
235+ @ Override
236+ public String apply (String input ) {
237+ return Strings .padEnd (input , ths , ' ' ) + END_CHAR ;
238+ }
239+ }));
240+
241+ // write completed line to builder
242+ this .builder .append ("\n " + END_CHAR ).append (key )
243+ .append (KEY_VALUE_SEP ).append (formatted );
244+
245+ this .content = true ;
246+ }
247+
248+ return this ;
249+ }
250+
251+ /**
252+ * @return true if {@link #line(java.lang.String, java.lang.String)}
253+ * has been called at least once
254+ */
255+ public boolean hasContent () {
256+ return content ;
257+ }
258+ }
0 commit comments