Components can't use interactive mode without tty
- Adding a concept of no-tty which in this commit simply tracks DumbTerminal as jline creates that if there nothing better. - For components without tty don't go to interaction loop. - For new sample show that we can at least manually handle required option with a flow while command option is not required. - Fixes #444
This commit is contained in:
@@ -210,6 +210,10 @@ public interface CommandExecution {
|
||||
this.parserExceptions = parserExceptions;
|
||||
}
|
||||
|
||||
public static CommandParserExceptionsException of(String message, List<CommandParserException> parserExceptions) {
|
||||
return new CommandParserExceptionsException(message, parserExceptions);
|
||||
}
|
||||
|
||||
public List<CommandParserException> getParserExceptions() {
|
||||
return parserExceptions;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import org.jline.keymap.BindingReader;
|
||||
import org.jline.keymap.KeyMap;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext;
|
||||
import org.springframework.shell.component.context.ComponentContext;
|
||||
@@ -39,6 +41,7 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
public class ConfirmationInput extends AbstractTextComponent<Boolean, ConfirmationInputContext> {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(ConfirmationInput.class);
|
||||
private final boolean defaultValue;
|
||||
private ConfirmationInputContext currentContext;
|
||||
|
||||
@@ -78,6 +81,10 @@ public class ConfirmationInput extends AbstractTextComponent<Boolean, Confirmati
|
||||
@Override
|
||||
protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, ConfirmationInputContext context) {
|
||||
String operation = bindingReader.readBinding(keyMap);
|
||||
log.debug("Binding read result {}", operation);
|
||||
if (operation == null) {
|
||||
return true;
|
||||
}
|
||||
String input;
|
||||
switch (operation) {
|
||||
case OPERATION_CHAR:
|
||||
|
||||
@@ -70,7 +70,10 @@ public class MultiItemSelector<T, I extends Nameable & Matchable & Enableable &
|
||||
@Override
|
||||
protected MultiItemSelectorContext<T, I> runInternal(MultiItemSelectorContext<T, I> context) {
|
||||
super.runInternal(context);
|
||||
loop(context);
|
||||
// if there's no tty don't try to loop as it would then cause user interaction
|
||||
if (hasTty()) {
|
||||
loop(context);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ import org.jline.keymap.BindingReader;
|
||||
import org.jline.keymap.KeyMap;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.shell.component.PathInput.PathInputContext;
|
||||
import org.springframework.shell.component.context.ComponentContext;
|
||||
@@ -42,6 +44,7 @@ import org.springframework.util.StringUtils;;
|
||||
*/
|
||||
public class PathInput extends AbstractTextComponent<Path, PathInputContext> {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(PathInput.class);
|
||||
private PathInputContext currentContext;
|
||||
private Function<String, Path> pathProvider = (path) -> Paths.get(path);
|
||||
|
||||
@@ -75,6 +78,10 @@ public class PathInput extends AbstractTextComponent<Path, PathInputContext> {
|
||||
@Override
|
||||
protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, PathInputContext context) {
|
||||
String operation = bindingReader.readBinding(keyMap);
|
||||
log.debug("Binding read result {}", operation);
|
||||
if (operation == null) {
|
||||
return true;
|
||||
}
|
||||
String input;
|
||||
switch (operation) {
|
||||
case OPERATION_CHAR:
|
||||
|
||||
@@ -70,7 +70,10 @@ public class SingleItemSelector<T, I extends Nameable & Matchable & Enableable &
|
||||
@Override
|
||||
protected SingleItemSelectorContext<T, I> runInternal(SingleItemSelectorContext<T, I> context) {
|
||||
super.runInternal(context);
|
||||
loop(context);
|
||||
// if there's no tty don't try to loop as it would then cause user interaction
|
||||
if (hasTty()) {
|
||||
loop(context);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ import org.jline.keymap.BindingReader;
|
||||
import org.jline.keymap.KeyMap;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.shell.component.StringInput.StringInputContext;
|
||||
import org.springframework.shell.component.context.ComponentContext;
|
||||
@@ -38,6 +40,7 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
public class StringInput extends AbstractTextComponent<String, StringInputContext> {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(StringInput.class);
|
||||
private final String defaultValue;
|
||||
private StringInputContext currentContext;
|
||||
private Character maskCharacter;
|
||||
@@ -83,6 +86,10 @@ public class StringInput extends AbstractTextComponent<String, StringInputContex
|
||||
@Override
|
||||
protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, StringInputContext context) {
|
||||
String operation = bindingReader.readBinding(keyMap);
|
||||
log.debug("Binding read result {}", operation);
|
||||
if (operation == null) {
|
||||
return true;
|
||||
}
|
||||
String input;
|
||||
switch (operation) {
|
||||
case OPERATION_CHAR:
|
||||
|
||||
@@ -56,6 +56,14 @@ public interface ComponentContext<C extends ComponentContext<C>> {
|
||||
*/
|
||||
<T> T get(Object key, Class<T> type);
|
||||
|
||||
/**
|
||||
* Check if a context contains a key.
|
||||
*
|
||||
* @param key the key
|
||||
* @return true if context contains key
|
||||
*/
|
||||
boolean containsKey(Object key);
|
||||
|
||||
/**
|
||||
* Put an entry into a context.
|
||||
*
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.jline.keymap.KeyMap;
|
||||
import org.jline.terminal.Attributes;
|
||||
import org.jline.terminal.Size;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.terminal.impl.DumbTerminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.Display;
|
||||
import org.jline.utils.InfoCmp.Capability;
|
||||
@@ -153,7 +154,8 @@ public abstract class AbstractComponent<T extends ComponentContext<T>> implement
|
||||
context = runPreRunHandlers(getThisContext(context));
|
||||
T run = runInternal(getThisContext(context));
|
||||
context = runPostRunHandlers(getThisContext(context));
|
||||
if (printResults) {
|
||||
// if there's no tty don't try to print results as it'd be pointless
|
||||
if (printResults && hasTty()) {
|
||||
printResults(context);
|
||||
}
|
||||
return run;
|
||||
@@ -186,6 +188,22 @@ public abstract class AbstractComponent<T extends ComponentContext<T>> implement
|
||||
this.templateLocation = templateLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this component has an existing {@code tty}.
|
||||
*
|
||||
* @return true if component has tty
|
||||
*/
|
||||
protected boolean hasTty() {
|
||||
boolean hasTty = true;
|
||||
if (this.terminal instanceof DumbTerminal) {
|
||||
if (this.terminal.getSize().getRows() == 0) {
|
||||
hasTty = false;
|
||||
}
|
||||
}
|
||||
log.debug("Terminal is {} with size {}, marking hasTty as {}", this.terminal, this.terminal.getSize(), hasTty);
|
||||
return hasTty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a given template with attributes.
|
||||
*
|
||||
|
||||
@@ -26,6 +26,8 @@ import org.jline.keymap.BindingReader;
|
||||
import org.jline.keymap.KeyMap;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.InfoCmp.Capability;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.shell.component.context.BaseComponentContext;
|
||||
import org.springframework.shell.component.context.ComponentContext;
|
||||
@@ -46,6 +48,7 @@ import static org.jline.keymap.KeyMap.key;
|
||||
public abstract class AbstractSelectorComponent<T, C extends SelectorComponentContext<T, I, C>, I extends Nameable & Matchable & Enableable & Itemable<T>>
|
||||
extends AbstractComponent<C> {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(AbstractSelectorComponent.class);
|
||||
protected final String name;
|
||||
private final List<I> items;
|
||||
private Comparator<I> comparator = (o1, o2) -> 0;
|
||||
@@ -155,6 +158,10 @@ public abstract class AbstractSelectorComponent<T, C extends SelectorComponentCo
|
||||
ItemStateViewProjection buildItemStateView = buildItemStateView(start.get(), thisContext);
|
||||
List<ItemState<I>> itemStateView = buildItemStateView.items;
|
||||
String operation = bindingReader.readBinding(keyMap);
|
||||
log.debug("Binding read result {}", operation);
|
||||
if (operation == null) {
|
||||
return true;
|
||||
}
|
||||
String input;
|
||||
switch (operation) {
|
||||
case OPERATION_SELECT:
|
||||
|
||||
@@ -66,7 +66,10 @@ public abstract class AbstractTextComponent<T, C extends TextComponentContext<T,
|
||||
|
||||
@Override
|
||||
protected C runInternal(C context) {
|
||||
loop(context);
|
||||
// if there's no tty don't try to loop as it would then cause user interaction
|
||||
if (hasTty()) {
|
||||
loop(context);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import org.jline.keymap.KeyMap;
|
||||
import org.jline.terminal.Size;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.terminal.impl.DumbTerminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
@@ -79,6 +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));
|
||||
|
||||
executorService.execute(() -> {
|
||||
try {
|
||||
|
||||
@@ -15,13 +15,17 @@
|
||||
*/
|
||||
package org.springframework.shell.component;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.jline.terminal.impl.DumbTerminal;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -55,6 +59,33 @@ public class ConfirmationInputTests extends AbstractShellTests {
|
||||
service = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNoTty() throws Exception {
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
DumbTerminal dumbTerminal = new DumbTerminal("terminal", "ansi", in, out, StandardCharsets.UTF_8);
|
||||
|
||||
ComponentContext<?> empty = ComponentContext.empty();
|
||||
ConfirmationInput component1 = new ConfirmationInput(dumbTerminal, "component1");
|
||||
component1.setResourceLoader(new DefaultResourceLoader());
|
||||
component1.setTemplateExecutor(getTemplateExecutor());
|
||||
|
||||
service.execute(() -> {
|
||||
ConfirmationInputContext run1Context = component1.run(empty);
|
||||
result1.set(run1Context);
|
||||
latch1.countDown();
|
||||
});
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
latch1.await(2, TimeUnit.SECONDS);
|
||||
ConfirmationInputContext run1Context = result1.get();
|
||||
|
||||
assertThat(run1Context).isNotNull();
|
||||
assertThat(run1Context.getResultValue()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResultUserInputEnterDefaultYes() throws InterruptedException, IOException {
|
||||
ComponentContext<?> empty = ComponentContext.empty();
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package org.springframework.shell.component;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -25,6 +28,8 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.terminal.impl.DumbTerminal;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -75,6 +80,19 @@ public class MultiItemSelectorTests extends AbstractShellTests {
|
||||
service = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNoTty() throws Exception {
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
DumbTerminal dumbTerminal = new DumbTerminal("terminal", "ansi", in, out, StandardCharsets.UTF_8);
|
||||
|
||||
scheduleSelect(dumbTerminal);
|
||||
awaitLatch();
|
||||
|
||||
List<SelectorItem<SimplePojo>> selected = result.get();
|
||||
assertThat(selected).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testItemsShown() {
|
||||
scheduleSelect();
|
||||
@@ -192,12 +210,21 @@ public class MultiItemSelectorTests extends AbstractShellTests {
|
||||
SELECTOR_ITEM_4));
|
||||
}
|
||||
|
||||
private void scheduleSelect(Terminal terminal) {
|
||||
scheduleSelect(Arrays.asList(SELECTOR_ITEM_1, SELECTOR_ITEM_2, SELECTOR_ITEM_3, SELECTOR_ITEM_4), null,
|
||||
terminal);
|
||||
}
|
||||
|
||||
private void scheduleSelect(List<SelectorItem<SimplePojo>> items) {
|
||||
scheduleSelect(items, null);
|
||||
}
|
||||
|
||||
private void scheduleSelect(List<SelectorItem<SimplePojo>> items, Integer maxItems) {
|
||||
MultiItemSelector<SimplePojo, SelectorItem<SimplePojo>> selector = new MultiItemSelector<>(getTerminal(),
|
||||
scheduleSelect(items, maxItems, getTerminal());
|
||||
}
|
||||
|
||||
private void scheduleSelect(List<SelectorItem<SimplePojo>> items, Integer maxItems, Terminal terminal) {
|
||||
MultiItemSelector<SimplePojo, SelectorItem<SimplePojo>> selector = new MultiItemSelector<>(terminal,
|
||||
items, "testSimple", null);
|
||||
selector.setResourceLoader(new DefaultResourceLoader());
|
||||
selector.setTemplateExecutor(getTemplateExecutor());
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
*/
|
||||
package org.springframework.shell.component;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -27,6 +30,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.google.common.jimfs.Jimfs;
|
||||
import org.jline.terminal.impl.DumbTerminal;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -69,6 +73,36 @@ public class PathInputTests extends AbstractShellTests {
|
||||
pathProvider = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNoTty() throws Exception {
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
DumbTerminal dumbTerminal = new DumbTerminal("terminal", "ansi", in, out, StandardCharsets.UTF_8);
|
||||
|
||||
Path path = fileSystem.getPath("tmp");
|
||||
Files.createDirectories(path);
|
||||
ComponentContext<?> empty = ComponentContext.empty();
|
||||
PathInput component1 = new PathInput(dumbTerminal, "component1");
|
||||
component1.setPathProvider(pathProvider);
|
||||
component1.setResourceLoader(new DefaultResourceLoader());
|
||||
component1.setTemplateExecutor(getTemplateExecutor());
|
||||
|
||||
service.execute(() -> {
|
||||
PathInputContext run1Context = component1.run(empty);
|
||||
result1.set(run1Context);
|
||||
latch1.countDown();
|
||||
});
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().append("tmp").cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
latch1.await(2, TimeUnit.SECONDS);
|
||||
PathInputContext run1Context = result1.get();
|
||||
|
||||
assertThat(run1Context).isNotNull();
|
||||
assertThat(run1Context.getResultValue()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResultUserInput() throws InterruptedException, IOException {
|
||||
Path path = fileSystem.getPath("tmp");
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package org.springframework.shell.component;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -25,6 +28,8 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.terminal.impl.DumbTerminal;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -92,6 +97,19 @@ public class SingleItemSelectorTests extends AbstractShellTests {
|
||||
"simplePojo3", "simplePojo4", "simplePojo5", "simplePojo6"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNoTty() throws Exception {
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
DumbTerminal dumbTerminal = new DumbTerminal("terminal", "ansi", in, out, StandardCharsets.UTF_8);
|
||||
|
||||
scheduleSelect(dumbTerminal);
|
||||
awaitLatch();
|
||||
|
||||
Optional<SelectorItem<SimplePojo>> selected = result.get();
|
||||
assertThat(selected).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectFirst() throws InterruptedException {
|
||||
scheduleSelect();
|
||||
@@ -156,9 +174,13 @@ public class SingleItemSelectorTests extends AbstractShellTests {
|
||||
assertThat(selected).isNotEmpty();
|
||||
}
|
||||
|
||||
private void scheduleSelect(Terminal terminal) {
|
||||
scheduleSelect(Arrays.asList(SELECTOR_ITEM_1, SELECTOR_ITEM_2, SELECTOR_ITEM_3, SELECTOR_ITEM_4), null,
|
||||
terminal);
|
||||
}
|
||||
|
||||
private void scheduleSelect() {
|
||||
scheduleSelect(Arrays.asList(SELECTOR_ITEM_1, SELECTOR_ITEM_2, SELECTOR_ITEM_3,
|
||||
SELECTOR_ITEM_4));
|
||||
scheduleSelect(Arrays.asList(SELECTOR_ITEM_1, SELECTOR_ITEM_2, SELECTOR_ITEM_3, SELECTOR_ITEM_4));
|
||||
}
|
||||
|
||||
private void scheduleSelect(List<SelectorItem<SimplePojo>> items) {
|
||||
@@ -166,7 +188,11 @@ public class SingleItemSelectorTests extends AbstractShellTests {
|
||||
}
|
||||
|
||||
private void scheduleSelect(List<SelectorItem<SimplePojo>> items, Integer maxItems) {
|
||||
SingleItemSelector<SimplePojo, SelectorItem<SimplePojo>> selector = new SingleItemSelector<>(getTerminal(),
|
||||
scheduleSelect(items, maxItems, getTerminal());
|
||||
}
|
||||
|
||||
private void scheduleSelect(List<SelectorItem<SimplePojo>> items, Integer maxItems, Terminal terminal) {
|
||||
SingleItemSelector<SimplePojo, SelectorItem<SimplePojo>> selector = new SingleItemSelector<>(terminal,
|
||||
items, "testSimple", null);
|
||||
selector.setResourceLoader(new DefaultResourceLoader());
|
||||
selector.setTemplateExecutor(getTemplateExecutor());
|
||||
|
||||
@@ -15,12 +15,16 @@
|
||||
*/
|
||||
package org.springframework.shell.component;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.jline.terminal.impl.DumbTerminal;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -60,6 +64,34 @@ public class StringInputTests extends AbstractShellTests {
|
||||
service = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNoTty() throws Exception {
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
DumbTerminal dumbTerminal = new DumbTerminal("terminal", "ansi", in, out, StandardCharsets.UTF_8);
|
||||
|
||||
ComponentContext<?> empty = ComponentContext.empty();
|
||||
StringInput component1 = new StringInput(dumbTerminal, "component1", "component1ResultValue");
|
||||
component1.setPrintResults(true);
|
||||
component1.setResourceLoader(new DefaultResourceLoader());
|
||||
component1.setTemplateExecutor(getTemplateExecutor());
|
||||
|
||||
service.execute(() -> {
|
||||
StringInputContext run1Context = component1.run(empty);
|
||||
result1.set(run1Context);
|
||||
latch1.countDown();
|
||||
});
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
latch1.await(2, TimeUnit.SECONDS);
|
||||
StringInputContext run1Context = result1.get();
|
||||
|
||||
assertThat(run1Context).isNotNull();
|
||||
assertThat(run1Context.getResultValue()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResultBasic() throws InterruptedException {
|
||||
ComponentContext<?> empty = ComponentContext.empty();
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import org.jline.keymap.KeyMap;
|
||||
import org.jline.terminal.Size;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.terminal.impl.DumbTerminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
@@ -79,6 +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));
|
||||
|
||||
executorService.execute(() -> {
|
||||
try {
|
||||
|
||||
@@ -23,15 +23,23 @@ import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.jline.terminal.impl.DumbTerminal;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.shell.command.CommandExecution.CommandParserExceptionsException;
|
||||
import org.springframework.shell.command.CommandParser;
|
||||
import org.springframework.shell.command.CommandParser.CommandParserException;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.component.flow.ComponentFlow;
|
||||
import org.springframework.shell.component.flow.ComponentFlow.ComponentFlowResult;
|
||||
import org.springframework.shell.component.flow.ResultMode;
|
||||
import org.springframework.shell.component.flow.SelectItem;
|
||||
import org.springframework.shell.component.flow.ComponentFlow.ComponentFlowResult;
|
||||
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
|
||||
public class ComponentFlowCommands extends AbstractShellComponent {
|
||||
@@ -133,6 +141,117 @@ public class ComponentFlowCommands extends AbstractShellComponent {
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommandRegistration showcaseRegistration() {
|
||||
return CommandRegistration.builder()
|
||||
.command("flow", "showcase3")
|
||||
.description("Showcase")
|
||||
.withOption()
|
||||
.longNames("field1")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("field2")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("confirmation1")
|
||||
.type(Boolean.class)
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("path1")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("single1")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("multi1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {
|
||||
|
||||
String field1 = ctx.getOptionValue("field1");
|
||||
String field2 = ctx.getOptionValue("field2");
|
||||
Boolean confirmation1 = ctx.getOptionValue("confirmation1");
|
||||
String path1 = ctx.getOptionValue("path1");
|
||||
String single1 = ctx.getOptionValue("single1");
|
||||
String asdf = ctx.getOptionValue("multi1");
|
||||
List<String> multi1 = new ArrayList<>();
|
||||
if (StringUtils.hasText(asdf)) {
|
||||
multi1.add(asdf);
|
||||
}
|
||||
|
||||
Map<String, String> single1SelectItems = new HashMap<>();
|
||||
single1SelectItems.put("key1", "value1");
|
||||
single1SelectItems.put("key2", "value2");
|
||||
List<SelectItem> multi1SelectItems = Arrays.asList(SelectItem.of("key1", "value1"),
|
||||
SelectItem.of("key2", "value2"), SelectItem.of("key3", "value3"));
|
||||
ComponentFlow flow = componentFlowBuilder.clone().reset()
|
||||
.withStringInput("field1")
|
||||
.name("Field1")
|
||||
.defaultValue("defaultField1Value")
|
||||
.resultValue(field1)
|
||||
.resultMode(ResultMode.ACCEPT)
|
||||
.and()
|
||||
.withStringInput("field2")
|
||||
.name("Field2")
|
||||
.resultValue(field2)
|
||||
.resultMode(ResultMode.ACCEPT)
|
||||
.and()
|
||||
.withConfirmationInput("confirmation1")
|
||||
.name("Confirmation1")
|
||||
.resultValue(confirmation1)
|
||||
.resultMode(ResultMode.ACCEPT)
|
||||
.and()
|
||||
.withPathInput("path1")
|
||||
.name("Path1")
|
||||
.resultValue(path1)
|
||||
.resultMode(ResultMode.ACCEPT)
|
||||
.and()
|
||||
.withSingleItemSelector("single1")
|
||||
.name("Single1")
|
||||
.selectItems(single1SelectItems)
|
||||
.resultValue(single1)
|
||||
.resultMode(ResultMode.ACCEPT)
|
||||
.and()
|
||||
.withMultiItemSelector("multi1")
|
||||
.name("Multi1")
|
||||
.selectItems(multi1SelectItems)
|
||||
.resultValues(multi1)
|
||||
.resultMode(ResultMode.ACCEPT)
|
||||
.and()
|
||||
.build();
|
||||
ComponentFlowResult result = flow.run();
|
||||
|
||||
boolean hasTty = !((ctx.getTerminal() instanceof DumbTerminal) && ctx.getTerminal().getSize().getRows() == 0);
|
||||
if (hasTty) {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
result.getContext().stream().forEach(e -> {
|
||||
buf.append(e.getKey());
|
||||
buf.append(" = ");
|
||||
buf.append(e.getValue());
|
||||
buf.append("\n");
|
||||
});
|
||||
ctx.getTerminal().writer().print(buf.toString());
|
||||
ctx.getTerminal().writer().flush();
|
||||
}
|
||||
else {
|
||||
List<CommandParser.CommandParserException> errors = new ArrayList<>();
|
||||
result.getContext().stream().forEach(e -> {
|
||||
if (e.getValue() == null) {
|
||||
errors.add(CommandParserException.of(String.format("Missing option, longnames='%s'", e.getKey())));
|
||||
}
|
||||
});
|
||||
if (!result.getContext().containsKey("single1")) {
|
||||
errors.add(CommandParserException.of("Missing option, longnames='single'"));
|
||||
}
|
||||
if (!errors.isEmpty()) {
|
||||
throw CommandParserExceptionsException.of("Missing options", errors);
|
||||
}
|
||||
}
|
||||
})
|
||||
.and()
|
||||
.build();
|
||||
}
|
||||
|
||||
@ShellMethod(key = "flow conditional", value = "Second component based on first", group = "Flow")
|
||||
public void conditional() {
|
||||
Map<String, String> single1SelectItems = new HashMap<>();
|
||||
|
||||
Reference in New Issue
Block a user