Rework themes support

- Add theme settings for figures.
- Settings for styles are now in it own class and
  ThemeSettings are wrapping those.
- Prefixed formatting string to have `style-` prefix.
- Templates can now use added `figures` dictionary which
  is resolved per activated theme.
- Create `dump` theme.
- New `ThemeActive` interface where default impl uses dump
  theme if `CI` or `NO_COLOR` env is set.
- Rework existing templates.
- Fixes #442
This commit is contained in:
Janne Valkealahti
2022-06-10 09:11:30 +01:00
parent fa65a2308e
commit 073cd8562c
27 changed files with 807 additions and 256 deletions

View File

@@ -174,7 +174,7 @@ public class SpringShellProperties {
public static class Theme {
private String name = "default";
private String name;
public String getName() {
return name;

View File

@@ -16,35 +16,57 @@
package org.springframework.shell.boot;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.style.TemplateExecutor;
import org.springframework.shell.style.Theme;
import org.springframework.shell.style.ThemeActive;
import org.springframework.shell.style.ThemeRegistry;
import org.springframework.shell.style.ThemeResolver;
import org.springframework.shell.style.ThemeSettings;
import org.springframework.util.StringUtils;
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(SpringShellProperties.class)
public class ThemingAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ThemeActive themeActive() {
return () -> {
if (System.getenv("CI") != null || System.getenv("NO_COLOR") != null) {
return "dump";
}
return "default";
};
}
@Bean
public ThemeRegistry themeRegistry(ObjectProvider<Theme> themes) {
ThemeRegistry registry = new ThemeRegistry();
registry.register(Theme.of("default", ThemeSettings.themeSettings()));
registry.register(Theme.of("default", ThemeSettings.defaults()));
registry.register(Theme.of("dump", ThemeSettings.dump()));
themes.orderedStream().forEachOrdered(registry::register);
return registry;
}
@Bean
public ThemeResolver shellThemeResolver(ThemeRegistry themeRegistry, SpringShellProperties properties) {
return new ThemeResolver(themeRegistry, properties.getTheme().getName());
public ThemeResolver shellThemeResolver(ThemeRegistry themeRegistry, SpringShellProperties properties,
ThemeActive themeActive) {
String themeName = properties.getTheme().getName();
if (!StringUtils.hasText(themeName)) {
themeName = themeActive.get();
}
if (!StringUtils.hasText(themeName)) {
themeName = "default";
}
return new ThemeResolver(themeRegistry, themeName);
}
@Bean
public TemplateExecutor templateExecutor(ThemeResolver themeResolver) {
return new TemplateExecutor(themeResolver);
}
}

View File

@@ -40,7 +40,7 @@ public class SpringShellPropertiesTests {
assertThat(properties.getScript().isEnabled()).isTrue();
assertThat(properties.getInteractive().isEnabled()).isTrue();
assertThat(properties.getNoninteractive().isEnabled()).isTrue();
assertThat(properties.getTheme().getName()).isEqualTo("default");
assertThat(properties.getTheme().getName()).isNull();
assertThat(properties.getCommand().getClear().isEnabled()).isTrue();
assertThat(properties.getCommand().getHelp().isEnabled()).isTrue();
assertThat(properties.getCommand().getHelp().getGroupingMode()).isEqualTo(GroupingMode.GROUP);

View File

@@ -15,17 +15,24 @@
*/
package org.springframework.shell.boot;
import java.util.stream.Stream;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.InstanceOfAssertFactory;
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.style.FigureSettings;
import org.springframework.shell.style.StyleSettings;
import org.springframework.shell.style.TemplateExecutor;
import org.springframework.shell.style.Theme;
import org.springframework.shell.style.ThemeRegistry;
import org.springframework.shell.style.ThemeResolver;
import org.springframework.shell.style.ThemeSettings;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -35,19 +42,22 @@ public class ThemingAutoConfigurationTests {
.withConfiguration(AutoConfigurations.of(ThemingAutoConfiguration.class));
@Test
public void testDefaults() {
void createsDefaultBeans() {
this.contextRunner
.run(context -> {
assertThat(context).hasSingleBean(TemplateExecutor.class);
assertThat(context).hasSingleBean(ThemeRegistry.class);
ThemeRegistry registry = context.getBean(ThemeRegistry.class);
assertThat(registry.get("default")).isNotNull();
assertThat(registry.get("dump")).isNotNull();
assertThat(context).hasSingleBean(ThemeResolver.class);
ThemeResolver resolver = context.getBean(ThemeResolver.class);
assertThat(resolver).extracting("theme").asInstanceOf(THEME).hasName("default", "dump");
});
}
@Test
public void testRegistersCustomTheme() {
public void canRegisterCustomTheme() {
this.contextRunner
.withUserConfiguration(CustomThemeConfig.class)
.run(context -> {
@@ -79,5 +89,36 @@ public class ThemingAutoConfigurationTests {
}
static class MyThemeSettings extends ThemeSettings {
MyThemeSettings() {
super(StyleSettings.defaults(), FigureSettings.defaults());
}
}
InstanceOfAssertFactory<Theme, ThemeAssert> THEME = new InstanceOfAssertFactory<>(Theme.class,
ThemeAssertions::assertThat);
static class ThemeAssertions {
public static ThemeAssert assertThat(Theme actual) {
return new ThemeAssert(actual);
}
}
static class ThemeAssert extends AbstractAssert<ThemeAssert, Theme> {
public ThemeAssert(Theme actual) {
super(actual, ThemeAssert.class);
}
public ThemeAssert hasName(String... names) {
isNotNull();
boolean match = Stream.of(names).filter(n -> actual.getName().equals(n)).findFirst().isPresent();
if (!match) {
failWithMessage("Expected theme to have names %s but was %s",
StringUtils.arrayToCommaDelimitedString(names), actual.getName());
}
return this;
}
}
}

View File

@@ -0,0 +1,257 @@
/*
* Copyright 2022 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.style;
/**
* Base class defining a settings for figures.
*
* @author Janne Valkealahti
*/
public abstract class FigureSettings {
/**
* Figure meant to be used as ok like a checkmark.
*/
public final static String TAG_TICK = "tick";
/**
* Figure indicating something about on a level of info.
*/
public final static String TAG_INFO = "info";
/**
* Figure indicating something about on a level of warning.
*/
public final static String TAG_WARNING = "warning";
/**
* Figure indicating something about on a level of error.
*/
public final static String TAG_ERROR = "error";
/**
* Figure used as a checkbox for off position.
*/
public final static String TAG_CHECKBOX_OFF = "checkboxOff";
/**
* Figure used as a checkbox for on position.
*/
public final static String TAG_CHECKBOX_ON = "checkboxOn";
/**
* Figure for left arrow.
*/
public final static String TAG_LEFTWARDS_ARROR = "leftwardsArrow";
/**
* Figure for up arrow.
*/
public final static String TAG_UPWARDS_ARROR = "upwardsArrow";
/**
* Figure for right arrow.
*/
public final static String TAG_RIGHTWARDS_ARROR = "righwardsArror";
/**
* Figure for down arrow.
*/
public final static String TAG_DOWNWARDS_ARROR = "downwardsArror";
/**
* Figure used as indicator pointing left.
*/
public final static String TAG_LEFT_POINTING_QUOTATION = "leftPointingQuotation";
/**
* Figure used as indicator pointing left.
*/
public final static String TAG_RIGHT_POINTING_QUOTATION = "rightPointingQuotation";
/**
* Figure meant for question.
*/
public final static String TAG_QUESTION_MARK = "questionMark";
public String tick() {
return ""; // U+2714 Heavy Check Mark Emoji, tick, checkmark
}
public String info() {
return ""; // U+2139 Information Source Emoji
}
public String warning() {
return ""; // U+26A0 Warning Sign Emoji, danger, аlert
}
public String error() {
return ""; // U+2716 Heavy Multiplication X Emoji, cross
}
public String checkboxOff() {
return ""; // U+2610 Ballot Box
}
public String checkboxOn() {
return ""; // U+2612 Ballot Box with X
}
public String leftwardsArrow() {
return ""; // U+2190 Leftwards Arrow
}
public String upwardsArrow() {
return ""; // U+2191 Upwards Arrow
}
public String righwardsArror() {
return ""; // U+2192 Rightwards Arrow
}
public String downwardsArror() {
return ""; // U+2193 Downwards Arrow
}
public String leftPointingQuotation() {
return ""; // U+276E Heavy Left-Pointing Angle Quotation Mark Ornament
}
public String rightPointingQuotation() {
return ""; // U+276F Heavy Right-Pointing Angle Quotation Mark Ornament
}
public String questionMark() {
return "?"; // U+003F Question Mark
}
public String resolveTag(String tag) {
switch (tag) {
case TAG_TICK:
return tick();
case TAG_INFO:
return info();
case TAG_WARNING:
return warning();
case TAG_ERROR:
return error();
case TAG_CHECKBOX_OFF:
return checkboxOff();
case TAG_CHECKBOX_ON:
return checkboxOn();
case TAG_LEFTWARDS_ARROR:
return leftwardsArrow();
case TAG_UPWARDS_ARROR:
return upwardsArrow();
case TAG_RIGHTWARDS_ARROR:
return righwardsArror();
case TAG_DOWNWARDS_ARROR:
return downwardsArror();
case TAG_LEFT_POINTING_QUOTATION:
return leftPointingQuotation();
case TAG_RIGHT_POINTING_QUOTATION:
return rightPointingQuotation();
case TAG_QUESTION_MARK:
return questionMark();
}
throw new IllegalArgumentException(String.format("Unknown tag '%s'", tag));
}
public static FigureSettings defaults() {
return new DefaultFigureSettings();
}
public static FigureSettings dump() {
return new DumpFigureSettings();
}
public static String[] tags() {
return new String[] {
TAG_TICK,
TAG_INFO,
TAG_WARNING,
TAG_ERROR,
TAG_CHECKBOX_OFF,
TAG_CHECKBOX_ON,
TAG_LEFTWARDS_ARROR,
TAG_UPWARDS_ARROR,
TAG_RIGHTWARDS_ARROR,
TAG_DOWNWARDS_ARROR,
TAG_LEFT_POINTING_QUOTATION,
TAG_RIGHT_POINTING_QUOTATION,
TAG_QUESTION_MARK
};
}
private static class DefaultFigureSettings extends FigureSettings {
}
private static class DumpFigureSettings extends FigureSettings {
@Override
public String tick() {
return "v";
}
@Override
public String info() {
return "i";
}
@Override
public String warning() {
return "!";
}
@Override
public String error() {
return "x";
}
public String checkboxOff() {
return "[ ]";
}
public String checkboxOn() {
return "[x]";
}
public String leftwardsArrow() {
return "<";
}
public String upwardsArrow() {
return "^";
}
public String righwardsArror() {
return ">";
}
public String downwardsArror() {
return "v";
}
public String leftPointingQuotation() {
return "<";
}
public String rightPointingQuotation() {
return ">";
}
}
}

View File

@@ -45,7 +45,7 @@ public class StringToStyleExpressionRenderer implements AttributeRenderer<String
return value;
}
else {
return String.format("@{%s %s}", themeResolver.resolveTag(formatString), value);
return String.format("@{%s %s}", themeResolver.resolveStyleTag(formatString), value);
}
}
}

View File

@@ -0,0 +1,290 @@
/*
* Copyright 2022 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.style;
/**
* Base class defining a settings for styles.
*
* @author Janne Valkealahti
*/
public abstract class StyleSettings {
/**
* Represents some arbitrary {@code title}.
*/
public final static String TAG_TITLE = "style-title";
/**
* Represents some arbitrary {@code value}.
*/
public final static String TAG_VALUE = "style-value";
/**
* Styling for keys or names in a lists:
* <list key1> : list value1
* <list key2> : list value2
*/
public final static String TAG_LIST_KEY = "style-list-key";
/**
* Styling for keys or names in a lists:
* list key1 : <list value1>
* list key2 : <list value2>
*/
public final static String TAG_LIST_VALUE = "style-list-value";
/**
* Styling for some arbitrary content indicating {@code INFO} level.
*/
public final static String TAG_LEVEL_INFO = "style-level-info";
/**
* Styling for some arbitrary content indicating {@code WARN} level.
*/
public final static String TAG_LEVEL_WARN = "style-level-warn";
/**
* Styling for some arbitrary content indicating {@code ERROR} level.
*/
public final static String TAG_LEVEL_ERROR = "style-level-error";
/**
* Styling for something i.e. in selectors when item is selectable.
*/
public final static String TAG_ITEM_ENABLED = "style-item-enabled";
/**
* Styling for something i.e. in selectors when item can't be selected.
*/
public final static String TAG_ITEM_DISABLED = "style-item-disabled";
/**
* Styling for something i.e. in selectors when item is selected.
*/
public final static String TAG_ITEM_SELECTED = "style-item-selected";
/**
* Styling for something i.e. in selectors when item is not selected.
*/
public final static String TAG_ITEM_UNSELECTED = "style-item-unselected";
/**
* Styling for selector i.e. arrow in selectors.
*/
public final static String TAG_ITEM_SELECTOR = "style-item-selector";
/**
* Styling for something which is highlighted.
*/
public final static String TAG_HIGHLIGHT = "style-highlight";
public String title() {
return "bold,fg:bright-white";
}
public String value() {
return "fg:blue";
}
public String listKey() {
return "default";
}
public String listValue() {
return "bold,fg:green";
}
public String listLevelInfo() {
return "fg:green";
}
public String listLevelWarn() {
return "fg:yellow";
}
public String listLevelError() {
return "fg:red";
}
public String itemEnabled() {
return "bold";
}
public String itemDisabled() {
return "faint";
}
public String itemSelected() {
return "fg:green";
}
public String itemUnselected() {
return "bold";
}
public String itemSelector() {
return "bold,fg:bright-cyan";
}
public String highlight() {
return "bold";
}
/**
* Resolve a theme setting from a given tag.
*
* @param tag the tag
* @return a theme setting
*/
public String resolveTag(String tag) {
switch (tag) {
case TAG_TITLE:
return title();
case TAG_VALUE:
return value();
case TAG_LIST_KEY:
return listKey();
case TAG_LIST_VALUE:
return listValue();
case TAG_LEVEL_INFO:
return listLevelInfo();
case TAG_LEVEL_WARN:
return listLevelWarn();
case TAG_LEVEL_ERROR:
return listLevelError();
case TAG_ITEM_ENABLED:
return itemEnabled();
case TAG_ITEM_DISABLED:
return itemDisabled();
case TAG_ITEM_SELECTED:
return itemSelected();
case TAG_ITEM_UNSELECTED:
return itemUnselected();
case TAG_ITEM_SELECTOR:
return itemSelector();
case TAG_HIGHLIGHT:
return highlight();
}
throw new IllegalArgumentException(String.format("Unknown tag '%s'", tag));
}
/**
* Creates an instance of a default settings.
*
* @return a default theme settings
*/
public static StyleSettings defaults() {
return new DefaultStyleSettings();
}
public static StyleSettings dump() {
return new DumpStyleSettings();
}
/**
* Gets all tags.
*
* @return array of all tags
*/
public static String[] tags() {
return new String[] {
TAG_TITLE,
TAG_VALUE,
TAG_LIST_KEY,
TAG_LIST_VALUE,
TAG_LEVEL_INFO,
TAG_LEVEL_WARN,
TAG_LEVEL_ERROR,
TAG_ITEM_ENABLED,
TAG_ITEM_DISABLED,
TAG_ITEM_SELECTED,
TAG_ITEM_UNSELECTED,
TAG_ITEM_SELECTOR,
TAG_HIGHLIGHT
};
}
private static class DefaultStyleSettings extends StyleSettings {
}
private static class DumpStyleSettings extends StyleSettings {
@Override
public String title() {
return "default";
}
@Override
public String value() {
return "default";
}
@Override
public String listKey() {
return "default";
}
@Override
public String listValue() {
return "default";
}
@Override
public String listLevelInfo() {
return "default";
}
@Override
public String listLevelWarn() {
return "default";
}
@Override
public String listLevelError() {
return "default";
}
@Override
public String itemEnabled() {
return "default";
}
@Override
public String itemDisabled() {
return "default";
}
@Override
public String itemSelected() {
return "default";
}
@Override
public String itemUnselected() {
return "default";
}
@Override
public String itemSelector() {
return "default";
}
@Override
public String highlight() {
return "default";
}
}
}

View File

@@ -16,6 +16,8 @@
package org.springframework.shell.style;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jline.utils.AttributedString;
import org.slf4j.Logger;
@@ -76,6 +78,11 @@ public class TemplateExecutor {
group.setListener(ERROR_LISTENER);
group.registerRenderer(String.class, renderer);
// define styled figures as dictionary
Map<String, Object> figureDict = Stream.of(FigureSettings.tags())
.collect(Collectors.toMap(tag -> tag, tag -> this.themeResolver.resolveFigureTag(tag)));
group.defineDictionary("figures", figureDict);
ST st = group.getInstanceOf("main");
if (st == null) {
throw new IllegalArgumentException("template instance 'main' not found from a group");

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2022 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.style;
import java.util.function.Supplier;
public interface ThemeActive extends Supplier<String> {
}

View File

@@ -54,8 +54,18 @@ public class ThemeResolver {
* @param tag the tag
* @return a style
*/
public String resolveTag(String tag) {
return theme.getSettings().resolveTag(tag);
public String resolveStyleTag(String tag) {
return theme.getSettings().styles().resolveTag(tag);
}
/**
* Resolve figure from a tag with activated theme.
*
* @param tag the tag
* @return a style
*/
public String resolveFigureTag(String tag) {
return theme.getSettings().figures().resolveTag(tag);
}
/**

View File

@@ -16,206 +16,77 @@
package org.springframework.shell.style;
/**
* {@link ThemeSettings} is storing {@link Theme} related settings together and
* is base class for further customizations.
*
* Settings in this base class are default styles.
* Base class defining a settings for themes.
*
* @author Janne Valkealahti
*/
public abstract class ThemeSettings {
/**
* Represents some arbitrary {@code title}.
*/
public final static String TAG_TITLE = "title";
private StyleSettings styleSettings;
private FigureSettings figureSettings;
/**
* Represents some arbitrary {@code value}.
* Creates theme settings with dump styles and figures.
*/
public final static String TAG_VALUE = "value";
/**
* Styling for keys or names in a lists:
* <list key1> : list value1
* <list key2> : list value2
*/
public final static String TAG_LIST_KEY = "list-key";
/**
* Styling for keys or names in a lists:
* list key1 : <list value1>
* list key2 : <list value2>
*/
public final static String TAG_LIST_VALUE = "list-value";
/**
* Styling for some arbitrary content indicating {@code INFO} level.
*/
public final static String TAG_LEVEL_INFO = "level-info";
/**
* Styling for some arbitrary content indicating {@code WARN} level.
*/
public final static String TAG_LEVEL_WARN = "level-warn";
/**
* Styling for some arbitrary content indicating {@code ERROR} level.
*/
public final static String TAG_LEVEL_ERROR = "level-error";
/**
* Styling for something i.e. in selectors when item is selectable.
*/
public final static String TAG_ITEM_ENABLED = "item-enabled";
/**
* Styling for something i.e. in selectors when item can't be selected.
*/
public final static String TAG_ITEM_DISABLED = "item-disabled";
/**
* Styling for something i.e. in selectors when item is selected.
*/
public final static String TAG_ITEM_SELECTED = "item-selected";
/**
* Styling for something i.e. in selectors when item is not selected.
*/
public final static String TAG_ITEM_UNSELECTED = "item-unselected";
/**
* Styling for selector i.e. arrow in selectors.
*/
public final static String TAG_ITEM_SELECTOR = "item-selector";
/**
* Styling for something which is highlighted.
*/
public final static String TAG_HIGHLIGHT = "highlight";
public String title() {
return "bold,fg:bright-white";
}
public String value() {
return "fg:blue";
}
public String listKey() {
return "default";
}
public String listValue() {
return "bold,fg:green";
}
public String listLevelInfo() {
return "fg:green";
}
public String listLevelWarn() {
return "fg:yellow";
}
public String listLevelError() {
return "fg:red";
}
public String itemEnabled() {
return "bold";
}
public String itemDisabled() {
return "faint";
}
public String itemSelected() {
return "fg:green";
}
public String itemUnselected() {
return "bold";
}
public String itemSelector() {
return "bold,fg:bright-cyan";
}
public String highlight() {
return "bold";
public ThemeSettings() {
this(StyleSettings.dump(), FigureSettings.dump());
}
/**
* Resolve a theme setting from a given tag.
* Creates theme settings with styles and figures.
*
* @param tag the tag
* @return a theme setting
* @param styleSettings style settings
* @param figureSettings figure settings
*/
public String resolveTag(String tag) {
switch (tag) {
case TAG_TITLE:
return title();
case TAG_VALUE:
return value();
case TAG_LIST_KEY:
return listKey();
case TAG_LIST_VALUE:
return listValue();
case TAG_LEVEL_INFO:
return listLevelInfo();
case TAG_LEVEL_WARN:
return listLevelWarn();
case TAG_LEVEL_ERROR:
return listLevelError();
case TAG_ITEM_ENABLED:
return itemEnabled();
case TAG_ITEM_DISABLED:
return itemDisabled();
case TAG_ITEM_SELECTED:
return itemSelected();
case TAG_ITEM_UNSELECTED:
return itemUnselected();
case TAG_ITEM_SELECTOR:
return itemSelector();
case TAG_HIGHLIGHT:
return highlight();
}
throw new IllegalArgumentException(String.format("Unknown tag '%s'", tag));
public ThemeSettings(StyleSettings styleSettings, FigureSettings figureSettings) {
this.styleSettings = styleSettings;
this.figureSettings = figureSettings;
}
/**
* Creates an instance of a default settings.
* Gets a {@link StyleSettings}.
*
* @return a default theme settings
* @return style settings
*/
public static ThemeSettings themeSettings() {
public StyleSettings styles() {
return this.styleSettings;
}
/**
* Gets a {@link FigureSettings}.
*
* @return figure settings
*/
public FigureSettings figures() {
return this.figureSettings;
}
/**
* Gets a default theme settings.
*
* @return default theme settings
*/
public static ThemeSettings defaults() {
return new DefaultThemeSettings(StyleSettings.defaults(), FigureSettings.defaults());
}
/**
* Gets a dump theme settings.
*
* @return dump theme settings
*/
public static ThemeSettings dump() {
return new DefaultThemeSettings();
}
/**
* Gets all tags.
*
* @return array of all tags
*/
public static String[] tags() {
return new String[] {
TAG_TITLE,
TAG_VALUE,
TAG_LIST_KEY,
TAG_LIST_VALUE,
TAG_LEVEL_INFO,
TAG_LEVEL_WARN,
TAG_LEVEL_ERROR,
TAG_ITEM_ENABLED,
TAG_ITEM_DISABLED,
TAG_ITEM_SELECTED,
TAG_ITEM_UNSELECTED,
TAG_ITEM_SELECTOR,
TAG_HIGHLIGHT
};
}
private static class DefaultThemeSettings extends ThemeSettings {
DefaultThemeSettings() {
super();
}
DefaultThemeSettings(StyleSettings styleSettings, FigureSettings figureSettings) {
super(styleSettings, figureSettings);
}
}
}

View File

@@ -12,20 +12,20 @@ message(model) ::= <%
// info section after '? xxx'
info(model) ::= <%
<if(model.defaultValue)>
<("(Y/n)"); format="item-disabled">
<("(Y/n)"); format="style-item-disabled">
<else>
<("(y/N)"); format="item-disabled">
<("(y/N)"); format="style-item-disabled">
<endif>
%>
// start '? xxx' shows both running and result
question_name(model) ::= <<
<("?"); format="list-value"> <model.name; format="title">
<({<figures.questionMark>}); format="style-list-value"> <model.name; format="style-title">
>>
// component result
result(model) ::= <<
<question_name(model)> <model.resultValue; format="value">
<question_name(model)> <model.resultValue; format="style-value">
>>
// component is running

View File

@@ -1,30 +1,30 @@
// used to select style if item is selected/unselected
selected_style(flag) ::= <%
<if(flag)>item-selected<else>item-unselected<endif>
<if(flag)>style-item-selected<else>style-item-unselected<endif>
%>
// selector rows
select_item(item) ::= <%
<if(item.onrow)>
<("> "); format="item-selector">
<({<figures.rightPointingQuotation> }); format="style-item-selector">
<else>
<(" ")>
<endif>
<if(item.enabled)>
<if(item.selected)>
<("[x]"); format=selected_style(item.selected)> <item.name>
<({<figures.checkboxOn> }); format=selected_style(item.selected)> <item.name>
<else>
<("[ ]"); format=selected_style(item.selected)> <item.name>
<({<figures.checkboxOff> }); format=selected_style(item.selected)> <item.name>
<endif>
<else>
<("[ ]"); format="item-disabled"> <item.name; format="item-disabled">
<({<figures.checkboxOff> }); format="style-item-disabled"> <item.name; format="style-item-disabled">
<endif>
%>
// start '? xxx' shows both running and result
question_name(model) ::= <<
<("?"); format="list-value"> <model.name; format="title">
<({<figures.questionMark>}); format="style-list-value"> <model.name; format="style-title">
>>
// within info section, dedicated instructions for user
@@ -48,7 +48,7 @@ comma_delimited(values) ::= <%
// component result
result(model) ::= <<
<question_name(model)> <(comma_delimited(model.values)); format="value">
<question_name(model)> <(comma_delimited(model.values)); format="style-value">
>>
// component is running

View File

@@ -1,11 +1,11 @@
// message
message(model) ::= <%
<if(model.message && model.hasMessageLevelError)>
<(">>>"); format="level-error"> <model.message; format="level-error">
<({<figures.error>}); format="style-level-error"> <model.message; format="style-level-error">
<elseif(model.message && model.hasMessageLevelWarn)>
<(">>"); format="level-warn"> <model.message; format="level-warn">
<({<figures.warning>}); format="style-level-warn"> <model.message; format="style-level-warn">
<elseif(model.message && model.hasMessageLevelInfo)>
<(">"); format="level-info"> <model.message; format="level-info">
<({<figures.info>}); format="style-level-info"> <model.message; format="style-level-info">
<endif>
%>
@@ -18,12 +18,12 @@ info(model) ::= <%
// start '? xxx' shows both running and result
question_name(model) ::= <<
<("?"); format="list-value"> <model.name; format="title">
<({<figures.questionMark>}); format="style-list-value"> <model.name; format="style-title">
>>
// component result
result(model) ::= <<
<question_name(model)> <model.resultValue; format="value">
<question_name(model)> <model.resultValue; format="style-value">
>>
// component is running

View File

@@ -1,7 +1,7 @@
// selector rows
select_item(item) ::= <%
<if(item.selected)>
<("> "); format="item-selector"><item.name; format="item-selector">
<({<figures.rightPointingQuotation> }); format="style-item-selector"><item.name; format="style-item-selector">
<else>
<(" ")><item.name>
<endif>
@@ -9,7 +9,7 @@ select_item(item) ::= <%
// start '? xxx' shows both running and result
question_name(model) ::= <<
<("?"); format="list-value"> <model.name; format="title">
<({<figures.questionMark>}); format="style-list-value"> <model.name; format="style-title">
>>
// within info section, dedicated instructions for user
@@ -28,7 +28,7 @@ info(model) ::= <<
// component result
result(model) ::= <<
<question_name(model)> <model.value; format="value">
<question_name(model)> <model.value; format="style-value">
>>
// component is running

View File

@@ -5,7 +5,7 @@ info(model) ::= <%
<model.maskedInput>
<else>
<if(model.defaultValue)>
<("[Default "); format="value"><model.defaultValue; format="value"><("]"); format="value">
<("[Default "); format="style-value"><model.defaultValue; format="style-value"><("]"); format="style-value">
<endif>
<endif>
<else>
@@ -13,7 +13,7 @@ info(model) ::= <%
<model.input>
<else>
<if(model.defaultValue)>
<("[Default "); format="value"><model.defaultValue; format="value"><("]"); format="value">
<("[Default "); format="style-value"><model.defaultValue; format="style-value"><("]"); format="style-value">
<endif>
<endif>
<endif>
@@ -21,12 +21,12 @@ info(model) ::= <%
// start '? xxx' shows both running and result
question_name(model) ::= <<
<("?"); format="list-value"> <model.name; format="title">
<({<figures.questionMark>}); format="style-list-value"> <model.name; format="style-title">
>>
// component result
result(model) ::= <<
<question_name(model)> <model.maskedResultValue; format="value">
<question_name(model)> <model.maskedResultValue; format="style-value">
>>
// component is running

View File

@@ -64,15 +64,15 @@ public abstract class AbstractShellTests {
themeRegistry.register(new Theme() {
@Override
public String getName() {
return "default";
return "dump";
}
@Override
public ThemeSettings getSettings() {
return ThemeSettings.themeSettings();
return ThemeSettings.dump();
}
});
ThemeResolver themeResolver = new ThemeResolver(themeRegistry, "default");
ThemeResolver themeResolver = new ThemeResolver(themeRegistry, "dump");
templateExecutor = new TemplateExecutor(themeResolver);
resourceLoader = new DefaultResourceLoader();

View File

@@ -96,7 +96,7 @@ public class MultiItemSelectorTests extends AbstractShellTests {
public void testItemsShownWithDisabled() {
scheduleSelect(Arrays.asList(SELECTOR_ITEM_1, SELECTOR_ITEM_7));
await().atMost(Duration.ofSeconds(4))
.untilAsserted(() -> assertStringOrderThat(consoleOut()).containsInOrder("[ ] simplePojo1", "[ ] simplePojo7"));
.untilAsserted(() -> assertStringOrderThat(consoleOut()).containsInOrder("[ ] simplePojo1", "[ ] simplePojo7"));
}
@Test

View File

@@ -69,7 +69,7 @@ public abstract class AbstractShellTests {
@Override
public ThemeSettings getSettings() {
return ThemeSettings.themeSettings();
return ThemeSettings.defaults();
}
});
ThemeResolver themeResolver = new ThemeResolver(themeRegistry, "default");

View File

@@ -41,7 +41,7 @@ public class TemplateExecutorTests {
@Override
public ThemeSettings getSettings() {
return ThemeSettings.themeSettings();
return ThemeSettings.defaults();
}
});
themeResolver = new ThemeResolver(themeRegistry, "default");
@@ -70,7 +70,7 @@ public class TemplateExecutorTests {
@Test
public void testWithTheme() {
TemplateExecutor executor = new TemplateExecutor(themeResolver);
String template = "<foo; format=\"title\">";
String template = "<foo; format=\"style-title\">";
Map<String, Object> attributes = new HashMap<>();
attributes.put("foo", "bar");
AttributedString result = executor.render(template, attributes);
@@ -84,7 +84,7 @@ public class TemplateExecutorTests {
@Test
public void testNullDontStyle() {
TemplateExecutor executor = new TemplateExecutor(themeResolver);
String template = "<foo; format=\"list-key\">";
String template = "<foo; format=\"style-list-key\">";
Map<String, Object> attributes = new HashMap<>();
attributes.put("foo", "bar");
AttributedString result = executor.render(template, attributes);

View File

@@ -24,7 +24,7 @@ public class ThemeRegistryTests {
@Test
public void test() {
ThemeRegistry registry = new ThemeRegistry();
Theme theme = Theme.of("name1", ThemeSettings.themeSettings());
Theme theme = Theme.of("name1", ThemeSettings.defaults());
registry.register(theme);
assertThat(registry.get("name1")).isSameAs(theme);
assertThat(registry.get("name2")).isNull();

View File

@@ -34,11 +34,11 @@ public class ThemeResolverTests {
@Override
public ThemeSettings getSettings() {
return ThemeSettings.themeSettings();
return ThemeSettings.defaults();
}
});
ThemeResolver themeResolver = new ThemeResolver(themeRegistry, "default");
assertThat(themeResolver.resolveTag(ThemeSettings.TAG_TITLE)).isEqualTo("bold,fg:bright-white");
assertThat(themeResolver.resolveStyleTag(StyleSettings.TAG_TITLE)).isEqualTo("bold,fg:bright-white");
assertThat(themeResolver.resolveStyle("bold")).isEqualTo(AttributedStyle.BOLD);
assertThat(themeResolver.evaluateExpression("@{bold foo}"))
.isEqualTo(new AttributedString("foo", AttributedStyle.BOLD));

View File

@@ -29,8 +29,8 @@ public class ThemeSettingsTests {
@Test
public void testGeneratedStyle() {
ThemeSettings themeSettings = ThemeSettings.themeSettings();
String resolveTag = themeSettings.resolveTag(ThemeSettings.TAG_TITLE);
ThemeSettings themeSettings = ThemeSettings.defaults();
String resolveTag = themeSettings.styles().resolveTag(StyleSettings.TAG_TITLE);
assertThat(resolveTag).isEqualTo("bold,fg:bright-white");
StyleSource styleSource = new MemoryStyleSource();
@@ -42,22 +42,22 @@ public class ThemeSettingsTests {
@ParameterizedTest
@ValueSource(strings = {
ThemeSettings.TAG_TITLE,
ThemeSettings.TAG_VALUE,
ThemeSettings.TAG_LIST_KEY,
ThemeSettings.TAG_LIST_VALUE,
ThemeSettings.TAG_LEVEL_INFO,
ThemeSettings.TAG_LEVEL_WARN,
ThemeSettings.TAG_LEVEL_ERROR,
ThemeSettings.TAG_ITEM_ENABLED,
ThemeSettings.TAG_ITEM_DISABLED,
ThemeSettings.TAG_ITEM_SELECTED,
ThemeSettings.TAG_ITEM_UNSELECTED,
ThemeSettings.TAG_ITEM_SELECTOR,
ThemeSettings.TAG_HIGHLIGHT })
StyleSettings.TAG_TITLE,
StyleSettings.TAG_VALUE,
StyleSettings.TAG_LIST_KEY,
StyleSettings.TAG_LIST_VALUE,
StyleSettings.TAG_LEVEL_INFO,
StyleSettings.TAG_LEVEL_WARN,
StyleSettings.TAG_LEVEL_ERROR,
StyleSettings.TAG_ITEM_ENABLED,
StyleSettings.TAG_ITEM_DISABLED,
StyleSettings.TAG_ITEM_SELECTED,
StyleSettings.TAG_ITEM_UNSELECTED,
StyleSettings.TAG_ITEM_SELECTOR,
StyleSettings.TAG_HIGHLIGHT })
public void testTags(String tag) {
ThemeSettings themeSettings = ThemeSettings.themeSettings();
String resolveTag = themeSettings.resolveTag(ThemeSettings.TAG_TITLE);
ThemeSettings themeSettings = ThemeSettings.defaults();
String resolveTag = themeSettings.styles().resolveTag(StyleSettings.TAG_TITLE);
assertThat(resolveTag).isNotNull();
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.shell.samples.standard;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStringBuilder;
@@ -26,8 +27,9 @@ import org.jline.utils.AttributedStyle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.style.FigureSettings;
import org.springframework.shell.style.StyleSettings;
import org.springframework.shell.style.ThemeResolver;
import org.springframework.shell.style.ThemeSettings;
@ShellComponent
public class StyleCommands {
@@ -39,10 +41,10 @@ public class StyleCommands {
List<String> rgbRedHue = Arrays.asList("#ff0000", "#ff4000", "#ff8000", "#ffbf00", "#ffff00", "#bfff00", "#80ff00",
"#40ff00", "#00ff00", "#00ff40", "#00ff80", "#00ffbf", "#00ffff", "#00bfff", "#0080ff", "#0040ff",
"#0000ff", "#4000ff", "#8000ff", "#bf00ff", "#ff00ff", "#ff00bf", "#ff0080", "#ff0040", "#ff0000");
List<String> themeTags = Arrays.asList(ThemeSettings.TAG_TITLE, ThemeSettings.TAG_VALUE, ThemeSettings.TAG_LIST_KEY,
ThemeSettings.TAG_LIST_VALUE, ThemeSettings.TAG_LEVEL_INFO, ThemeSettings.TAG_LEVEL_WARN,
ThemeSettings.TAG_LEVEL_ERROR, ThemeSettings.TAG_ITEM_ENABLED, ThemeSettings.TAG_ITEM_DISABLED,
ThemeSettings.TAG_ITEM_SELECTED, ThemeSettings.TAG_ITEM_UNSELECTED, ThemeSettings.TAG_ITEM_SELECTOR);
List<String> themeTags = Arrays.asList(StyleSettings.TAG_TITLE, StyleSettings.TAG_VALUE, StyleSettings.TAG_LIST_KEY,
StyleSettings.TAG_LIST_VALUE, StyleSettings.TAG_LEVEL_INFO, StyleSettings.TAG_LEVEL_WARN,
StyleSettings.TAG_LEVEL_ERROR, StyleSettings.TAG_ITEM_ENABLED, StyleSettings.TAG_ITEM_DISABLED,
StyleSettings.TAG_ITEM_SELECTED, StyleSettings.TAG_ITEM_UNSELECTED, StyleSettings.TAG_ITEM_SELECTOR);
@Autowired
private ThemeResolver themeResolver;
@@ -82,14 +84,32 @@ public class StyleCommands {
AttributedStringBuilder builder = new AttributedStringBuilder();
themeTags.stream()
.forEach(tag -> {
String resolvedStyle = themeResolver.resolveTag(tag);
String resolvedStyle = themeResolver.resolveStyleTag(tag);
AttributedStyle style = themeResolver.resolveStyle(resolvedStyle);
AttributedString styledStr = new AttributedString(tag, style);
builder.append(String.format("%-25s", tag));
builder.append(" ");
builder.append(styledStr);
builder.append("\n");
});;
});
return builder.toAttributedString();
}
@ShellMethod(key = "style figure", value = "Showcase figures", group = "Styles")
public AttributedString stylesFigures() {
AttributedStringBuilder builder = new AttributedStringBuilder();
Stream.of(FigureSettings.tags())
.forEach(tag -> {
builder.append(String.format("%-25s", tag));
builder.append(" ");
String resolveFigureTag = themeResolver.resolveFigureTag(tag);
combinations3().stream().forEach(spec -> {
AttributedStyle style = themeResolver.resolveStyle(spec);
builder.append(" ");
builder.append(new AttributedString(resolveFigureTag, style));
});
builder.append("\n");
});
return builder.toAttributedString();
}
@@ -112,4 +132,16 @@ public class StyleCommands {
});
return styles;
}
private List<String> combinations3() {
List<String> styles = new ArrayList<>();
Arrays.asList("fg").stream().forEach(ground -> {
colors.stream().forEach(color -> {
named.stream().forEach(named -> {
styles.add(String.format("%s,%s:%s", named, ground, color));
});
});
});
return styles;
}
}

View File

@@ -1,6 +1,6 @@
// NAME
name(commandName, commandShortDesc) ::= <<
<("NAME"); format="highlight">
<("NAME"); format="style-highlight">
<commandName> - <commandShortDesc>
>>
@@ -18,8 +18,8 @@ synopsisOption(option) ::= <%
%>
synopsis(commandName, options) ::= <<
<("SYNOPSIS"); format="highlight">
<(commandName); format="highlight"> <options: { o | <synopsisOption(o)>}; separator=" ">
<("SYNOPSIS"); format="style-highlight">
<(commandName); format="style-highlight"> <options: { o | <synopsisOption(o)>}; separator=" ">
>>
// OPTIONS
@@ -53,7 +53,7 @@ option(option) ::= <<
>>
options(options) ::= <<
<("OPTIONS"); format="highlight">
<("OPTIONS"); format="style-highlight">
<options:{ o | <option(o)>}>
>>

View File

@@ -1,15 +1,15 @@
name() ::= <<
<("AVAILABLE COMMANDS"); format="highlight">
<("AVAILABLE COMMANDS"); format="style-highlight">
>>
command(command) ::= <<
<(command.name); format="highlight"><(":"); format="highlight"> <command.description>
<(command.name); format="style-highlight"><(":"); format="style-highlight"> <command.description>
>>
commandGroup(commandGroup) ::= <<
<(commandGroup.group); format="highlight">
<(commandGroup.group); format="style-highlight">
<commandGroup.commands:{ c | <command(c)>}>
>>

View File

@@ -228,7 +228,7 @@ public class HelpTests {
@Bean
public Help help() {
ThemeRegistry registry = new ThemeRegistry();
registry.register(Theme.of("default", ThemeSettings.themeSettings()));
registry.register(Theme.of("default", ThemeSettings.defaults()));
ThemeResolver resolver = new ThemeResolver(registry, "default");
TemplateExecutor executor = new TemplateExecutor(resolver);
Help help = new Help(executor);