Skip to content

Commit 82df453

Browse files
committed
Autoconfigure TerminalUI
- Add TerminalUIBuilder which can be used to build TerminalUI - Add TerminalUICustomizer which can customize TerminalUI - What is autoconfigured is TerminalUIBuilder. - In TerminalUI add configure for views which now allows easier way to set needed stuff in views. - Various changes in a catalog app - Fixes #900
1 parent f3b8bf3 commit 82df453

File tree

18 files changed

+468
-70
lines changed

18 files changed

+468
-70
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://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+
package org.springframework.shell.boot;
17+
18+
import org.jline.terminal.Terminal;
19+
20+
import org.springframework.beans.factory.ObjectProvider;
21+
import org.springframework.boot.autoconfigure.AutoConfiguration;
22+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
23+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
24+
import org.springframework.context.annotation.Bean;
25+
import org.springframework.context.annotation.Scope;
26+
import org.springframework.shell.component.view.TerminalUI;
27+
import org.springframework.shell.component.view.TerminalUIBuilder;
28+
import org.springframework.shell.component.view.TerminalUICustomizer;
29+
import org.springframework.shell.style.ThemeActive;
30+
import org.springframework.shell.style.ThemeResolver;
31+
32+
@AutoConfiguration
33+
@ConditionalOnClass(TerminalUI.class)
34+
public class TerminalUIAutoConfiguration {
35+
36+
@Bean
37+
@Scope("prototype")
38+
@ConditionalOnMissingBean
39+
public TerminalUIBuilder terminalUIBuilder(Terminal terminal, ThemeResolver themeResolver, ThemeActive themeActive,
40+
ObjectProvider<TerminalUICustomizer> customizerProvider) {
41+
TerminalUIBuilder builder = new TerminalUIBuilder(terminal);
42+
builder = builder.themeName(themeActive.get());
43+
builder = builder.themeResolver(themeResolver);
44+
builder = builder.customizers(customizerProvider.orderedStream().toList());
45+
return builder;
46+
}
47+
48+
}

spring-shell-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ org.springframework.shell.boot.StandardAPIAutoConfiguration
1414
org.springframework.shell.boot.ThemingAutoConfiguration
1515
org.springframework.shell.boot.StandardCommandsAutoConfiguration
1616
org.springframework.shell.boot.ComponentFlowAutoConfiguration
17+
org.springframework.shell.boot.TerminalUIAutoConfiguration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://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+
package org.springframework.shell.boot;
17+
18+
import java.util.Set;
19+
20+
import org.jline.terminal.Size;
21+
import org.jline.terminal.Terminal;
22+
import org.junit.jupiter.api.Test;
23+
24+
import org.springframework.boot.autoconfigure.AutoConfigurations;
25+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
26+
import org.springframework.context.annotation.Bean;
27+
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.shell.component.view.TerminalUI;
29+
import org.springframework.shell.component.view.TerminalUIBuilder;
30+
import org.springframework.shell.component.view.TerminalUICustomizer;
31+
import org.springframework.shell.style.ThemeActive;
32+
import org.springframework.shell.style.ThemeRegistry;
33+
import org.springframework.shell.style.ThemeResolver;
34+
import org.springframework.test.util.ReflectionTestUtils;
35+
36+
import static org.assertj.core.api.Assertions.assertThat;
37+
import static org.mockito.Mockito.mock;
38+
import static org.mockito.Mockito.when;
39+
40+
class TerminalUIAutoConfigurationTests {
41+
42+
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
43+
.withConfiguration(AutoConfigurations.of(TerminalUIAutoConfiguration.class));
44+
45+
@Test
46+
public void terminalUICreated() {
47+
this.contextRunner
48+
.withUserConfiguration(MockConfiguration.class)
49+
.run(context -> {
50+
assertThat(context).hasSingleBean(TerminalUIBuilder.class);
51+
});
52+
}
53+
54+
@Test
55+
@SuppressWarnings("unchecked")
56+
public void canCustomize() {
57+
this.contextRunner
58+
.withUserConfiguration(TestConfiguration.class, MockConfiguration.class)
59+
.run(context -> {
60+
TerminalUIBuilder builder = context.getBean(TerminalUIBuilder.class);
61+
Set<TerminalUICustomizer> customizers = (Set<TerminalUICustomizer>) ReflectionTestUtils
62+
.getField(builder, "customizers");
63+
assertThat(customizers).hasSize(1);
64+
});
65+
}
66+
67+
@Configuration(proxyBeanMethods = false)
68+
static class MockConfiguration {
69+
70+
@Bean
71+
Terminal mockTerminal() {
72+
Terminal terminal = mock(Terminal.class);
73+
when(terminal.getBufferSize()).thenReturn(new Size());
74+
return terminal;
75+
}
76+
77+
@Bean
78+
ThemeResolver mockThemeResolver() {
79+
return new ThemeResolver(new ThemeRegistry(), "default");
80+
}
81+
82+
@Bean
83+
ThemeActive themeActive() {
84+
return () -> {
85+
return "default";
86+
};
87+
}
88+
89+
}
90+
91+
@Configuration(proxyBeanMethods = false)
92+
static class TestConfiguration {
93+
94+
@Bean
95+
TerminalUICustomizer terminalUICustomizer() {
96+
return new TestTerminalUICustomizer();
97+
}
98+
}
99+
100+
static class TestTerminalUICustomizer implements TerminalUICustomizer {
101+
102+
@Override
103+
public void customize(TerminalUI terminalUI) {
104+
terminalUI.setThemeName("test");
105+
}
106+
}
107+
108+
}

spring-shell-core/src/main/java/org/springframework/shell/component/view/TerminalUI.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.springframework.shell.component.view.event.MouseHandler.MouseHandlerResult;
4848
import org.springframework.shell.component.view.screen.DefaultScreen;
4949
import org.springframework.shell.geom.Rectangle;
50+
import org.springframework.shell.style.ThemeResolver;
5051
import org.springframework.util.Assert;
5152
import org.springframework.util.StringUtils;
5253

@@ -73,6 +74,8 @@ public class TerminalUI implements ViewService {
7374
private final KeyBinder keyBinder;
7475
private DefaultEventLoop eventLoop = new DefaultEventLoop();
7576
private View focus = null;
77+
private ThemeResolver themeResolver;
78+
private String themeName = "default";
7679

7780
/**
7881
* Constructs a handler with a given terminal.
@@ -135,6 +138,64 @@ public void redraw() {
135138
getEventLoop().dispatch(ShellMessageBuilder.ofRedraw());
136139
}
137140

141+
/**
142+
* Sets a {@link ThemeResolver}.
143+
*
144+
* @param themeResolver the theme resolver
145+
*/
146+
public void setThemeResolver(ThemeResolver themeResolver) {
147+
this.themeResolver = themeResolver;
148+
}
149+
150+
/**
151+
* Sets a {@link ThemeResolver}.
152+
*
153+
* @return a theme resolver
154+
*/
155+
public ThemeResolver getThemeResolver() {
156+
return themeResolver;
157+
}
158+
159+
/**
160+
* Sets a {@code theme name}.
161+
*
162+
* @param themeName the theme name
163+
*/
164+
public void setThemeName(String themeName) {
165+
this.themeName = themeName;
166+
}
167+
168+
/**
169+
* Gets a {@code theme name}.
170+
*
171+
* @return a theme name
172+
*/
173+
public String getThemeName() {
174+
return themeName;
175+
}
176+
177+
/**
178+
* Gets a {@link ViewService}.
179+
*
180+
* @return a view service
181+
*/
182+
public ViewService getViewService() {
183+
return this;
184+
}
185+
186+
/**
187+
* Configure view for {@link EventLoop}, {@link ThemeResolver},
188+
* {@code theme name} and {@link ViewService}.
189+
*
190+
* @param view the view to configure
191+
*/
192+
public void configure(View view) {
193+
view.setEventLoop(eventLoop);
194+
view.setThemeResolver(themeResolver);
195+
view.setThemeName(themeName);
196+
view.setViewService(getViewService());
197+
}
198+
138199
public void setFocus(@Nullable View view) {
139200
if (focus != null) {
140201
focus.focus(focus, false);
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://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+
package org.springframework.shell.component.view;
17+
18+
import java.util.Arrays;
19+
import java.util.Collection;
20+
import java.util.Collections;
21+
import java.util.LinkedHashSet;
22+
import java.util.Set;
23+
24+
import org.jline.terminal.Terminal;
25+
26+
import org.springframework.shell.style.ThemeResolver;
27+
import org.springframework.util.Assert;
28+
import org.springframework.util.CollectionUtils;
29+
import org.springframework.util.StringUtils;
30+
31+
/**
32+
* Builder that can be used to configure and create a {@link TerminalUI}.
33+
*
34+
* @author Janne Valkealahti
35+
*/
36+
public class TerminalUIBuilder {
37+
38+
private final Terminal terminal;
39+
private final Set<TerminalUICustomizer> customizers;
40+
private final ThemeResolver themeResolver;
41+
private final String themeName;
42+
43+
/**
44+
* Create a new {@link TerminalUIBuilder} instance.
45+
*
46+
* @param terminal the terminal
47+
* @param customizers any {@link TerminalUICustomizer TerminalUICustomizers}
48+
* that should be applied when the {@link TerminalUI} is
49+
* built
50+
*/
51+
public TerminalUIBuilder(Terminal terminal, TerminalUICustomizer... customizers) {
52+
this.terminal = terminal;
53+
this.customizers = copiedSetOf(customizers);
54+
this.themeResolver = null;
55+
this.themeName = null;
56+
}
57+
58+
/**
59+
* Create a new {@link TerminalUIBuilder} instance.
60+
*
61+
* @param terminal the terminal
62+
* @param customizers any {@link TerminalUICustomizer TerminalUICustomizers}
63+
* that should be applied when the {@link TerminalUI} is
64+
* built
65+
* @param themeResolver the theme resolver
66+
* @param themeName the theme name
67+
*/
68+
public TerminalUIBuilder(Terminal terminal, Set<TerminalUICustomizer> customizers, ThemeResolver themeResolver,
69+
String themeName) {
70+
this.terminal = terminal;
71+
this.customizers = customizers;
72+
this.themeResolver = themeResolver;
73+
this.themeName = themeName;
74+
}
75+
76+
/**
77+
* Sets a {@link ThemeResolver} for {@link TerminalUI} to build.
78+
*
79+
* @param themeResolver the theme resolver
80+
* @return a new builder instance
81+
*/
82+
public TerminalUIBuilder themeResolver(ThemeResolver themeResolver) {
83+
return new TerminalUIBuilder(terminal, customizers, themeResolver, themeName);
84+
}
85+
86+
/**
87+
* Sets a {@code theme name} for {@link TerminalUI} to build.
88+
*
89+
* @param themeName the theme name
90+
* @return a new builder instance
91+
*/
92+
public TerminalUIBuilder themeName(String themeName) {
93+
return new TerminalUIBuilder(terminal, customizers, themeResolver, themeName);
94+
}
95+
96+
/**
97+
* Set the {@link TerminalUICustomizer TerminalUICustomizer} that should be
98+
* applied to the {@link TerminalUI}. Customizers are applied in the order that they
99+
* were added after builder configuration has been applied. Setting this value will
100+
* replace any previously configured customizers.
101+
*
102+
* @param customizers the customizers to set
103+
* @return a new builder instance
104+
*/
105+
public TerminalUIBuilder customizers(Collection<? extends TerminalUICustomizer> customizers) {
106+
Assert.notNull(customizers, "Customizers must not be null");
107+
return new TerminalUIBuilder(terminal, copiedSetOf(customizers), themeResolver, themeName);
108+
}
109+
110+
/**
111+
* Build a new {@link TerminalUI} instance and configure it using this builder.
112+
*
113+
* @return a configured {@link TerminalUI} instance.
114+
*/
115+
public TerminalUI build() {
116+
return configure(new TerminalUI(terminal));
117+
}
118+
119+
/**
120+
* Configure the provided {@link TerminalUI} instance using this builder.
121+
*
122+
* @param <T> the type of terminal ui
123+
* @param terminalUI the {@link TerminalUI} to configure
124+
* @return the terminal ui instance
125+
*/
126+
public <T extends TerminalUI> T configure(T terminalUI) {
127+
if (themeResolver != null) {
128+
terminalUI.setThemeResolver(themeResolver);
129+
}
130+
if (StringUtils.hasText(themeName)) {
131+
terminalUI.setThemeName(themeName);
132+
}
133+
if (!CollectionUtils.isEmpty(customizers)) {
134+
for (TerminalUICustomizer customizer : customizers) {
135+
customizer.customize(terminalUI);
136+
}
137+
}
138+
return terminalUI;
139+
}
140+
141+
@SuppressWarnings("unchecked")
142+
private <T> Set<T> copiedSetOf(T... items) {
143+
return copiedSetOf(Arrays.asList(items));
144+
}
145+
146+
private <T> Set<T> copiedSetOf(Collection<? extends T> collection) {
147+
return Collections.unmodifiableSet(new LinkedHashSet<>(collection));
148+
}
149+
150+
}

0 commit comments

Comments
 (0)