Skip to content

Commit

Permalink
ViewComponent async execution
Browse files Browse the repository at this point in the history
- Modifies ViewComponent with changed api's so that
  it can be executed with a thread allowing caller
  not to block.
- Add ViewComponentBuilder concept and create is as
  a bean similar to TerminalUIBuilder.
- Relates #997
  • Loading branch information
jvalkeal committed Feb 10, 2024
1 parent abc4ffa commit 7fa953f
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -23,6 +23,8 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.shell.component.ViewComponentBuilder;
import org.springframework.shell.component.ViewComponentExecutor;
import org.springframework.shell.component.view.TerminalUI;
import org.springframework.shell.component.view.TerminalUIBuilder;
import org.springframework.shell.component.view.TerminalUICustomizer;
Expand All @@ -45,4 +47,18 @@ public TerminalUIBuilder terminalUIBuilder(Terminal terminal, ThemeResolver them
return builder;
}

@Bean
@Scope("prototype")
@ConditionalOnMissingBean
public ViewComponentBuilder viewComponentBuilder(TerminalUIBuilder terminalUIBuilder,
ViewComponentExecutor viewComponentExecutor, Terminal terminal) {
return new ViewComponentBuilder(terminalUIBuilder, viewComponentExecutor, terminal);
}

@Bean
@ConditionalOnMissingBean
public ViewComponentExecutor viewComponentExecutor() {
return new ViewComponentExecutor();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

import org.jline.terminal.Size;
import org.jline.terminal.Terminal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.shell.component.message.ShellMessageBuilder;
import org.springframework.shell.component.view.TerminalUI;
Expand All @@ -33,31 +35,50 @@
*/
public class ViewComponent {

private final static Logger log = LoggerFactory.getLogger(ViewComponent.class);
private final Terminal terminal;
private final View view;
private EventLoop eventLoop;
private TerminalUI ui;
private TerminalUI terminalUI;
private boolean useTerminalWidth = true;
private ViewComponentExecutor viewComponentExecutor;

/**
* 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) {
public ViewComponent(TerminalUI terminalUI, Terminal terminal, ViewComponentExecutor viewComponentExecutor,
View view) {
Assert.notNull(terminalUI, "terminal ui must be set");
Assert.notNull(terminal, "terminal must be set");
Assert.notNull(view, "view must be set");
this.terminalUI = terminalUI;
this.terminal = terminal;
this.view = view;
this.ui = new TerminalUI(terminal);
this.eventLoop = ui.getEventLoop();
this.viewComponentExecutor = viewComponentExecutor;
this.eventLoop = terminalUI.getEventLoop();
}

/**
* Run a component asyncronously. Returned state can be used to wait, cancel or
* see its completion status.
*
* @return run state
*/
public ViewComponentRun runAsync() {
ViewComponentRun run = viewComponentExecutor.start(() -> {
runBlocking();
});
return run;
}

/**
* Run a view execution loop.
*/
public void run() {
public void runBlocking() {
log.debug("Start run()");
eventLoop.onDestroy(eventLoop.viewEvents(ViewDoneEvent.class, view)
.subscribe(event -> {
exit();
Expand All @@ -69,8 +90,9 @@ public void run() {
if (useTerminalWidth) {
view.setRect(rect.x(), rect.y(), terminalSize.getColumns() - rect.x(), rect.height());
}
ui.setRoot(view, false);
ui.run();
terminalUI.setRoot(view, false);
terminalUI.run();
log.debug("End run()");
}

/**
Expand Down Expand Up @@ -98,4 +120,28 @@ public void exit() {
eventLoop.dispatch(ShellMessageBuilder.ofInterrupt());
}

/**
* Represent run state of an async run of a component.
*/
public interface ViewComponentRun {

/**
* Await component termination.
*/
void await();

/**
* Cancel component run.
*/
void cancel();

/**
* Returns {@code true} if component run has completed.
*
* @return {@code true} if component run has completed
*/
boolean isDone();

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 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.
* 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.component;

import org.jline.terminal.Terminal;

import org.springframework.shell.component.view.TerminalUIBuilder;
import org.springframework.shell.component.view.control.View;

/**
* Builder that can be used to configure and create a {@link ViewComponent}.
*
* @author Janne Valkealahti
*/
public class ViewComponentBuilder {

private final TerminalUIBuilder terminalUIBuilder;
private final ViewComponentExecutor viewComponentExecutor;
private final Terminal terminal;

public ViewComponentBuilder(TerminalUIBuilder terminalUIBuilder, ViewComponentExecutor viewComponentExecutor,
Terminal terminal) {
this.terminalUIBuilder = terminalUIBuilder;
this.viewComponentExecutor = viewComponentExecutor;
this.terminal = terminal;
}

/**
* Build a new {@link ViewComponent} instance and configure it using this builder.
*
* @param view the view to use with view component
* @return a configured {@link ViewComponent} instance.
*/
public ViewComponent build(View view) {
return new ViewComponent(terminalUIBuilder.build(), terminal, viewComponentExecutor, view);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright 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.
* 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.component;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.shell.component.ViewComponent.ViewComponentRun;

/**
* Executor for {@code ViewComponent}. Purpose of this executor is to run
* component in a thread so that it doesn't need to block from a command.
*
* @author Janne Valkealahti
*/
public class ViewComponentExecutor implements AutoCloseable {

private final Logger log = LoggerFactory.getLogger(ViewComponentExecutor.class);
private final SimpleAsyncTaskExecutor executor;
private Future<?> future;

public ViewComponentExecutor() {
this.executor = new SimpleAsyncTaskExecutor();
}

@Override
public void close() throws Exception {
this.executor.close();
}

private static class FutureViewComponentRun implements ViewComponentRun {

private Future<?> future;

private FutureViewComponentRun(Future<?> future) {
this.future = future;
}

@Override
public void await() {
try {
this.future.get();
} catch (InterruptedException e) {
} catch (ExecutionException e) {
}
}

@Override
public void cancel() {
this.future.cancel(true);
}

@Override
public boolean isDone() {
return this.future.isDone();
}

}

/**
* Execute runnable and return state which can be used for further operations.
*
* @param runnable the runnable
* @return run state
*/
public ViewComponentRun start(Runnable runnable) {
if (future != null && !future.isDone()) {
throw new IllegalStateException("Can run component as there is existing one in non stopped state");
}
future = executor.submit(() -> {
log.debug("About to run component");
runnable.run();
log.debug("Finished run component");
});
return new FutureViewComponentRun(future);
}

/**
* Stop a {@code ViewComponent} which has been previously started with this
* executor.
*/
public void stop() {
if (future != null) {
future.cancel(true);
}
future = null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.shell.command.annotation.Command;
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;
Expand Down Expand Up @@ -90,14 +91,15 @@ public void tui3() {
public String stringInput() {
InputView view = new InputView();
view.setRect(0, 0, 10, 1);
ViewComponent component = new ViewComponent(getTerminal(), view);
component.run();
ViewComponent component = getViewComponentBuilder().build(view);
component.runBlocking();
String input = view.getInputText();
return String.format("Input was '%s'", input);
}

private void runProgress(ProgressView view) {
ViewComponent component = new ViewComponent(getTerminal(), view);
ViewComponent component = getViewComponentBuilder().build(view);

EventLoop eventLoop = component.getEventLoop();

Flux<Message<?>> ticks = Flux.interval(Duration.ofMillis(100)).map(l -> {
Expand All @@ -118,7 +120,7 @@ private void runProgress(ProgressView view) {
}
}));

component.run();
component.runAsync().await();
}

@Command(command = "componentui progress1")
Expand Down Expand Up @@ -166,4 +168,60 @@ public void progress4() {
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");
view.setRect(0, 0, 20, 1);
view.start();

ViewComponent component = getViewComponentBuilder().build(view);
component.setUseTerminalWidth(false);

Flux<Message<?>> ticks = Flux.interval(Duration.ofMillis(100)).map(l -> {
Message<Long> 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());
}
}));

ViewComponentRun run = component.runAsync();

for (int i = 0; i < 4; i++) {

if (run.isDone()) {
break;
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
if (run.isDone()) {
break;
}

String msg = String.format("%s ", i);
getTerminal().writer().write(msg + System.lineSeparator());
getTerminal().writer().flush();

}

run.cancel();
}

}
Loading

0 comments on commit 7fa953f

Please sign in to comment.