Skip to content

Commit e0714c7

Browse files
committed
GROOVY-11932: Allow theme switching light/dark/system in groovyConsole (store themes as resource files)
1 parent 077feea commit e0714c7

3 files changed

Lines changed: 212 additions & 127 deletions

File tree

subprojects/groovy-console/src/main/groovy/groovy/console/ui/ThemeManager.groovy

Lines changed: 124 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -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() {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one or more
2+
# contributor license agreements. See the NOTICE file distributed with
3+
# this work for additional information regarding copyright ownership.
4+
# The ASF licenses this file to You under the Apache License, Version 2.0
5+
# (the "License"); you may not use this file except in compliance with
6+
# the License. You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
# Groovy Console — dark theme
17+
#
18+
# Value syntax: [#RRGGBB] [on #RRGGBB] [, bold] [, italic] [, underline]
19+
# An empty value means "no style override" (inherits default / parent style).
20+
21+
# --- area backgrounds
22+
input.background = #1E1E1E
23+
output.background = #2B2B2B
24+
25+
# --- output window styles
26+
regular = #CCCCCC
27+
prompt = #6AB465
28+
command = #6897BB
29+
stacktrace = #CC6666
30+
hyperlink = #6897BB, underline
31+
output = #CCCCCC
32+
result = #A9B7C6 on #323250
33+
34+
# --- syntax highlighting (input area)
35+
default = #CCCCCC
36+
comment = #A0A0A0, italic
37+
quotes = #DCAFF0
38+
single_quotes = #A0E19B
39+
slashy_quotes = #EBBE82
40+
digit = #DC9696
41+
annotation = #D2D282
42+
operation = #CCCCCC, bold
43+
ident = #CCCCCC
44+
reserved_word = #B4D2F0, bold
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one or more
2+
# contributor license agreements. See the NOTICE file distributed with
3+
# this work for additional information regarding copyright ownership.
4+
# The ASF licenses this file to You under the Apache License, Version 2.0
5+
# (the "License"); you may not use this file except in compliance with
6+
# the License. You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
# Groovy Console — light theme
17+
#
18+
# Value syntax: [#RRGGBB] [on #RRGGBB] [, bold] [, italic] [, underline]
19+
# An empty value means "no style override" (inherits default / parent style).
20+
21+
# --- area backgrounds
22+
input.background = #FFFFFF
23+
output.background = #FFFFDA
24+
25+
# --- output window styles
26+
regular = #000000
27+
prompt = #008000
28+
command = #0000FF
29+
stacktrace = #B20000
30+
hyperlink = #0000FF, underline
31+
output = #000000
32+
result = #0000FF on #FFFF00
33+
34+
# --- syntax highlighting (input area)
35+
default =
36+
comment = #5D5D5D, italic
37+
quotes = #7C007C
38+
single_quotes = #007C00
39+
slashy_quotes = #B28C00
40+
digit = #B20000
41+
annotation = #808000
42+
operation = bold
43+
ident =
44+
reserved_word = #00007C, bold

0 commit comments

Comments
 (0)