@@ -138,143 +138,140 @@ class ThemeManager {
138138 }
139139
140140 static Color getInputBackground () {
141- isDark() ? new Color ( 30 , 30 , 30 ) : Color . WHITE
141+ activeTheme . inputBackground
142142 }
143143
144144 static Color getOutputBackground () {
145- isDark() ? new Color ( 43 , 43 , 43 ) : new Color ( 255 , 255 , 218 )
145+ activeTheme . outputBackground
146146 }
147147
148148 static Map getStyles (String fontFamily ) {
149- isDark() ? getDarkStyles(fontFamily) : getLightStyles( fontFamily)
149+ buildSwingStyles(activeTheme, fontFamily)
150150 }
151151
152- private static Map getLightStyles (String fontFamily ) {
153- [
154- // output window styles
155- regular : [
156- (StyleConstants.FontFamily ): fontFamily,
157- (StyleConstants.Foreground ): Color . BLACK
158- ],
159- prompt : [
160- (StyleConstants.Foreground ): new Color (0 , 128 , 0 )
161- ],
162- command : [
163- (StyleConstants.Foreground ): Color . BLUE
164- ],
165- stacktrace : [
166- (StyleConstants.Foreground ): Color . RED . darker()
167- ],
168- hyperlink : [
169- (StyleConstants.Foreground ): Color . BLUE ,
170- (StyleConstants.Underline ): true
171- ],
172- output : [
173- (StyleConstants.Foreground ): Color . BLACK
174- ],
175- result : [
176- (StyleConstants.Foreground ): Color . BLUE ,
177- (StyleConstants.Background ): Color . YELLOW
178- ],
179-
180- // syntax highlighting styles
181- (StyleContext . DEFAULT_STYLE ): [
182- (StyleConstants.FontFamily ): fontFamily
183- ],
184- (GroovyFilter . COMMENT ): [
185- (StyleConstants.Foreground ): Color . LIGHT_GRAY . darker(). darker(),
186- (StyleConstants.Italic ): true
187- ],
188- (GroovyFilter . QUOTES ): [
189- (StyleConstants.Foreground ): Color . MAGENTA . darker(). darker()
190- ],
191- (GroovyFilter . SINGLE_QUOTES ): [
192- (StyleConstants.Foreground ): Color . GREEN . darker(). darker()
193- ],
194- (GroovyFilter . SLASHY_QUOTES ): [
195- (StyleConstants.Foreground ): Color . ORANGE . darker()
196- ],
197- (GroovyFilter . DIGIT ): [
198- (StyleConstants.Foreground ): Color . RED . darker()
199- ],
200- (GroovyFilter . ANNOTATION ): [
201- (StyleConstants.Foreground ): new Color (128 , 128 , 0 )
202- ],
203- (GroovyFilter . OPERATION ): [
204- (StyleConstants.Bold ): true
205- ],
206- (GroovyFilter . IDENT ): [:],
207- (GroovyFilter . RESERVED_WORD ): [
208- (StyleConstants.Bold ): true ,
209- (StyleConstants.Foreground ): Color . BLUE . darker(). darker()
210- ]
211- ]
152+ // --- theme loading + parsing ---
153+
154+ private static final Map<String , Object > themeCache = [:]
155+
156+ private static Map getActiveTheme () {
157+ loadBundledTheme(isDark() ? ' dark' : ' light' )
158+ }
159+
160+ private static Map loadBundledTheme (String name ) {
161+ themeCache. computeIfAbsent(name) { key ->
162+ def resource = ThemeManager . classLoader. getResourceAsStream(" groovy/console/ui/themes/${ key} .theme" )
163+ if (! resource) {
164+ throw new IllegalStateException (" Missing bundled theme resource: ${ key} .theme" )
165+ }
166+ resource. withStream { stream ->
167+ parseTheme(new InputStreamReader (stream, ' UTF-8' ))
168+ }
169+ }
212170 }
213171
214- private static Map getDarkStyles (String fontFamily ) {
215- [
216- // output window styles
217- regular : [
218- (StyleConstants.FontFamily ): fontFamily,
219- (StyleConstants.Foreground ): new Color (204 , 204 , 204 )
220- ],
221- prompt : [
222- (StyleConstants.Foreground ): new Color (106 , 180 , 101 )
223- ],
224- command : [
225- (StyleConstants.Foreground ): new Color (104 , 151 , 187 )
226- ],
227- stacktrace : [
228- (StyleConstants.Foreground ): new Color (204 , 102 , 102 )
229- ],
230- hyperlink : [
231- (StyleConstants.Foreground ): new Color (104 , 151 , 187 ),
232- (StyleConstants.Underline ): true
233- ],
234- output : [
235- (StyleConstants.Foreground ): new Color (204 , 204 , 204 )
236- ],
237- result : [
238- (StyleConstants.Foreground ): new Color (169 , 183 , 198 ),
239- (StyleConstants.Background ): new Color (50 , 50 , 80 )
240- ],
241-
242- // syntax highlighting styles
243- (StyleContext . DEFAULT_STYLE ): [
244- (StyleConstants.FontFamily ): fontFamily,
245- (StyleConstants.Foreground ): new Color (204 , 204 , 204 )
246- ],
247- (GroovyFilter . COMMENT ): [
248- (StyleConstants.Foreground ): new Color (160 , 160 , 160 ),
249- (StyleConstants.Italic ): true
250- ],
251- (GroovyFilter . QUOTES ): [
252- (StyleConstants.Foreground ): new Color (220 , 175 , 240 )
253- ],
254- (GroovyFilter . SINGLE_QUOTES ): [
255- (StyleConstants.Foreground ): new Color (160 , 225 , 155 )
256- ],
257- (GroovyFilter . SLASHY_QUOTES ): [
258- (StyleConstants.Foreground ): new Color (235 , 190 , 130 )
259- ],
260- (GroovyFilter . DIGIT ): [
261- (StyleConstants.Foreground ): new Color (220 , 150 , 150 )
262- ],
263- (GroovyFilter . ANNOTATION ): [
264- (StyleConstants.Foreground ): new Color (210 , 210 , 130 )
265- ],
266- (GroovyFilter . OPERATION ): [
267- (StyleConstants.Bold ): true ,
268- (StyleConstants.Foreground ): new Color (204 , 204 , 204 )
269- ],
270- (GroovyFilter . IDENT ): [
271- (StyleConstants.Foreground ): new Color (204 , 204 , 204 )
272- ],
273- (GroovyFilter . RESERVED_WORD ): [
274- (StyleConstants.Bold ): true ,
275- (StyleConstants.Foreground ): new Color (180 , 210 , 240 )
276- ]
277- ]
172+ /**
173+ * Parses a .theme file (java.util.Properties format with our value sub-syntax)
174+ * into a structured theme: { inputBackground, outputBackground, styles: name→attrs }.
175+ * Each attrs map may contain foreground/background Colors and bold/italic/underline flags.
176+ * Unknown keys are silently ignored so theme files stay forward-compatible.
177+ */
178+ static Map parseTheme (Reader reader ) {
179+ def props = new Properties ()
180+ props. load(reader)
181+ def result = [inputBackground : null , outputBackground : null , styles : [:]]
182+ props. stringPropertyNames(). each { key ->
183+ def value = props. getProperty(key)?. trim() ?: ' '
184+ switch (key. trim(). toLowerCase()) {
185+ case ' input.background' :
186+ result. inputBackground = parseHexColor(value)
187+ break
188+ case ' output.background' :
189+ result. outputBackground = parseHexColor(value)
190+ break
191+ default :
192+ result. styles[key. trim(). toLowerCase()] = parseStyleValue(value)
193+ }
194+ }
195+ result
196+ }
197+
198+ private static Map parseStyleValue (String raw ) {
199+ def attrs = [:]
200+ if (! raw) return attrs
201+ // split off "<fg> on <bg>"
202+ int onIdx = raw. toLowerCase(). indexOf(' on ' )
203+ String bg = null
204+ if (onIdx >= 0 ) {
205+ bg = raw. substring(onIdx + 4 ). trim()
206+ raw = raw. substring(0 , onIdx). trim()
207+ }
208+ for (String part : raw. split(' ,' )) {
209+ part = part. trim()
210+ if (! part) continue
211+ if (part. startsWith(' #' )) {
212+ attrs. foreground = parseHexColor(part)
213+ } else {
214+ switch (part. toLowerCase()) {
215+ case ' bold' : attrs. bold = true ; break
216+ case ' italic' : attrs. italic = true ; break
217+ case ' underline' : attrs. underline = true ; break
218+ }
219+ }
220+ }
221+ if (bg) attrs. background = parseHexColor(bg)
222+ attrs
223+ }
224+
225+ private static Color parseHexColor (String hex ) {
226+ hex = hex. trim()
227+ if (hex. startsWith(' #' )) hex = hex. substring(1 )
228+ new Color (Integer . parseInt(hex, 16 ))
229+ }
230+
231+ /**
232+ * Converts a parsed theme into the Swing-shaped style map consumed by the
233+ * output area (per-document styles keyed by String) and by the input area's
234+ * syntax highlighter (global StyleContext styles keyed by GroovyFilter
235+ * constants / StyleContext.DEFAULT_STYLE).
236+ */
237+ private static Map buildSwingStyles (Map theme , String fontFamily ) {
238+ def result = [:]
239+ theme. styles. each { String name , Map attrs ->
240+ def key = resolveStyleKey(name)
241+ if (key == null ) return
242+ def styleAttrs = [:]
243+ if (attrs. foreground) styleAttrs[StyleConstants.Foreground ] = attrs. foreground
244+ if (attrs. background) styleAttrs[StyleConstants.Background ] = attrs. background
245+ if (attrs. bold) styleAttrs[StyleConstants.Bold ] = true
246+ if (attrs. italic) styleAttrs[StyleConstants.Italic ] = true
247+ if (attrs. underline) styleAttrs[StyleConstants.Underline ] = true
248+ result[key] = styleAttrs
249+ }
250+ // ensure regular + default carry the user-configured monospaced family
251+ result. computeIfAbsent(' regular' ) { [:] }[StyleConstants.FontFamily ] = fontFamily
252+ result. computeIfAbsent(StyleContext . DEFAULT_STYLE ) { [:] }[StyleConstants.FontFamily ] = fontFamily
253+ result
254+ }
255+
256+ private static Object resolveStyleKey (String name ) {
257+ switch (name) {
258+ // output window styles — literal String keys (per-document)
259+ case ' regular' : case ' prompt' : case ' command' : case ' stacktrace' :
260+ case ' hyperlink' : case ' output' : case ' result' :
261+ return name
262+ // syntax-highlighting styles — global StyleContext keys
263+ case ' default' : return StyleContext . DEFAULT_STYLE
264+ case ' comment' : return GroovyFilter . COMMENT
265+ case ' quotes' : return GroovyFilter . QUOTES
266+ case ' single_quotes' : return GroovyFilter . SINGLE_QUOTES
267+ case ' slashy_quotes' : return GroovyFilter . SLASHY_QUOTES
268+ case ' digit' : return GroovyFilter . DIGIT
269+ case ' annotation' : return GroovyFilter . ANNOTATION
270+ case ' operation' : return GroovyFilter . OPERATION
271+ case ' ident' : return GroovyFilter . IDENT
272+ case ' reserved_word' : return GroovyFilter . RESERVED_WORD
273+ default : return null
274+ }
278275 }
279276
280277 private static boolean probeSystemDarkMode () {
0 commit comments