diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/ViewComponent.java b/spring-shell-core/src/main/java/org/springframework/shell/component/ViewComponent.java index ca3e32e62..ce30d32b6 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/ViewComponent.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/ViewComponent.java @@ -59,6 +59,7 @@ public ViewComponent(TerminalUI terminalUI, Terminal terminal, ViewComponentExec this.view = view; this.viewComponentExecutor = viewComponentExecutor; this.eventLoop = terminalUI.getEventLoop(); + view.setEventLoop(this.eventLoop); } /** diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ProgressView.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ProgressView.java index dfbc65fdb..985ca1034 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ProgressView.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ProgressView.java @@ -15,22 +15,33 @@ */ package org.springframework.shell.component.view.control; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.UUID; import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.publisher.Flux; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; import org.springframework.shell.component.message.ShellMessageBuilder; +import org.springframework.shell.component.message.ShellMessageHeaderAccessor; +import org.springframework.shell.component.message.StaticShellMessageHeaderAccessor; import org.springframework.shell.component.view.control.cell.TextCell; +import org.springframework.shell.component.view.event.EventLoop; import org.springframework.shell.component.view.screen.Screen; import org.springframework.shell.geom.HorizontalAlign; import org.springframework.shell.geom.Rectangle; import org.springframework.shell.style.SpinnerSettings; import org.springframework.shell.style.ThemeResolver; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; /** * {@code ProgressView} is used to show a progress indicator. @@ -100,26 +111,36 @@ public class ProgressView extends BoxView { }; /** - * Construct view with {@code tickStart 0} and {@code tickEnd 100}. + * Construct view with {@code tickStart 0} and {@code tickEnd 100}. Uses default + * {@link ProgressViewItem}s. */ public ProgressView() { this(0, 100); } /** - * Construct view with given bounds for {@code tickStart} and {@code tickEnd}. - * {@code tickStart} needs to be equal or more than zero. {@code tickEnd} needs - * to be higher than {@code tickStart}. Defines default items for {@code text}, - * {@code spinner} and {@code percent}. + * Construct view with {@code tickStart 0} and {@code tickEnd 100}. Uses default + * {@link ProgressViewItem}s. * * @param tickStart the tick start - * @param tickEnd the tick end + * @param tickEnd the tick end */ public ProgressView(int tickStart, int tickEnd) { this(tickStart, tickEnd, new ProgressViewItem[] { ProgressViewItem.ofText(), ProgressViewItem.ofSpinner(), ProgressViewItem.ofPercent() }); } + /** + * Construct view with given {@link ProgressViewItem}s using {@code tickStart 0} + * and {@code tickEnd 100}. + * + * @param items the progress view items + */ + public ProgressView(ProgressViewItem... items) { + this(0, 100, new ProgressViewItem[] { ProgressViewItem.ofText(), ProgressViewItem.ofSpinner(), + ProgressViewItem.ofPercent() }); + } + /** * Construct view with given bounds for {@code tickStart} and {@code tickEnd}. * {@code tickStart} needs to be equal or more than zero. {@code tickEnd} needs @@ -208,6 +229,9 @@ public void setSpinner(Spinner spinner) { this.spinner = spinner; } + /** + * Starts a runtime logic. Call to already started progress has no effect. + */ public void start() { if (running) { return; @@ -215,13 +239,62 @@ public void start() { running = true; startTime = System.currentTimeMillis(); ProgressState state = getState(); + scheduleTicks(); dispatch(ShellMessageBuilder.ofView(this, ProgressViewStartEvent.of(this, state))); } + private Disposable.Composite disposables; + private final String TAG_KEY = "ProgressView"; + private final String TAG_VALUE = UUID.randomUUID().toString(); + + private void scheduleTicks() { + if (disposables != null) { + return; + } + EventLoop eventLoop = getEventLoop(); + if (eventLoop == null) { + return; + } + Flux> ticks = Flux.interval(Duration.ofMillis(50)).map(l -> { + Message message = MessageBuilder + .withPayload(l) + .setHeader(ShellMessageHeaderAccessor.EVENT_TYPE, EventLoop.Type.USER) + .setHeader(TAG_KEY, TAG_VALUE) + .build(); + return message; + }); + Disposable ticksDisposable = ticks.subscribe(m -> { + eventLoop.dispatch(m); + }); + Disposable eventsDisposable = eventLoop.events() + .filter(m -> EventLoop.Type.USER.equals(StaticShellMessageHeaderAccessor.getEventType(m))) + .filter(m -> ObjectUtils.nullSafeEquals(m.getHeaders().get(TAG_KEY), TAG_VALUE)) + .subscribe(m -> { + requestRedraw(); + }); + disposables = Disposables.composite(); + disposables.add(eventsDisposable); + disposables.add(ticksDisposable); + } + + private void requestRedraw() { + EventLoop eventLoop = getEventLoop(); + if (eventLoop != null) { + eventLoop.dispatch(ShellMessageBuilder.ofRedraw()); + } + } + + /** + * Stops a runtime logic. + */ public void stop() { if (!running) { return; } + if (disposables != null) { + disposables.dispose(); + disposables = null; + } running = false; ProgressState state = getState(); dispatch(ShellMessageBuilder.ofView(this, ProgressViewEndEvent.of(this, state))); @@ -309,6 +382,7 @@ else if (value < tickStart) { if (changed) { ProgressState state = getState(); dispatch(ShellMessageBuilder.ofView(this, ProgressViewStateChangeEvent.of(this, state))); + requestRedraw(); } } diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/ProgressViewTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/ProgressViewTests.java index f8680f75c..95a5fdf52 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/ProgressViewTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/ProgressViewTests.java @@ -16,6 +16,7 @@ package org.springframework.shell.component.view.control; import java.time.Duration; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -27,6 +28,7 @@ import org.springframework.shell.component.view.control.ProgressView.ProgressViewItem; import org.springframework.shell.component.view.control.ProgressView.ProgressViewStartEvent; import org.springframework.shell.component.view.control.ProgressView.ProgressViewStateChangeEvent; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -40,20 +42,23 @@ class Construction { @Test void constructDefault() { view = new ProgressView(); + assertThat(getViewItems(view)).hasSize(3); assertThat(view.getState().tickValue()).isEqualTo(0); } @Test void constructBounds() { view = new ProgressView(10, 30); + assertThat(getViewItems(view)).hasSize(3); assertThat(view.getState().tickValue()).isEqualTo(10); assertThat(view.getState().tickStart()).isEqualTo(10); assertThat(view.getState().tickEnd()).isEqualTo(30); } - void xxx() { + @Test + void constructJustText() { view = new ProgressView(10, 30, ProgressViewItem.ofText()); - ProgressViewItem.ofText(); + assertThat(getViewItems(view)).hasSize(1); } } @@ -179,4 +184,10 @@ void hasBorder() { } + private static List getViewItems(ProgressView view) { + + @SuppressWarnings("unchecked") + List items = (List) ReflectionTestUtils.getField(view, "items"); + return items; + } } diff --git a/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentUiCommands.java b/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentUiCommands.java index eedd25f92..1722a5181 100644 --- a/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentUiCommands.java +++ b/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentUiCommands.java @@ -15,25 +15,17 @@ */ package org.springframework.shell.samples.standard; -import java.time.Duration; +import java.util.ArrayList; -import reactor.core.publisher.Flux; - -import org.springframework.messaging.Message; -import org.springframework.messaging.support.MessageBuilder; import org.springframework.shell.command.annotation.Command; +import org.springframework.shell.command.annotation.Option; import org.springframework.shell.component.ViewComponent; import org.springframework.shell.component.ViewComponent.ViewComponentRun; -import org.springframework.shell.component.message.ShellMessageBuilder; -import org.springframework.shell.component.message.ShellMessageHeaderAccessor; -import org.springframework.shell.component.message.StaticShellMessageHeaderAccessor; import org.springframework.shell.component.view.TerminalUI; import org.springframework.shell.component.view.control.BoxView; import org.springframework.shell.component.view.control.InputView; import org.springframework.shell.component.view.control.ProgressView; import org.springframework.shell.component.view.control.ProgressView.ProgressViewItem; -import org.springframework.shell.component.view.control.Spinner; -import org.springframework.shell.component.view.event.EventLoop; import org.springframework.shell.geom.HorizontalAlign; import org.springframework.shell.geom.VerticalAlign; import org.springframework.shell.standard.AbstractShellComponent; @@ -97,131 +89,91 @@ public String stringInput() { return String.format("Input was '%s'", input); } - private void runProgress(ProgressView view) { - ViewComponent component = getViewComponentBuilder().build(view); - - EventLoop eventLoop = component.getEventLoop(); - - Flux> ticks = Flux.interval(Duration.ofMillis(100)).map(l -> { - Message message = MessageBuilder - .withPayload(l) - .setHeader(ShellMessageHeaderAccessor.EVENT_TYPE, EventLoop.Type.USER) - .build(); - return message; - }); - eventLoop.dispatch(ticks); - - eventLoop.onDestroy(eventLoop.events() - .filter(m -> EventLoop.Type.USER.equals(StaticShellMessageHeaderAccessor.getEventType(m))) - .subscribe(m -> { - if (m.getPayload() instanceof Long) { - view.tickAdvance(5); - eventLoop.dispatch(ShellMessageBuilder.ofRedraw()); - } - })); - - component.runAsync().await(); - } - - @Command(command = "componentui progress1") - public void progress1() { - ProgressView view = new ProgressView(); - view.setDescription("name"); - view.setRect(0, 0, 20, 1); - view.start(); - - runProgress(view); - } - - @Command(command = "componentui progress2") - public void progress2() { - ProgressView view = new ProgressView(0, 100, - ProgressViewItem.ofText(10, HorizontalAlign.LEFT), - ProgressViewItem.ofSpinner(3, HorizontalAlign.LEFT), - ProgressViewItem.ofPercent(0, HorizontalAlign.RIGHT)); - view.setDescription("name"); - view.setRect(0, 0, 20, 1); - view.start(); - - runProgress(view); - } - - @Command(command = "componentui progress3") - public void progress3() { - ProgressView view = new ProgressView(); - view.setDescription("name"); - view.setRect(0, 0, 20, 1); - view.setSpinner(Spinner.of(Spinner.DOTS1, 80)); - view.start(); - - runProgress(view); - } - - @Command(command = "componentui progress4") - public void progress4() { - ProgressView view = new ProgressView(); - view.setDescription("name"); - view.setRect(0, 0, 20, 1); - view.setThemeResolver(getThemeResolver()); - view.start(); - - runProgress(view); - } - - @Command(command = "componentui progress5") - public void progress5() { - ProgressView view = new ProgressView(0, 100, - ProgressViewItem.ofText(10, HorizontalAlign.LEFT), - ProgressViewItem.ofSpinner(3, HorizontalAlign.LEFT), - ProgressViewItem.ofPercent(0, HorizontalAlign.RIGHT)); - - view.setDescription("name"); + @Command(command = "componentui progress") + public void progress( + @Option(defaultValue = "desc") String description, + @Option(defaultValue = "true") boolean textEnabled, + @Option(defaultValue = "true") boolean spinnerEnabled, + @Option(defaultValue = "true") boolean percentEnabled, + @Option(defaultValue = "0") int textSize, + @Option(defaultValue = "0") int spinnerSize, + @Option(defaultValue = "0") int percentSize, + @Option(defaultValue = "CENTER") HorizontalAlign textAlign, + @Option(defaultValue = "CENTER") HorizontalAlign spinnerAlign, + @Option(defaultValue = "CENTER") HorizontalAlign percentAlign, + @Option(defaultValue = "-1") int logMessagesRate, + @Option(defaultValue = "200") long advanceSleep, + @Option(defaultValue = "false") boolean logMessagesSleep + ) { + ArrayList items = new ArrayList<>(); + if (textEnabled) { + items.add(ProgressViewItem.ofText(textSize, textAlign)); + } + if (spinnerEnabled) { + items.add(ProgressViewItem.ofSpinner(spinnerSize, spinnerAlign)); + } + if (percentEnabled) { + items.add(ProgressViewItem.ofPercent(percentSize, percentAlign)); + } + ProgressViewItem[] itemsArray = items.toArray(new ProgressViewItem[0]); + ProgressView view = new ProgressView(itemsArray); + view.setDescription(description); view.setRect(0, 0, 20, 1); - view.start(); ViewComponent component = getViewComponentBuilder().build(view); - component.setUseTerminalWidth(false); - - Flux> ticks = Flux.interval(Duration.ofMillis(100)).map(l -> { - Message message = MessageBuilder - .withPayload(l) - .setHeader(ShellMessageHeaderAccessor.EVENT_TYPE, EventLoop.Type.USER) - .build(); - return message; - }); - EventLoop eventLoop = component.getEventLoop(); - eventLoop.dispatch(ticks); - eventLoop.onDestroy(eventLoop.events() - .filter(m -> EventLoop.Type.USER.equals(StaticShellMessageHeaderAccessor.getEventType(m))) - .subscribe(m -> { - if (m.getPayload() instanceof Long) { - view.tickAdvance(5); - eventLoop.dispatch(ShellMessageBuilder.ofRedraw()); - } - })); + view.start(); ViewComponentRun run = component.runAsync(); - for (int i = 0; i < 4; i++) { - + for (int i = 0; i < 51; i++) { if (run.isDone()) { break; } - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - } + sleep(advanceSleep); if (run.isDone()) { break; } + if (logMessagesRate > 0 && (i % logMessagesRate) == 0) { + int width = getTerminal().getWidth(); + String msg = String.format("%-" + width + "s", i); + getTerminal().writer().write(msg + System.lineSeparator()); + getTerminal().writer().flush(); + } + view.tickAdvance(1); + } - String msg = String.format("%s ", i); - getTerminal().writer().write(msg + System.lineSeparator()); - getTerminal().writer().flush(); + if (logMessagesSleep) { + view.stop(); + sleep(2000); + view.start(); + } + for (int i = 51; i < 101; i++) { + if (run.isDone()) { + break; + } + sleep(advanceSleep); + if (run.isDone()) { + break; + } + if (logMessagesRate > 0 && (i % logMessagesRate) == 0) { + int width = getTerminal().getWidth(); + String msg = String.format("%-" + width + "s", i); + getTerminal().writer().write(msg + System.lineSeparator()); + getTerminal().writer().flush(); + } + view.tickAdvance(1); } + view.stop(); run.cancel(); } + private static void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + } + } + }