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 = MultiItemSelectorContext.empty(getItemMapper());
|
||||||
currentContext.setName(name);
|
currentContext.setName(name);
|
||||||
|
currentContext.setTerminalWidth(getTerminal().getWidth());
|
||||||
if (currentContext.getItems() == null) {
|
if (currentContext.getItems() == null) {
|
||||||
currentContext.setItems(getItems());
|
currentContext.setItems(getItems());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ public class SingleItemSelector<T, I extends Nameable & Matchable & Enableable &
|
|||||||
}
|
}
|
||||||
currentContext = SingleItemSelectorContext.empty(getItemMapper());
|
currentContext = SingleItemSelectorContext.empty(getItemMapper());
|
||||||
currentContext.setName(name);
|
currentContext.setName(name);
|
||||||
|
currentContext.setTerminalWidth(getTerminal().getWidth());
|
||||||
if (currentContext.getItems() == null) {
|
if (currentContext.getItems() == null) {
|
||||||
currentContext.setItems(getItems());
|
currentContext.setItems(getItems());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import java.util.stream.Stream;
|
|||||||
public class BaseComponentContext<C extends ComponentContext<C>> extends LinkedHashMap<Object, Object>
|
public class BaseComponentContext<C extends ComponentContext<C>> extends LinkedHashMap<Object, Object>
|
||||||
implements ComponentContext<C> {
|
implements ComponentContext<C> {
|
||||||
|
|
||||||
|
private Integer terminalWidth;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object get(Object key) {
|
public Object get(Object key) {
|
||||||
Object o = super.get(key);
|
Object o = super.get(key);
|
||||||
@@ -60,12 +62,28 @@ public class BaseComponentContext<C extends ComponentContext<C>> extends LinkedH
|
|||||||
return entrySet().stream();
|
return entrySet().stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getTerminalWidth() {
|
||||||
|
return terminalWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTerminalWidth(Integer terminalWidth) {
|
||||||
|
this.terminalWidth = terminalWidth;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> toTemplateModel() {
|
public Map<String, Object> toTemplateModel() {
|
||||||
Map<String, Object> attributes = new HashMap<>();
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
// hardcoding enclosed map values into 'rawValues'
|
// hardcoding enclosed map values into 'rawValues'
|
||||||
// as it may contain anything
|
// as it may contain anything
|
||||||
attributes.put("rawValues", this);
|
attributes.put("rawValues", this);
|
||||||
|
attributes.put("terminalWidth", terminalWidth);
|
||||||
return attributes;
|
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();
|
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
|
* Gets context values as a map. Every context implementation can
|
||||||
* do their own model as essentially what matter is a one coming
|
* do their own model as essentially what matter is a one coming
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
package org.springframework.shell.style;
|
package org.springframework.shell.style;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.jline.style.StyleExpression;
|
import org.jline.style.StyleExpression;
|
||||||
import org.stringtemplate.v4.AttributeRenderer;
|
import org.stringtemplate.v4.AttributeRenderer;
|
||||||
@@ -33,6 +34,7 @@ import org.springframework.util.StringUtils;
|
|||||||
public class StringToStyleExpressionRenderer implements AttributeRenderer<String> {
|
public class StringToStyleExpressionRenderer implements AttributeRenderer<String> {
|
||||||
|
|
||||||
private final ThemeResolver themeResolver;
|
private final ThemeResolver themeResolver;
|
||||||
|
private final static String TRUNCATE = "truncate-";
|
||||||
|
|
||||||
public StringToStyleExpressionRenderer(ThemeResolver themeResolver) {
|
public StringToStyleExpressionRenderer(ThemeResolver themeResolver) {
|
||||||
Assert.notNull(themeResolver, "themeResolver must be set");
|
Assert.notNull(themeResolver, "themeResolver must be set");
|
||||||
@@ -44,8 +46,51 @@ public class StringToStyleExpressionRenderer implements AttributeRenderer<String
|
|||||||
if (!StringUtils.hasText(formatString)) {
|
if (!StringUtils.hasText(formatString)) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
else {
|
else if (formatString.startsWith("style-")) {
|
||||||
return String.format("@{%s %s}", themeResolver.resolveStyleTag(formatString), value);
|
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
|
// used to select style if item is selected/unselected
|
||||||
selected_style(flag) ::= <%
|
selected_style(flag) ::= <%
|
||||||
<if(flag)>style-item-selected<else>style-item-unselected<endif>
|
<if(flag)>style-item-selected<else>style-item-unselected<endif>
|
||||||
%>
|
%>
|
||||||
|
|
||||||
// selector rows
|
// selector rows
|
||||||
select_item(item) ::= <%
|
select_item(item,model) ::= <%
|
||||||
<if(item.onrow)>
|
<if(item.onrow)>
|
||||||
<({<figures.rightPointingQuotation> }); format="style-item-selector">
|
<({<figures.rightPointingQuotation> }); format="style-item-selector">
|
||||||
<else>
|
<else>
|
||||||
@@ -13,16 +18,16 @@ select_item(item) ::= <%
|
|||||||
|
|
||||||
<if(item.enabled)>
|
<if(item.enabled)>
|
||||||
<if(item.selected)>
|
<if(item.selected)>
|
||||||
<({<figures.checkboxOn> }); format=selected_style(item.selected)> <item.name>
|
<({<figures.checkboxOn> }); format=selected_style(item.selected)> <truncate(item.name,model)>
|
||||||
<else>
|
<else>
|
||||||
<({<figures.checkboxOff> }); format=selected_style(item.selected)> <item.name>
|
<({<figures.checkboxOff> }); format=selected_style(item.selected)> <truncate(item.name,model)>
|
||||||
<endif>
|
<endif>
|
||||||
<else>
|
<else>
|
||||||
<if(item.selected)>
|
<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>
|
<else>
|
||||||
<({<figures.checkboxOff> }); format="style-item-disabled"> <item.name; format="style-item-disabled">
|
<({<figures.checkboxOff> }); format="style-item-disabled"> <({<truncate(item.name,model)>}); format="style-item-disabled">
|
||||||
<endif>
|
<endif>
|
||||||
<endif>
|
<endif>
|
||||||
%>
|
%>
|
||||||
|
|
||||||
@@ -58,7 +63,7 @@ result(model) ::= <<
|
|||||||
// component is running
|
// component is running
|
||||||
running(model) ::= <<
|
running(model) ::= <<
|
||||||
<question_name(model)> <info(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
|
// main - hardcoded name
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
// selector rows
|
// selector rows
|
||||||
select_item(item) ::= <%
|
truncate(name,model) ::= <%
|
||||||
|
<name; format={truncate-width:<model.terminalWidth>-prefix:2}>
|
||||||
|
%>
|
||||||
|
|
||||||
|
select_item(item,model) ::= <%
|
||||||
<if(item.selected)>
|
<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>
|
<else>
|
||||||
<(" ")><item.name>
|
<(" ")><truncate(item.name,model)>
|
||||||
<endif>
|
<endif>
|
||||||
%>
|
%>
|
||||||
|
|
||||||
@@ -34,7 +38,7 @@ result(model) ::= <<
|
|||||||
// component is running
|
// component is running
|
||||||
running(model) ::= <<
|
running(model) ::= <<
|
||||||
<question_name(model)> <info(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
|
// main - hardcoded name
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ public abstract class AbstractShellTests {
|
|||||||
|
|
||||||
pipedInputStream.connect(pipedOutputStream);
|
pipedInputStream.connect(pipedOutputStream);
|
||||||
terminal = new DumbTerminal("terminal", "ansi", pipedInputStream, consoleOut, StandardCharsets.UTF_8);
|
terminal = new DumbTerminal("terminal", "ansi", pipedInputStream, consoleOut, StandardCharsets.UTF_8);
|
||||||
terminal.setSize(new Size(1, 1));
|
terminal.setSize(new Size(80, 24));
|
||||||
|
|
||||||
executorService.execute(() -> {
|
executorService.execute(() -> {
|
||||||
try {
|
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.
|
|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.AbstractShellComponent;
|
||||||
import org.springframework.shell.standard.ShellComponent;
|
import org.springframework.shell.standard.ShellComponent;
|
||||||
import org.springframework.shell.standard.ShellMethod;
|
import org.springframework.shell.standard.ShellMethod;
|
||||||
|
import org.springframework.shell.standard.ShellOption;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
@ShellComponent
|
@ShellComponent
|
||||||
@@ -75,10 +76,16 @@ public class ComponentCommands extends AbstractShellComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ShellMethod(key = "component single", value = "Single selector", group = "Components")
|
@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<>();
|
List<SelectorItem<String>> items = new ArrayList<>();
|
||||||
items.add(SelectorItem.of("key1", "value1"));
|
items.add(SelectorItem.of("key1", "value1"));
|
||||||
items.add(SelectorItem.of("key2", "value2"));
|
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(),
|
SingleItemSelector<String, SelectorItem<String>> component = new SingleItemSelector<>(getTerminal(),
|
||||||
items, "testSimple", null);
|
items, "testSimple", null);
|
||||||
component.setResourceLoader(getResourceLoader());
|
component.setResourceLoader(getResourceLoader());
|
||||||
@@ -90,11 +97,17 @@ public class ComponentCommands extends AbstractShellComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ShellMethod(key = "component multi", value = "Multi selector", group = "Components")
|
@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<>();
|
List<SelectorItem<String>> items = new ArrayList<>();
|
||||||
items.add(SelectorItem.of("key1", "value1"));
|
items.add(SelectorItem.of("key1", "value1"));
|
||||||
items.add(SelectorItem.of("key2", "value2", false, true));
|
items.add(SelectorItem.of("key2", "value2", false, true));
|
||||||
items.add(SelectorItem.of("key3", "value3"));
|
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(),
|
MultiItemSelector<String, SelectorItem<String>> component = new MultiItemSelector<>(getTerminal(),
|
||||||
items, "testSimple", null);
|
items, "testSimple", null);
|
||||||
component.setResourceLoader(getResourceLoader());
|
component.setResourceLoader(getResourceLoader());
|
||||||
|
|||||||
Reference in New Issue
Block a user