Skip to content

Commit feba345

Browse files
committed
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
1 parent 4c48017 commit feba345

File tree

11 files changed

+198
-15
lines changed

11 files changed

+198
-15
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public MultiItemSelectorContext<T, I> getThisContext(ComponentContext<?> context
5959
}
6060
currentContext = MultiItemSelectorContext.empty(getItemMapper());
6161
currentContext.setName(name);
62+
currentContext.setTerminalWidth(getTerminal().getWidth());
6263
if (currentContext.getItems() == null) {
6364
currentContext.setItems(getItems());
6465
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public SingleItemSelectorContext<T, I> getThisContext(ComponentContext<?> contex
5959
}
6060
currentContext = SingleItemSelectorContext.empty(getItemMapper());
6161
currentContext.setName(name);
62+
currentContext.setTerminalWidth(getTerminal().getWidth());
6263
if (currentContext.getItems() == null) {
6364
currentContext.setItems(getItems());
6465
}

spring-shell-core/src/main/java/org/springframework/shell/component/context/BaseComponentContext.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
public class BaseComponentContext<C extends ComponentContext<C>> extends LinkedHashMap<Object, Object>
3131
implements ComponentContext<C> {
3232

33+
private Integer terminalWidth;
34+
3335
@Override
3436
public Object get(Object key) {
3537
Object o = super.get(key);
@@ -60,12 +62,28 @@ public Stream<java.util.Map.Entry<Object, Object>> stream() {
6062
return entrySet().stream();
6163
}
6264

65+
@Override
66+
public Integer getTerminalWidth() {
67+
return terminalWidth;
68+
}
69+
70+
@Override
71+
public void setTerminalWidth(Integer terminalWidth) {
72+
this.terminalWidth = terminalWidth;
73+
}
74+
6375
@Override
6476
public Map<String, Object> toTemplateModel() {
6577
Map<String, Object> attributes = new HashMap<>();
6678
// hardcoding enclosed map values into 'rawValues'
6779
// as it may contain anything
6880
attributes.put("rawValues", this);
81+
attributes.put("terminalWidth", terminalWidth);
6982
return attributes;
7083
}
84+
85+
@Override
86+
public String toString() {
87+
return "BaseComponentContext [terminalWidth=" + terminalWidth + "]";
88+
}
7189
}

spring-shell-core/src/main/java/org/springframework/shell/component/context/ComponentContext.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,20 @@ static <C extends ComponentContext<C>> ComponentContext<C> empty() {
8080
*/
8181
Stream<Map.Entry<Object, Object>> stream();
8282

83+
/**
84+
* Get terminal width.
85+
*
86+
* @return a terminal width
87+
*/
88+
Integer getTerminalWidth();
89+
90+
/**
91+
* Set terminal width.
92+
*
93+
* @param terminalWidth the width
94+
*/
95+
void setTerminalWidth(Integer terminalWidth);
96+
8397
/**
8498
* Gets context values as a map. Every context implementation can
8599
* do their own model as essentially what matter is a one coming

spring-shell-core/src/main/java/org/springframework/shell/style/StringToStyleExpressionRenderer.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.shell.style;
1717

1818
import java.util.Locale;
19+
import java.util.stream.Stream;
1920

2021
import org.jline.style.StyleExpression;
2122
import org.stringtemplate.v4.AttributeRenderer;
@@ -33,6 +34,7 @@
3334
public class StringToStyleExpressionRenderer implements AttributeRenderer<String> {
3435

3536
private final ThemeResolver themeResolver;
37+
private final static String TRUNCATE = "truncate-";
3638

3739
public StringToStyleExpressionRenderer(ThemeResolver themeResolver) {
3840
Assert.notNull(themeResolver, "themeResolver must be set");
@@ -44,8 +46,51 @@ public String toString(String value, String formatString, Locale locale) {
4446
if (!StringUtils.hasText(formatString)) {
4547
return value;
4648
}
47-
else {
49+
else if (formatString.startsWith("style-")) {
4850
return String.format("@{%s %s}", themeResolver.resolveStyleTag(formatString), value);
4951
}
52+
else if (formatString.startsWith(TRUNCATE)) {
53+
String f = formatString.substring(TRUNCATE.length());
54+
TruncateValues config = mapValues(f);
55+
if (value.length() + config.prefix > config.width) {
56+
return String.format(locale, "%1." + (config.width - config.prefix - 2) + "s.." , value);
57+
}
58+
else {
59+
return value;
60+
}
61+
}
62+
else {
63+
return String.format(locale, formatString, value);
64+
}
65+
}
66+
67+
private static class TruncateValues {
68+
Integer width;
69+
Integer prefix;
70+
71+
public void setWidth(Integer width) {
72+
this.width = width;
73+
}
74+
public void setPrefix(Integer prefix) {
75+
this.prefix = prefix;
76+
}
77+
}
78+
79+
private static TruncateValues mapValues(String expression) {
80+
TruncateValues values = new TruncateValues();
81+
Stream.of(expression.split("-"))
82+
.map(String::trim)
83+
.forEach(v -> {
84+
String[] split = v.split(":", 2);
85+
if (split.length == 2) {
86+
if ("width".equals(split[0])) {
87+
values.setWidth(Integer.parseInt(split[1]));
88+
}
89+
else if ("prefix".equals(split[0])) {
90+
values.setPrefix(Integer.parseInt(split[1]));
91+
}
92+
}
93+
});
94+
return values;
5095
}
5196
}

spring-shell-core/src/main/resources/org/springframework/shell/component/multi-item-selector-default.stg

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
// selector rows
2+
truncate(name,model) ::= <%
3+
<name; format={truncate-width:<model.terminalWidth>-prefix:5}>
4+
%>
5+
16
// used to select style if item is selected/unselected
27
selected_style(flag) ::= <%
38
<if(flag)>style-item-selected<else>style-item-unselected<endif>
49
%>
510

611
// selector rows
7-
select_item(item) ::= <%
12+
select_item(item,model) ::= <%
813
<if(item.onrow)>
914
<({<figures.rightPointingQuotation> }); format="style-item-selector">
1015
<else>
@@ -13,16 +18,16 @@ select_item(item) ::= <%
1318

1419
<if(item.enabled)>
1520
<if(item.selected)>
16-
<({<figures.checkboxOn> }); format=selected_style(item.selected)> <item.name>
21+
<({<figures.checkboxOn> }); format=selected_style(item.selected)> <truncate(item.name,model)>
1722
<else>
18-
<({<figures.checkboxOff> }); format=selected_style(item.selected)> <item.name>
23+
<({<figures.checkboxOff> }); format=selected_style(item.selected)> <truncate(item.name,model)>
1924
<endif>
2025
<else>
2126
<if(item.selected)>
22-
<({<figures.checkboxOn> }); format="style-item-disabled"> <item.name; format="style-item-disabled">
27+
<({<figures.checkboxOn> }); format="style-item-disabled"> <({<truncate(item.name,model)>}); format="style-item-disabled">
2328
<else>
24-
<({<figures.checkboxOff> }); format="style-item-disabled"> <item.name; format="style-item-disabled">
25-
<endif>
29+
<({<figures.checkboxOff> }); format="style-item-disabled"> <({<truncate(item.name,model)>}); format="style-item-disabled">
30+
<endif>
2631
<endif>
2732
%>
2833

@@ -58,7 +63,7 @@ result(model) ::= <<
5863
// component is running
5964
running(model) ::= <<
6065
<question_name(model)> <info(model)>
61-
<model.rows:{x|<select_item(x)>}; separator="\n">
66+
<model.rows:{x|<select_item(x,model)>}; separator="\n">
6267
>>
6368

6469
// main - hardcoded name

spring-shell-core/src/main/resources/org/springframework/shell/component/single-item-selector-default.stg

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
// selector rows
2-
select_item(item) ::= <%
2+
truncate(name,model) ::= <%
3+
<name; format={truncate-width:<model.terminalWidth>-prefix:2}>
4+
%>
5+
6+
select_item(item,model) ::= <%
37
<if(item.selected)>
4-
<({<figures.rightPointingQuotation> }); format="style-item-selector"><item.name; format="style-item-selector">
8+
<({<figures.rightPointingQuotation> }); format="style-item-selector"><({<truncate(item.name,model)>}); format="style-item-selector">
59
<else>
6-
<(" ")><item.name>
10+
<(" ")><truncate(item.name,model)>
711
<endif>
812
%>
913

@@ -34,7 +38,7 @@ result(model) ::= <<
3438
// component is running
3539
running(model) ::= <<
3640
<question_name(model)> <info(model)>
37-
<model.rows:{x|<select_item(x)>}; separator="\n">
41+
<model.rows:{x|<select_item(x,model)>}; separator="\n">
3842
>>
3943

4044
// main - hardcoded name

spring-shell-core/src/test/java/org/springframework/shell/component/AbstractShellTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public ThemeSettings getSettings() {
8080

8181
pipedInputStream.connect(pipedOutputStream);
8282
terminal = new DumbTerminal("terminal", "ansi", pipedInputStream, consoleOut, StandardCharsets.UTF_8);
83-
terminal.setSize(new Size(1, 1));
83+
terminal.setSize(new Size(80, 24));
8484

8585
executorService.execute(() -> {
8686
try {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2022 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.style;
17+
18+
import java.util.Locale;
19+
import java.util.stream.Stream;
20+
21+
import org.junit.jupiter.api.BeforeAll;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.params.ParameterizedTest;
24+
import org.junit.jupiter.params.provider.Arguments;
25+
import org.junit.jupiter.params.provider.MethodSource;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
29+
class StringToStyleExpressionRendererTests {
30+
31+
private static Locale LOCALE = Locale.getDefault();
32+
private static StringToStyleExpressionRenderer renderer;
33+
34+
@BeforeAll
35+
static void setup() {
36+
ThemeRegistry themeRegistry = new ThemeRegistry();
37+
themeRegistry.register(new Theme() {
38+
@Override
39+
public String getName() {
40+
return "default";
41+
}
42+
43+
@Override
44+
public ThemeSettings getSettings() {
45+
return ThemeSettings.defaults();
46+
}
47+
});
48+
ThemeResolver themeResolver = new ThemeResolver(themeRegistry, "default");
49+
renderer = new StringToStyleExpressionRenderer(themeResolver);
50+
}
51+
52+
@Test
53+
void emptyFormatReturnsValue() {
54+
String value = renderer.toString("fake", null, LOCALE);
55+
assertThat(value).isEqualTo("fake");
56+
}
57+
58+
static Stream<Arguments> truncate() {
59+
return Stream.of(
60+
Arguments.of("0123456789", "truncate-width:6-prefix:2", "01.."),
61+
Arguments.of("0123456789", "truncate-width:6-prefix:0", "0123.."),
62+
Arguments.of("0123456789", "truncate-width:11-prefix:0", "0123456789")
63+
);
64+
}
65+
66+
@ParameterizedTest
67+
@MethodSource
68+
void truncate(String value, String expression, String expected) {
69+
assertThat(renderer.toString(value, expression, LOCALE)).isEqualTo(expected);
70+
}
71+
}

spring-shell-docs/src/main/asciidoc/using-shell-components-ui-render.adoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,14 @@ from a parent component types. The following tables show those context variables
8989
|The current cursor row in a selector.
9090

9191
|===
92+
93+
94+
[[componentcontext-template-variables]]
95+
.ComponentContext Template Variables
96+
|===
97+
|Key |Description
98+
99+
|`terminalWidth`
100+
|The width of terminal, type is _Integer_ and defaults to _NULL_ if not set.
101+
102+
|===

spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentCommands.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.springframework.shell.standard.AbstractShellComponent;
4040
import org.springframework.shell.standard.ShellComponent;
4141
import org.springframework.shell.standard.ShellMethod;
42+
import org.springframework.shell.standard.ShellOption;
4243
import org.springframework.util.StringUtils;
4344

4445
@ShellComponent
@@ -75,10 +76,16 @@ public String confirmationInput(boolean no) {
7576
}
7677

7778
@ShellMethod(key = "component single", value = "Single selector", group = "Components")
78-
public String singleSelector() {
79+
public String singleSelector(
80+
@ShellOption(defaultValue = ShellOption.NULL) Boolean longKeys
81+
) {
7982
List<SelectorItem<String>> items = new ArrayList<>();
8083
items.add(SelectorItem.of("key1", "value1"));
8184
items.add(SelectorItem.of("key2", "value2"));
85+
if (longKeys != null && longKeys == true) {
86+
items.add(SelectorItem.of("key3 long long long long long", "value3"));
87+
items.add(SelectorItem.of("key4 long long long long long long long long long long", "value4"));
88+
}
8289
SingleItemSelector<String, SelectorItem<String>> component = new SingleItemSelector<>(getTerminal(),
8390
items, "testSimple", null);
8491
component.setResourceLoader(getResourceLoader());
@@ -90,11 +97,17 @@ public String singleSelector() {
9097
}
9198

9299
@ShellMethod(key = "component multi", value = "Multi selector", group = "Components")
93-
public String multiSelector() {
100+
public String multiSelector(
101+
@ShellOption(defaultValue = ShellOption.NULL) Boolean longKeys
102+
) {
94103
List<SelectorItem<String>> items = new ArrayList<>();
95104
items.add(SelectorItem.of("key1", "value1"));
96105
items.add(SelectorItem.of("key2", "value2", false, true));
97106
items.add(SelectorItem.of("key3", "value3"));
107+
if (longKeys != null && longKeys == true) {
108+
items.add(SelectorItem.of("key4 long long long long long", "value4", false, true));
109+
items.add(SelectorItem.of("key5 long long long long long long long long long long", "value5"));
110+
}
98111
MultiItemSelector<String, SelectorItem<String>> component = new MultiItemSelector<>(getTerminal(),
99112
items, "testSimple", null);
100113
component.setResourceLoader(getResourceLoader());

0 commit comments

Comments
 (0)