Skip to content

Commit

Permalink
Autoconfigure TerminalUI
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
jvalkeal committed Oct 25, 2023
1 parent f3b8bf3 commit 82df453
Show file tree
Hide file tree
Showing 18 changed files with 468 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.boot;

import org.jline.terminal.Terminal;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.shell.component.view.TerminalUI;
import org.springframework.shell.component.view.TerminalUIBuilder;
import org.springframework.shell.component.view.TerminalUICustomizer;
import org.springframework.shell.style.ThemeActive;
import org.springframework.shell.style.ThemeResolver;

@AutoConfiguration
@ConditionalOnClass(TerminalUI.class)
public class TerminalUIAutoConfiguration {

@Bean
@Scope("prototype")
@ConditionalOnMissingBean
public TerminalUIBuilder terminalUIBuilder(Terminal terminal, ThemeResolver themeResolver, ThemeActive themeActive,
ObjectProvider<TerminalUICustomizer> customizerProvider) {
TerminalUIBuilder builder = new TerminalUIBuilder(terminal);
builder = builder.themeName(themeActive.get());
builder = builder.themeResolver(themeResolver);
builder = builder.customizers(customizerProvider.orderedStream().toList());
return builder;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ org.springframework.shell.boot.StandardAPIAutoConfiguration
org.springframework.shell.boot.ThemingAutoConfiguration
org.springframework.shell.boot.StandardCommandsAutoConfiguration
org.springframework.shell.boot.ComponentFlowAutoConfiguration
org.springframework.shell.boot.TerminalUIAutoConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.boot;

import java.util.Set;

import org.jline.terminal.Size;
import org.jline.terminal.Terminal;
import org.junit.jupiter.api.Test;

import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.component.view.TerminalUI;
import org.springframework.shell.component.view.TerminalUIBuilder;
import org.springframework.shell.component.view.TerminalUICustomizer;
import org.springframework.shell.style.ThemeActive;
import org.springframework.shell.style.ThemeRegistry;
import org.springframework.shell.style.ThemeResolver;
import org.springframework.test.util.ReflectionTestUtils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class TerminalUIAutoConfigurationTests {

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(TerminalUIAutoConfiguration.class));

@Test
public void terminalUICreated() {
this.contextRunner
.withUserConfiguration(MockConfiguration.class)
.run(context -> {
assertThat(context).hasSingleBean(TerminalUIBuilder.class);
});
}

@Test
@SuppressWarnings("unchecked")
public void canCustomize() {
this.contextRunner
.withUserConfiguration(TestConfiguration.class, MockConfiguration.class)
.run(context -> {
TerminalUIBuilder builder = context.getBean(TerminalUIBuilder.class);
Set<TerminalUICustomizer> customizers = (Set<TerminalUICustomizer>) ReflectionTestUtils
.getField(builder, "customizers");
assertThat(customizers).hasSize(1);
});
}

@Configuration(proxyBeanMethods = false)
static class MockConfiguration {

@Bean
Terminal mockTerminal() {
Terminal terminal = mock(Terminal.class);
when(terminal.getBufferSize()).thenReturn(new Size());
return terminal;
}

@Bean
ThemeResolver mockThemeResolver() {
return new ThemeResolver(new ThemeRegistry(), "default");
}

@Bean
ThemeActive themeActive() {
return () -> {
return "default";
};
}

}

@Configuration(proxyBeanMethods = false)
static class TestConfiguration {

@Bean
TerminalUICustomizer terminalUICustomizer() {
return new TestTerminalUICustomizer();
}
}

static class TestTerminalUICustomizer implements TerminalUICustomizer {

@Override
public void customize(TerminalUI terminalUI) {
terminalUI.setThemeName("test");
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.springframework.shell.component.view.event.MouseHandler.MouseHandlerResult;
import org.springframework.shell.component.view.screen.DefaultScreen;
import org.springframework.shell.geom.Rectangle;
import org.springframework.shell.style.ThemeResolver;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

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

/**
* Constructs a handler with a given terminal.
Expand Down Expand Up @@ -135,6 +138,64 @@ public void redraw() {
getEventLoop().dispatch(ShellMessageBuilder.ofRedraw());
}

/**
* Sets a {@link ThemeResolver}.
*
* @param themeResolver the theme resolver
*/
public void setThemeResolver(ThemeResolver themeResolver) {
this.themeResolver = themeResolver;
}

/**
* Sets a {@link ThemeResolver}.
*
* @return a theme resolver
*/
public ThemeResolver getThemeResolver() {
return themeResolver;
}

/**
* Sets a {@code theme name}.
*
* @param themeName the theme name
*/
public void setThemeName(String themeName) {
this.themeName = themeName;
}

/**
* Gets a {@code theme name}.
*
* @return a theme name
*/
public String getThemeName() {
return themeName;
}

/**
* Gets a {@link ViewService}.
*
* @return a view service
*/
public ViewService getViewService() {
return this;
}

/**
* Configure view for {@link EventLoop}, {@link ThemeResolver},
* {@code theme name} and {@link ViewService}.
*
* @param view the view to configure
*/
public void configure(View view) {
view.setEventLoop(eventLoop);
view.setThemeResolver(themeResolver);
view.setThemeName(themeName);
view.setViewService(getViewService());
}

public void setFocus(@Nullable View view) {
if (focus != null) {
focus.focus(focus, false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.component.view;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;

import org.jline.terminal.Terminal;

import org.springframework.shell.style.ThemeResolver;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

/**
* Builder that can be used to configure and create a {@link TerminalUI}.
*
* @author Janne Valkealahti
*/
public class TerminalUIBuilder {

private final Terminal terminal;
private final Set<TerminalUICustomizer> customizers;
private final ThemeResolver themeResolver;
private final String themeName;

/**
* Create a new {@link TerminalUIBuilder} instance.
*
* @param terminal the terminal
* @param customizers any {@link TerminalUICustomizer TerminalUICustomizers}
* that should be applied when the {@link TerminalUI} is
* built
*/
public TerminalUIBuilder(Terminal terminal, TerminalUICustomizer... customizers) {
this.terminal = terminal;
this.customizers = copiedSetOf(customizers);
this.themeResolver = null;
this.themeName = null;
}

/**
* Create a new {@link TerminalUIBuilder} instance.
*
* @param terminal the terminal
* @param customizers any {@link TerminalUICustomizer TerminalUICustomizers}
* that should be applied when the {@link TerminalUI} is
* built
* @param themeResolver the theme resolver
* @param themeName the theme name
*/
public TerminalUIBuilder(Terminal terminal, Set<TerminalUICustomizer> customizers, ThemeResolver themeResolver,
String themeName) {
this.terminal = terminal;
this.customizers = customizers;
this.themeResolver = themeResolver;
this.themeName = themeName;
}

/**
* Sets a {@link ThemeResolver} for {@link TerminalUI} to build.
*
* @param themeResolver the theme resolver
* @return a new builder instance
*/
public TerminalUIBuilder themeResolver(ThemeResolver themeResolver) {
return new TerminalUIBuilder(terminal, customizers, themeResolver, themeName);
}

/**
* Sets a {@code theme name} for {@link TerminalUI} to build.
*
* @param themeName the theme name
* @return a new builder instance
*/
public TerminalUIBuilder themeName(String themeName) {
return new TerminalUIBuilder(terminal, customizers, themeResolver, themeName);
}

/**
* Set the {@link TerminalUICustomizer TerminalUICustomizer} that should be
* applied to the {@link TerminalUI}. Customizers are applied in the order that they
* were added after builder configuration has been applied. Setting this value will
* replace any previously configured customizers.
*
* @param customizers the customizers to set
* @return a new builder instance
*/
public TerminalUIBuilder customizers(Collection<? extends TerminalUICustomizer> customizers) {
Assert.notNull(customizers, "Customizers must not be null");
return new TerminalUIBuilder(terminal, copiedSetOf(customizers), themeResolver, themeName);
}

/**
* Build a new {@link TerminalUI} instance and configure it using this builder.
*
* @return a configured {@link TerminalUI} instance.
*/
public TerminalUI build() {
return configure(new TerminalUI(terminal));
}

/**
* Configure the provided {@link TerminalUI} instance using this builder.
*
* @param <T> the type of terminal ui
* @param terminalUI the {@link TerminalUI} to configure
* @return the terminal ui instance
*/
public <T extends TerminalUI> T configure(T terminalUI) {
if (themeResolver != null) {
terminalUI.setThemeResolver(themeResolver);
}
if (StringUtils.hasText(themeName)) {
terminalUI.setThemeName(themeName);
}
if (!CollectionUtils.isEmpty(customizers)) {
for (TerminalUICustomizer customizer : customizers) {
customizer.customize(terminalUI);
}
}
return terminalUI;
}

@SuppressWarnings("unchecked")
private <T> Set<T> copiedSetOf(T... items) {
return copiedSetOf(Arrays.asList(items));
}

private <T> Set<T> copiedSetOf(Collection<? extends T> collection) {
return Collections.unmodifiableSet(new LinkedHashSet<>(collection));
}

}

0 comments on commit 82df453

Please sign in to comment.