Component text can be truncated
- BaseComponentContext has new field terminalWidth. - StringToStyleExpressionRenderer contains new format for "truncate-" prefixes and this is something what template can use to instruct max length based on terminal width. - Change single/multi selectors to use this feature. - Fixes #543
This commit is contained in:
@@ -59,6 +59,7 @@ public class MultiItemSelector<T, I extends Nameable & Matchable & Enableable &
|
||||
}
|
||||
currentContext = MultiItemSelectorContext.empty(getItemMapper());
|
||||
currentContext.setName(name);
|
||||
currentContext.setTerminalWidth(getTerminal().getWidth());
|
||||
if (currentContext.getItems() == null) {
|
||||
currentContext.setItems(getItems());
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ public class SingleItemSelector<T, I extends Nameable & Matchable & Enableable &
|
||||
}
|
||||
currentContext = SingleItemSelectorContext.empty(getItemMapper());
|
||||
currentContext.setName(name);
|
||||
currentContext.setTerminalWidth(getTerminal().getWidth());
|
||||
if (currentContext.getItems() == null) {
|
||||
currentContext.setItems(getItems());
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ import java.util.stream.Stream;
|
||||
public class BaseComponentContext<C extends ComponentContext<C>> extends LinkedHashMap<Object, Object>
|
||||
implements ComponentContext<C> {
|
||||
|
||||
private Integer terminalWidth;
|
||||
|
||||
@Override
|
||||
public Object get(Object key) {
|
||||
Object o = super.get(key);
|
||||
@@ -60,12 +62,28 @@ public class BaseComponentContext<C extends ComponentContext<C>> extends LinkedH
|
||||
return entrySet().stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getTerminalWidth() {
|
||||
return terminalWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTerminalWidth(Integer terminalWidth) {
|
||||
this.terminalWidth = terminalWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toTemplateModel() {
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
// hardcoding enclosed map values into 'rawValues'
|
||||
// as it may contain anything
|
||||
attributes.put("rawValues", this);
|
||||
attributes.put("terminalWidth", terminalWidth);
|
||||
return attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BaseComponentContext [terminalWidth=" + terminalWidth + "]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,20 @@ public interface ComponentContext<C extends ComponentContext<C>> {
|
||||
*/
|
||||
Stream<Map.Entry<Object, Object>> stream();
|
||||
|
||||
/**
|
||||
* Get terminal width.
|
||||
*
|
||||
* @return a terminal width
|
||||
*/
|
||||
Integer getTerminalWidth();
|
||||
|
||||
/**
|
||||
* Set terminal width.
|
||||
*
|
||||
* @param terminalWidth the width
|
||||
*/
|
||||
void setTerminalWidth(Integer terminalWidth);
|
||||
|
||||
/**
|
||||
* Gets context values as a map. Every context implementation can
|
||||
* do their own model as essentially what matter is a one coming
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.shell.style;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jline.style.StyleExpression;
|
||||
import org.stringtemplate.v4.AttributeRenderer;
|
||||
@@ -33,6 +34,7 @@ import org.springframework.util.StringUtils;
|
||||
public class StringToStyleExpressionRenderer implements AttributeRenderer<String> {
|
||||
|
||||
private final ThemeResolver themeResolver;
|
||||
private final static String TRUNCATE = "truncate-";
|
||||
|
||||
public StringToStyleExpressionRenderer(ThemeResolver themeResolver) {
|
||||
Assert.notNull(themeResolver, "themeResolver must be set");
|
||||
@@ -44,8 +46,51 @@ public class StringToStyleExpressionRenderer implements AttributeRenderer<String
|
||||
if (!StringUtils.hasText(formatString)) {
|
||||
return value;
|
||||
}
|
||||
else {
|
||||
else if (formatString.startsWith("style-")) {
|
||||
return String.format("@{%s %s}", themeResolver.resolveStyleTag(formatString), value);
|
||||
}
|
||||
else if (formatString.startsWith(TRUNCATE)) {
|
||||
String f = formatString.substring(TRUNCATE.length());
|
||||
TruncateValues config = mapValues(f);
|
||||
if (value.length() + config.prefix > config.width) {
|
||||
return String.format(locale, "%1." + (config.width - config.prefix - 2) + "s.." , value);
|
||||
}
|
||||
else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return String.format(locale, formatString, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TruncateValues {
|
||||
Integer width;
|
||||
Integer prefix;
|
||||
|
||||
public void setWidth(Integer width) {
|
||||
this.width = width;
|
||||
}
|
||||
public void setPrefix(Integer prefix) {
|
||||
this.prefix = prefix;
|
||||
}
|
||||
}
|
||||
|
||||
private static TruncateValues mapValues(String expression) {
|
||||
TruncateValues values = new TruncateValues();
|
||||
Stream.of(expression.split("-"))
|
||||
.map(String::trim)
|
||||
.forEach(v -> {
|
||||
String[] split = v.split(":", 2);
|
||||
if (split.length == 2) {
|
||||
if ("width".equals(split[0])) {
|
||||
values.setWidth(Integer.parseInt(split[1]));
|
||||
}
|
||||
else if ("prefix".equals(split[0])) {
|
||||
values.setPrefix(Integer.parseInt(split[1]));
|
||||
}
|
||||
}
|
||||
});
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
// selector rows
|
||||
truncate(name,model) ::= <%
|
||||
<name; format={truncate-width:<model.terminalWidth>-prefix:5}>
|
||||
%>
|
||||
|
||||
// used to select style if item is selected/unselected
|
||||
selected_style(flag) ::= <%
|
||||
<if(flag)>style-item-selected<else>style-item-unselected<endif>
|
||||
%>
|
||||
|
||||
// selector rows
|
||||
select_item(item) ::= <%
|
||||
select_item(item,model) ::= <%
|
||||
<if(item.onrow)>
|
||||
<({<figures.rightPointingQuotation> }); format="style-item-selector">
|
||||
<else>
|
||||
@@ -13,16 +18,16 @@ select_item(item) ::= <%
|
||||
|
||||
<if(item.enabled)>
|
||||
<if(item.selected)>
|
||||
<({<figures.checkboxOn> }); format=selected_style(item.selected)> <item.name>
|
||||
<({<figures.checkboxOn> }); format=selected_style(item.selected)> <truncate(item.name,model)>
|
||||
<else>
|
||||
<({<figures.checkboxOff> }); format=selected_style(item.selected)> <item.name>
|
||||
<({<figures.checkboxOff> }); format=selected_style(item.selected)> <truncate(item.name,model)>
|
||||
<endif>
|
||||
<else>
|
||||
<if(item.selected)>
|
||||
<({<figures.checkboxOn> }); format="style-item-disabled"> <item.name; format="style-item-disabled">
|
||||
<({<figures.checkboxOn> }); format="style-item-disabled"> <({<truncate(item.name,model)>}); format="style-item-disabled">
|
||||
<else>
|
||||
<({<figures.checkboxOff> }); format="style-item-disabled"> <item.name; format="style-item-disabled">
|
||||
<endif>
|
||||
<({<figures.checkboxOff> }); format="style-item-disabled"> <({<truncate(item.name,model)>}); format="style-item-disabled">
|
||||
<endif>
|
||||
<endif>
|
||||
%>
|
||||
|
||||
@@ -58,7 +63,7 @@ result(model) ::= <<
|
||||
// component is running
|
||||
running(model) ::= <<
|
||||
<question_name(model)> <info(model)>
|
||||
<model.rows:{x|<select_item(x)>}; separator="\n">
|
||||
<model.rows:{x|<select_item(x,model)>}; separator="\n">
|
||||
>>
|
||||
|
||||
// main - hardcoded name
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// selector rows
|
||||
select_item(item) ::= <%
|
||||
truncate(name,model) ::= <%
|
||||
<name; format={truncate-width:<model.terminalWidth>-prefix:2}>
|
||||
%>
|
||||
|
||||
select_item(item,model) ::= <%
|
||||
<if(item.selected)>
|
||||
<({<figures.rightPointingQuotation> }); format="style-item-selector"><item.name; format="style-item-selector">
|
||||
<({<figures.rightPointingQuotation> }); format="style-item-selector"><({<truncate(item.name,model)>}); format="style-item-selector">
|
||||
<else>
|
||||
<(" ")><item.name>
|
||||
<(" ")><truncate(item.name,model)>
|
||||
<endif>
|
||||
%>
|
||||
|
||||
@@ -34,7 +38,7 @@ result(model) ::= <<
|
||||
// component is running
|
||||
running(model) ::= <<
|
||||
<question_name(model)> <info(model)>
|
||||
<model.rows:{x|<select_item(x)>}; separator="\n">
|
||||
<model.rows:{x|<select_item(x,model)>}; separator="\n">
|
||||
>>
|
||||
|
||||
// main - hardcoded name
|
||||
|
||||
@@ -80,7 +80,7 @@ public abstract class AbstractShellTests {
|
||||
|
||||
pipedInputStream.connect(pipedOutputStream);
|
||||
terminal = new DumbTerminal("terminal", "ansi", pipedInputStream, consoleOut, StandardCharsets.UTF_8);
|
||||
terminal.setSize(new Size(1, 1));
|
||||
terminal.setSize(new Size(80, 24));
|
||||
|
||||
executorService.execute(() -> {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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.Locale;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class StringToStyleExpressionRendererTests {
|
||||
|
||||
private static Locale LOCALE = Locale.getDefault();
|
||||
private static StringToStyleExpressionRenderer renderer;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
ThemeRegistry themeRegistry = new ThemeRegistry();
|
||||
themeRegistry.register(new Theme() {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "default";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThemeSettings getSettings() {
|
||||
return ThemeSettings.defaults();
|
||||
}
|
||||
});
|
||||
ThemeResolver themeResolver = new ThemeResolver(themeRegistry, "default");
|
||||
renderer = new StringToStyleExpressionRenderer(themeResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyFormatReturnsValue() {
|
||||
String value = renderer.toString("fake", null, LOCALE);
|
||||
assertThat(value).isEqualTo("fake");
|
||||
}
|
||||
|
||||
static Stream<Arguments> truncate() {
|
||||
return Stream.of(
|
||||
Arguments.of("0123456789", "truncate-width:6-prefix:2", "01.."),
|
||||
Arguments.of("0123456789", "truncate-width:6-prefix:0", "0123.."),
|
||||
Arguments.of("0123456789", "truncate-width:11-prefix:0", "0123456789")
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void truncate(String value, String expression, String expected) {
|
||||
assertThat(renderer.toString(value, expression, LOCALE)).isEqualTo(expected);
|
||||
}
|
||||
}
|
||||
@@ -89,3 +89,14 @@ from a parent component types. The following tables show those context variables
|
||||
|The current cursor row in a selector.
|
||||
|
||||
|===
|
||||
|
||||
|
||||
[[componentcontext-template-variables]]
|
||||
.ComponentContext Template Variables
|
||||
|===
|
||||
|Key |Description
|
||||
|
||||
|`terminalWidth`
|
||||
|The width of terminal, type is _Integer_ and defaults to _NULL_ if not set.
|
||||
|
||||
|===
|
||||
|
||||
@@ -39,6 +39,7 @@ import org.springframework.shell.component.support.SelectorItem;
|
||||
import org.springframework.shell.standard.AbstractShellComponent;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
import org.springframework.shell.standard.ShellOption;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@ShellComponent
|
||||
@@ -75,10 +76,16 @@ public class ComponentCommands extends AbstractShellComponent {
|
||||
}
|
||||
|
||||
@ShellMethod(key = "component single", value = "Single selector", group = "Components")
|
||||
public String singleSelector() {
|
||||
public String singleSelector(
|
||||
@ShellOption(defaultValue = ShellOption.NULL) Boolean longKeys
|
||||
) {
|
||||
List<SelectorItem<String>> items = new ArrayList<>();
|
||||
items.add(SelectorItem.of("key1", "value1"));
|
||||
items.add(SelectorItem.of("key2", "value2"));
|
||||
if (longKeys != null && longKeys == true) {
|
||||
items.add(SelectorItem.of("key3 long long long long long", "value3"));
|
||||
items.add(SelectorItem.of("key4 long long long long long long long long long long", "value4"));
|
||||
}
|
||||
SingleItemSelector<String, SelectorItem<String>> component = new SingleItemSelector<>(getTerminal(),
|
||||
items, "testSimple", null);
|
||||
component.setResourceLoader(getResourceLoader());
|
||||
@@ -90,11 +97,17 @@ public class ComponentCommands extends AbstractShellComponent {
|
||||
}
|
||||
|
||||
@ShellMethod(key = "component multi", value = "Multi selector", group = "Components")
|
||||
public String multiSelector() {
|
||||
public String multiSelector(
|
||||
@ShellOption(defaultValue = ShellOption.NULL) Boolean longKeys
|
||||
) {
|
||||
List<SelectorItem<String>> items = new ArrayList<>();
|
||||
items.add(SelectorItem.of("key1", "value1"));
|
||||
items.add(SelectorItem.of("key2", "value2", false, true));
|
||||
items.add(SelectorItem.of("key3", "value3"));
|
||||
if (longKeys != null && longKeys == true) {
|
||||
items.add(SelectorItem.of("key4 long long long long long", "value4", false, true));
|
||||
items.add(SelectorItem.of("key5 long long long long long long long long long long", "value5"));
|
||||
}
|
||||
MultiItemSelector<String, SelectorItem<String>> component = new MultiItemSelector<>(getTerminal(),
|
||||
items, "testSimple", null);
|
||||
component.setResourceLoader(getResourceLoader());
|
||||
|
||||
Reference in New Issue
Block a user