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 16f31683..e1a43a27 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 @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2024 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. @@ -15,6 +15,7 @@ */ package org.springframework.shell.component; +import org.jline.terminal.Size; import org.jline.terminal.Terminal; import org.springframework.shell.component.message.ShellMessageBuilder; @@ -22,6 +23,7 @@ import org.springframework.shell.component.view.TerminalUI; import org.springframework.shell.component.view.control.View; import org.springframework.shell.component.view.control.ViewDoneEvent; import org.springframework.shell.component.view.event.EventLoop; +import org.springframework.shell.geom.Rectangle; import org.springframework.util.Assert; /** @@ -34,37 +36,65 @@ public class ViewComponent { private final Terminal terminal; private final View view; private EventLoop eventLoop; + private TerminalUI ui; + private boolean useTerminalWidth = true; + /** + * Construct view component with a given {@link Terminal} and {@link View}. + * + * @param terminal the terminal + * @param view the main view + */ public ViewComponent(Terminal terminal, View view) { Assert.notNull(terminal, "terminal must be set"); Assert.notNull(view, "view must be set"); this.terminal = terminal; this.view = view; + this.ui = new TerminalUI(terminal); + this.eventLoop = ui.getEventLoop(); } /** * Run a view execution loop. */ public void run() { - TerminalUI ui = new TerminalUI(terminal); - eventLoop = ui.getEventLoop(); eventLoop.onDestroy(eventLoop.viewEvents(ViewDoneEvent.class, view) .subscribe(event -> { exit(); } )); view.setEventLoop(eventLoop); + Size terminalSize = terminal.getSize(); + Rectangle rect = view.getRect(); + if (useTerminalWidth) { + view.setRect(rect.x(), rect.y(), terminalSize.getColumns() - rect.x(), rect.height()); + } ui.setRoot(view, false); ui.run(); } + /** + * Sets if full terminal width should be used for a view. Defaults to {@code true}. + * + * @param useTerminalWidth the use terminal width flag + */ + public void setUseTerminalWidth(boolean useTerminalWidth) { + this.useTerminalWidth = useTerminalWidth; + } + + /** + * Gets an {@link EventLoop} associated with this view component. + * + * @return event loop with this view component + */ + public EventLoop getEventLoop() { + return eventLoop; + } + /** * Request exit from an execution loop. */ public void exit() { - if (eventLoop == null) { - return; - } eventLoop.dispatch(ShellMessageBuilder.ofInterrupt()); } 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 4086b127..0f92ec65 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 @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2024 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. @@ -15,11 +15,23 @@ */ package org.springframework.shell.samples.standard; +import java.time.Duration; + +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.component.ViewComponent; +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.event.EventLoop; import org.springframework.shell.geom.HorizontalAlign; import org.springframework.shell.geom.VerticalAlign; import org.springframework.shell.standard.AbstractShellComponent; @@ -83,4 +95,70 @@ public class ComponentUiCommands extends AbstractShellComponent { return String.format("Input was '%s'", input); } + @Command(command = "componentui progress1") + public void progress1() { + ProgressView view = new ProgressView(); + view.setDescription("name"); + view.setRect(0, 0, 20, 1); + + + ViewComponent component = new ViewComponent(getTerminal(), 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.run(); + } + + @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); + + + ViewComponent component = new ViewComponent(getTerminal(), 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.run(); + } + }