Initial terminal ui implementation

- This commit adds proof of concept work for terminal ui as is.
- Some things work, some don't but we need to start from somewhere.
  Further development continues in a main.
- Essentially we are starting to have enough so that it merits to
  move all this work into a main repo.
- Everything new is kept under org.springframework.shell.component.view
  and will get revisiter later to find correct locations for some classes.
- Catalog sample has been modified to provide "showcase" app for
  terminal ui features. This is a start while it already contains
  some usefull scenarios.
- Relates #800
- Relates #801
- Relates #802
- Relates #803
- Relates #804
- Relates #805
- Relates #806
- Relates #807
- Relates #808
- Relates #809
- Relates #810
- Relates #811
This commit is contained in:
Janne Valkealahti
2023-07-15 16:42:39 +01:00
parent da04202822
commit a0bdbfc033
94 changed files with 9990 additions and 8 deletions

View File

@@ -0,0 +1,252 @@
/*
* Copyright 2023 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.samples.catalog;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.jline.terminal.Terminal;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.messaging.Message;
import org.springframework.shell.component.view.TerminalUI;
import org.springframework.shell.component.view.control.AppView;
import org.springframework.shell.component.view.control.AppView.AppViewEvent;
import org.springframework.shell.component.view.control.GridView;
import org.springframework.shell.component.view.control.ListView;
import org.springframework.shell.component.view.control.ListView.ListViewOpenSelectedItemEvent;
import org.springframework.shell.component.view.control.ListView.ListViewSelectedItemChangedEvent;
import org.springframework.shell.component.view.control.MenuBarView;
import org.springframework.shell.component.view.control.MenuBarView.MenuBarItem;
import org.springframework.shell.component.view.control.MenuView.MenuItem;
import org.springframework.shell.component.view.control.MenuView.MenuItemCheckStyle;
import org.springframework.shell.component.view.control.StatusBarView;
import org.springframework.shell.component.view.control.StatusBarView.StatusItem;
import org.springframework.shell.component.view.control.View;
import org.springframework.shell.component.view.control.cell.ListCell;
import org.springframework.shell.component.view.event.EventLoop;
import org.springframework.shell.component.view.event.KeyEvent.Key;
import org.springframework.shell.component.view.geom.Rectangle;
import org.springframework.shell.component.view.message.ShellMessageBuilder;
import org.springframework.shell.component.view.screen.Screen;
import org.springframework.shell.component.view.screen.Screen.Writer;
import org.springframework.shell.samples.catalog.scenario.Scenario;
import org.springframework.shell.samples.catalog.scenario.ScenarioComponent;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Catalog app logic. Builds a simple application ui where scenarios can be
* selected and run.
*
* @author Janne Valkealahti
*/
public class Catalog {
// ref types helping with deep nested generics from events
private final static ParameterizedTypeReference<ListViewOpenSelectedItemEvent<ScenarioData>> LISTVIEW_SCENARIO_TYPEREF
= new ParameterizedTypeReference<ListViewOpenSelectedItemEvent<ScenarioData>>() {};
private final static ParameterizedTypeReference<ListViewSelectedItemChangedEvent<String>> LISTVIEW_STRING_TYPEREF
= new ParameterizedTypeReference<ListViewSelectedItemChangedEvent<String>>() {};
// mapping from category name to scenarios(can belong to multiple categories)
private final Map<String, List<ScenarioData>> categoryMap = new TreeMap<>();
private final Terminal terminal;
private View currentScenarioView = null;
private TerminalUI ui;
private ListView<String> categories;
private EventLoop eventLoop;
public Catalog(Terminal terminal, List<Scenario> scenarios) {
this.terminal = terminal;
mapScenarios(scenarios);
}
private void mapScenarios(List<Scenario> scenarios) {
// we blindly expect scenario to have ScenarioComponent annotation with all fields
scenarios.forEach(sce -> {
ScenarioComponent ann = AnnotationUtils.findAnnotation(sce.getClass(), ScenarioComponent.class);
if (ann != null) {
String name = ann.name();
String description = ann.description();
String[] category = ann.category();
if (StringUtils.hasText(name) && StringUtils.hasText(description) && !ObjectUtils.isEmpty(category)) {
for (String cat : category) {
ScenarioData scenarioData = new ScenarioData(sce, name, description, category);
categoryMap.computeIfAbsent(Scenario.CATEGORY_ALL, key -> new ArrayList<>()).add(scenarioData);
categoryMap.computeIfAbsent(cat, key -> new ArrayList<>()).add(scenarioData);
}
}
}
});
}
private void requestQuit() {
Message<String> msg = ShellMessageBuilder.withPayload("int")
.setEventType(EventLoop.Type.SYSTEM)
.setPriority(0)
.build();
eventLoop.dispatch(msg);
}
/**
* Main run loop. Builds the ui and exits when user requests exit.
*/
public void run() {
ui = new TerminalUI(terminal);
eventLoop = ui.getEventLoop();
AppView app = buildScenarioBrowser(eventLoop, ui);
// handle logic to switch between main scenario browser
// and currently active scenario
eventLoop.onDestroy(eventLoop.keyEvents()
.doOnNext(m -> {
if (m.getPlainKey() == Key.q && m.hasCtrl()) {
if (currentScenarioView != null) {
currentScenarioView = null;
ui.setRoot(app, true);
}
else {
requestQuit();
}
}
})
.subscribe());
// start main scenario browser
ui.setRoot(app, true);
ui.setFocus(categories);
categories.setSelected(0);
ui.run();
}
private AppView buildScenarioBrowser(EventLoop eventLoop, TerminalUI component) {
// we use main app view to represent scenario browser
AppView app = new AppView();
app.setEventLoop(eventLoop);
// category selector on left, scenario selector on right
GridView grid = new GridView();
grid.setRowSize(1, 0, 1);
grid.setColumnSize(30, 0);
categories = buildCategorySelector(eventLoop);
ListView<ScenarioData> scenarios = buildScenarioSelector(eventLoop);
// handle event when scenario is chosen
eventLoop.onDestroy(eventLoop.viewEvents(LISTVIEW_SCENARIO_TYPEREF, scenarios)
.subscribe(event -> {
View view = event.args().item().scenario().configure(eventLoop).build();
component.setRoot(view, true);
currentScenarioView = view;
}));
// handle event when category selection is changed
eventLoop.onDestroy(eventLoop.viewEvents(LISTVIEW_STRING_TYPEREF, categories)
.subscribe(event -> {
if (event.args().item() != null) {
String selected = event.args().item();
List<ScenarioData> list = categoryMap.get(selected);
scenarios.setItems(list);
}
}));
// handle focus change between lists
eventLoop.onDestroy(eventLoop.viewEvents(AppViewEvent.class, app)
.subscribe(event -> {
switch (event.args().direction()) {
case NEXT -> ui.setFocus(scenarios);
case PREVIOUS -> ui.setFocus(categories);
}
}
));
// We place statusbar below categories and scenarios
MenuBarView menuBar = buildMenuBar(eventLoop);
StatusBarView statusBar = buildStatusBar(eventLoop);
grid.addItem(menuBar, 0, 0, 1, 2, 0, 0);
grid.addItem(categories, 1, 0, 1, 1, 0, 0);
grid.addItem(scenarios, 1, 1, 1, 1, 0, 0);
grid.addItem(statusBar, 2, 0, 1, 2, 0, 0);
app.setMain(grid);
return app;
}
private ListView<String> buildCategorySelector(EventLoop eventLoop) {
ListView<String> categories = new ListView<>();
categories.setEventLoop(eventLoop);
List<String> items = List.copyOf(categoryMap.keySet());
categories.setItems(items);
categories.setTitle("Categories");
categories.setShowBorder(true);
return categories;
}
private static class ScenarioListCell extends ListCell<ScenarioData> {
@Override
public void draw(Screen screen) {
Rectangle rect = getRect();
Writer writer = screen.writerBuilder().style(getStyle()).build();
writer.text(String.format("%-20s %s", getItem().name(), getItem().description()), rect.x(), rect.y());
writer.background(rect, getBackgroundColor());
}
}
private ListView<ScenarioData> buildScenarioSelector(EventLoop eventLoop) {
ListView<ScenarioData> scenarios = new ListView<>();
scenarios.setEventLoop(eventLoop);
scenarios.setTitle("Scenarios");
scenarios.setShowBorder(true);
scenarios.setCellFactory(list -> new ScenarioListCell());
return scenarios;
}
private MenuBarView buildMenuBar(EventLoop eventLoop) {
Runnable quitAction = () -> requestQuit();
MenuBarView menuBar = MenuBarView.of(
MenuBarItem.of("File",
MenuItem.of("Quit", MenuItemCheckStyle.NOCHECK, quitAction)),
MenuBarItem.of("Theme",
MenuItem.of("Dump", MenuItemCheckStyle.RADIO),
MenuItem.of("Funky", MenuItemCheckStyle.RADIO)
),
MenuBarItem.of("Help",
MenuItem.of("About"))
);
menuBar.setEventLoop(eventLoop);
return menuBar;
}
private StatusBarView buildStatusBar(EventLoop eventLoop) {
Runnable quitAction = () -> requestQuit();
StatusBarView statusBar = new StatusBarView();
statusBar.setEventLoop(eventLoop);
StatusItem item1 = new StatusBarView.StatusItem("CTRL-Q Quit", quitAction);
StatusItem item2 = new StatusBarView.StatusItem("F10 Status Bar");
statusBar.setItems(Arrays.asList(item1, item2));
return statusBar;
}
private record ScenarioData(Scenario scenario, String name, String description, String[] category){};
}

View File

@@ -15,16 +15,29 @@
*/
package org.springframework.shell.samples.catalog;
import java.util.List;
import org.springframework.shell.command.annotation.Command;
import org.springframework.shell.command.annotation.Option;
import org.springframework.shell.samples.catalog.scenario.Scenario;
import org.springframework.shell.standard.AbstractShellComponent;
/**
* Main command access point to view showcase catalog.
*
* @author Janne Valkealahti
*/
@Command
public class CatalogCommand {
public class CatalogCommand extends AbstractShellComponent {
@Command
String catalog(
@Option() String arg
) {
return String.format("Hi arg=%s", arg);
private final List<Scenario> scenarios;
public CatalogCommand(List<Scenario> scenarios) {
this.scenarios = scenarios;
}
@Command(command = "catalog")
public void catalog() {
Catalog catalog = new Catalog(getTerminal(), scenarios);
catalog.run();
}
}

View File

@@ -18,8 +18,10 @@ package org.springframework.shell.samples.catalog;
import org.springframework.boot.Banner.Mode;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.shell.command.annotation.CommandScan;
@SpringBootApplication
@CommandScan
public class SpringShellApplication {
public static void main(String[] args) throws Exception {

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2023 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.samples.catalog.scenario;
import org.springframework.shell.component.view.event.EventLoop;
/**
* Base implementation of a {@link Scenario} helping to avoid some bloatware.
*
* @author Janne Valkealahti
*/
public abstract class AbstractScenario implements Scenario {
private EventLoop eventloop;
@Override
public Scenario configure(EventLoop eventloop) {
this.eventloop = eventloop;
return this;
}
protected EventLoop getEventloop() {
return eventloop;
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2023 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.samples.catalog.scenario;
import org.springframework.shell.component.view.control.View;
import org.springframework.shell.component.view.event.EventLoop;
/**
* {@link Scenario} participates in a catalog showcase.
*
* @author Janne Valkealahti
*/
public interface Scenario {
// Common category names
public static final String CATEGORY_ALL = "All Scenarios";
public static final String CATEGORY_LISTVIEW = "ListView";
public static final String CATEGORY_BOXVIEW = "BoxView";
public static final String CATEGORY_LAYOUT = "Layout";
public static final String CATEGORY_OTHER = "Other";
/**
* Build a {@link View} to be shown with a scenario.
*
* @return view of a scenario
*/
View build();
/**
* Configure scenario.
*
* @param eventloop eventloop for scenario
* @return scenario for chaining
*/
Scenario configure(EventLoop eventloop);
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2023 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.samples.catalog.scenario;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Indexed;
/**
* Annotation needed for a scenarios to get hooked up into a catalog app.
* Typically all fields in this annotation needs to have content to get attached
* into a catalog app.
*
* @author Janne Valkealahti
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
@Component
public @interface ScenarioComponent {
/**
* Define a name of a scenario.
*
* @return name of a scenario
*/
String name() default "";
/**
* Define a short description of a scenario.
*
* @return short description of a scenario
*/
String description() default "";
/**
* Define a categories of a scenario.
*
* @return categories of a scenario
*/
String[] category() default {};
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2023 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.samples.catalog.scenario.box;
import org.springframework.shell.component.view.control.BoxView;
import org.springframework.shell.component.view.control.View;
import org.springframework.shell.component.view.geom.HorizontalAlign;
import org.springframework.shell.component.view.geom.VerticalAlign;
import org.springframework.shell.samples.catalog.scenario.AbstractScenario;
import org.springframework.shell.samples.catalog.scenario.Scenario;
import org.springframework.shell.samples.catalog.scenario.ScenarioComponent;
@ScenarioComponent(name = "Draw Function", description = "BoxView with DrawFunction", category = {
Scenario.CATEGORY_BOXVIEW })
public class DrawFunctionScenario extends AbstractScenario {
@Override
public View build() {
BoxView view = new BoxView();
view.setDrawFunction((screen, rect) -> {
screen.writerBuilder().build().text("Hello World", rect, HorizontalAlign.CENTER, VerticalAlign.CENTER);
return rect;
});
return view;
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2023 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.samples.catalog.scenario.box;
import org.springframework.shell.component.view.control.BoxView;
import org.springframework.shell.component.view.control.View;
import org.springframework.shell.component.view.geom.HorizontalAlign;
import org.springframework.shell.component.view.screen.Color;
import org.springframework.shell.component.view.screen.ScreenItem;
import org.springframework.shell.samples.catalog.scenario.AbstractScenario;
import org.springframework.shell.samples.catalog.scenario.Scenario;
import org.springframework.shell.samples.catalog.scenario.ScenarioComponent;
@ScenarioComponent(name = "Simple boxview", description = "BoxView with color and style", category = {
Scenario.CATEGORY_BOXVIEW })
public class SimpleBoxViewScenario extends AbstractScenario {
@Override
public View build() {
BoxView box = new BoxView();
box.setTitle("Title");
box.setShowBorder(true);
box.setBackgroundColor(Color.KHAKI4);
box.setTitleColor(Color.RED);
box.setTitleStyle(ScreenItem.STYLE_BOLD | ScreenItem.STYLE_ITALIC);
box.setTitleAlign(HorizontalAlign.CENTER);
return box;
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2023 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.samples.catalog.scenario.grid;
import org.springframework.shell.component.view.control.BoxView;
import org.springframework.shell.component.view.control.GridView;
import org.springframework.shell.component.view.control.View;
import org.springframework.shell.component.view.screen.Color;
import org.springframework.shell.samples.catalog.scenario.AbstractScenario;
import org.springframework.shell.samples.catalog.scenario.Scenario;
import org.springframework.shell.samples.catalog.scenario.ScenarioComponent;
@ScenarioComponent(name = "Simple gridview", description = "GridView sample", category = { Scenario.CATEGORY_LAYOUT })
public class SimpleGridViewScenario extends AbstractScenario {
@Override
public View build() {
BoxView menu = new BoxView();
menu.setBackgroundColor(Color.KHAKI4);
menu.setTitle("Menu");
menu.setShowBorder(true);
BoxView main = new BoxView();
main.setBackgroundColor(Color.KHAKI4);
main.setTitle("Main");
main.setShowBorder(true);
BoxView sideBar = new BoxView();
sideBar.setBackgroundColor(Color.KHAKI4);
sideBar.setTitle("Sidebar");
sideBar.setShowBorder(true);
BoxView header = new BoxView();
header.setBackgroundColor(Color.KHAKI4);
header.setTitle("Header");
header.setShowBorder(true);
BoxView footer = new BoxView();
footer.setBackgroundColor(Color.KHAKI4);
footer.setTitle("Footer");
footer.setShowBorder(true);
GridView grid = new GridView();
grid.setBackgroundColor(Color.KHAKI3);
grid.setTitle("Grid");
grid.setShowBorder(true);
grid.setRowSize(3, 0, 3);
grid.setColumnSize(30, 0, 30);
// grid.setShowBorder(true);
grid.setShowBorders(true);
grid.addItem(header, 0, 0, 1, 3, 0, 0);
grid.addItem(footer, 2, 0, 1, 3, 0, 0);
grid.addItem(menu, 0, 0, 0, 0, 0, 0);
grid.addItem(main, 1, 0, 1, 3, 0, 0);
grid.addItem(sideBar, 0, 0, 0, 0, 0, 0);
grid.addItem(menu, 1, 0, 1, 1, 0, 100);
grid.addItem(main, 1, 1, 1, 1, 0, 100);
grid.addItem(sideBar, 1, 2, 1, 1, 0, 100);
return grid;
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2023 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.samples.catalog.scenario.listview;
import java.util.Arrays;
import org.springframework.shell.component.view.control.ListView;
import org.springframework.shell.component.view.control.View;
import org.springframework.shell.samples.catalog.scenario.AbstractScenario;
import org.springframework.shell.samples.catalog.scenario.Scenario;
import org.springframework.shell.samples.catalog.scenario.ScenarioComponent;
@ScenarioComponent(name = "Basic", description = "Basic list", category = { Scenario.CATEGORY_LISTVIEW })
public class SimpleListViewScenario extends AbstractScenario {
@Override
public View build() {
ListView<String> view = new ListView<>();
view.setEventLoop(getEventloop());
view.setItems(Arrays.asList("item1", "item2"));
return view;
}
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright 2023 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.samples.catalog.scenario.other;
import java.time.Duration;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import reactor.core.publisher.Flux;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.shell.component.view.control.BoxView;
import org.springframework.shell.component.view.control.View;
import org.springframework.shell.component.view.event.EventLoop;
import org.springframework.shell.component.view.event.KeyEvent.Key;
import org.springframework.shell.component.view.geom.HorizontalAlign;
import org.springframework.shell.component.view.geom.Rectangle;
import org.springframework.shell.component.view.geom.VerticalAlign;
import org.springframework.shell.component.view.message.ShellMessageBuilder;
import org.springframework.shell.component.view.message.ShellMessageHeaderAccessor;
import org.springframework.shell.component.view.message.StaticShellMessageHeaderAccessor;
import org.springframework.shell.samples.catalog.scenario.AbstractScenario;
import org.springframework.shell.samples.catalog.scenario.Scenario;
import org.springframework.shell.samples.catalog.scenario.ScenarioComponent;
@ScenarioComponent(name = "Clock", description = "Showing time ticks", category = { Scenario.CATEGORY_OTHER })
public class ClockScenario extends AbstractScenario {
@Override
public View build() {
// simply use a plain box view to draw a date using a custom
// draw function
BoxView root = new BoxView();
root.setTitle("What's o'clock");
root.setShowBorder(true);
// store text to print
AtomicReference<String> ref = new AtomicReference<>();
// dispatch dates as messages
Flux<Message<?>> dates = Flux.interval(Duration.ofSeconds(1)).map(l -> {
String date = new Date().toString();
Message<String> message = MessageBuilder
.withPayload(date)
.setHeader(ShellMessageHeaderAccessor.EVENT_TYPE, EventLoop.Type.USER)
.build();
return message;
});
getEventloop().dispatch(dates);
// process dates
getEventloop().onDestroy(getEventloop().events()
.filter(m -> EventLoop.Type.USER.equals(StaticShellMessageHeaderAccessor.getEventType(m)))
.subscribe(m -> {
if (m.getPayload() instanceof String s) {
ref.set(s);
getEventloop().dispatch(ShellMessageBuilder.ofRedraw());
}
}));
// testing for animations for now
AtomicInteger animX = new AtomicInteger();
getEventloop().onDestroy(getEventloop().events()
.filter(m -> EventLoop.Type.SYSTEM.equals(StaticShellMessageHeaderAccessor.getEventType(m)))
.filter(m -> m.getHeaders().containsKey("animationtick"))
.subscribe(m -> {
Object payload = m.getPayload();
if (payload instanceof Integer i) {
animX.set(i);
getEventloop().dispatch(ShellMessageBuilder.ofRedraw());
}
}));
AtomicReference<HorizontalAlign> hAlign = new AtomicReference<>(HorizontalAlign.CENTER);
AtomicReference<VerticalAlign> vAlign = new AtomicReference<>(VerticalAlign.CENTER);
getEventloop().onDestroy(getEventloop().keyEvents()
.subscribe(event -> {
switch (event.key()) {
case Key.CursorDown -> {
if (vAlign.get() == VerticalAlign.TOP) {
vAlign.set(VerticalAlign.CENTER);
}
else if (vAlign.get() == VerticalAlign.CENTER) {
vAlign.set(VerticalAlign.BOTTOM);
}
}
case Key.CursorUp -> {
if (vAlign.get() == VerticalAlign.BOTTOM) {
vAlign.set(VerticalAlign.CENTER);
}
else if (vAlign.get() == VerticalAlign.CENTER) {
vAlign.set(VerticalAlign.TOP);
}
}
case Key.CursorLeft -> {
if (hAlign.get() == HorizontalAlign.RIGHT) {
hAlign.set(HorizontalAlign.CENTER);
}
else if (hAlign.get() == HorizontalAlign.CENTER) {
hAlign.set(HorizontalAlign.LEFT);
}
}
case Key.CursorRight -> {
Message<String> animStart = MessageBuilder
.withPayload("")
.setHeader(ShellMessageHeaderAccessor.EVENT_TYPE, EventLoop.Type.SYSTEM)
.setHeader("animationstart", true)
.build();
getEventloop().dispatch(animStart);
}
};
}));
// draw current date
root.setDrawFunction((screen, rect) -> {
int a = animX.get();
Rectangle r = new Rectangle(rect.x() + 1 + a, rect.y() + 1, rect.width() - 2, rect.height() - 2);
// Rectangle r = new View.Rectangle(rect.x() + 1, rect.y() + 1, rect.width() - 2, rect.height() - 2);
String s = ref.get();
if (s != null) {
screen.writerBuilder().build().text(s, r, hAlign.get(), vAlign.get());
}
return rect;
});
return root;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2023 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.samples.catalog.scenario.other;
import org.springframework.shell.component.view.control.InputView;
import org.springframework.shell.component.view.control.View;
import org.springframework.shell.samples.catalog.scenario.AbstractScenario;
import org.springframework.shell.samples.catalog.scenario.Scenario;
import org.springframework.shell.samples.catalog.scenario.ScenarioComponent;;
@ScenarioComponent(name = "Simple inputview", description = "InputView sample", category = { Scenario.CATEGORY_OTHER })
public class SimpleInputViewScenario extends AbstractScenario {
@Override
public View build() {
InputView view = new InputView();
view.setEventLoop(getEventloop());
view.setTitle("Input");
view.setShowBorder(true);
return view;
}
}

View File

@@ -0,0 +1,272 @@
/*
* Copyright 2023 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.samples.catalog.scenario.other;
import java.time.Duration;
import java.util.LinkedList;
import reactor.core.publisher.Flux;
import org.springframework.shell.component.view.control.BoxView;
import org.springframework.shell.component.view.control.View;
import org.springframework.shell.component.view.event.KeyEvent.Key;
import org.springframework.shell.component.view.message.ShellMessageBuilder;
import org.springframework.shell.component.view.screen.Screen;
import org.springframework.shell.component.view.screen.Screen.Writer;
import org.springframework.shell.samples.catalog.scenario.AbstractScenario;
import org.springframework.shell.samples.catalog.scenario.Scenario;
import org.springframework.shell.samples.catalog.scenario.ScenarioComponent;
/**
* Scenario implementing a classic snake game.
*
* Demonstrates how we can just use box view to draw something
* manually with its draw function.
*
* Game logic.
* 1. Snake starts in a center, initial direction needs arrow key
* 2. Arrows control snake direction
* 3. Eating a food crows a snake, new food is generated
* 4. Game ends if snake eats itself or goes out of bounds
* 5. Game ends if perfect score is established
*
* @author Janne Valkealahti
*/
@ScenarioComponent(name = "Snake", description = "Classic snake game", category = { Scenario.CATEGORY_OTHER })
public class SnakeGameScenario extends AbstractScenario {
@Override
public View build() {
SnakeGame snakeGame = new SnakeGame(10, 15);
BoxView view = new BoxView();
view.setTitle("Snake");
view.setShowBorder(true);
// we're outside of a view so no bindings,
// just subscribe to events and handle what is needed.
getEventloop().onDestroy(getEventloop().keyEvents()
.subscribe(event -> {
Integer direction = switch (event.key()) {
case Key.CursorDown -> 1;
case Key.CursorUp -> -1;
case Key.CursorLeft -> -2;
case Key.CursorRight -> 2;
default -> 0;
};
if (direction != null) {
snakeGame.update(direction);
}
}));
// schedule game updates
getEventloop().onDestroy(Flux.interval(Duration.ofMillis(500))
.subscribe(l -> {
snakeGame.update(0);
getEventloop().dispatch(ShellMessageBuilder.ofRedraw());
}
));
// draw game area
view.setDrawFunction((screen, rect) -> {
snakeGame.draw(screen);
return rect;
});
return view;
}
private static class SnakeGame {
Board board;
Game game;
SnakeGame(int rows, int cols) {
// snake starts from a center
Cell initial = new Cell(rows / 2, cols / 2, 1);
Snake snake = new Snake(initial);
board = new Board(rows, cols, initial);
game = new Game(snake, board);
}
void update(int direction) {
if (direction != 0) {
game.direction = direction;
}
game.update();
}
void draw(Screen screen) {
Cell[][] cells = board.cells;
Writer writer = screen.writerBuilder().build();
// draw game area border
writer.border(2, 2, board.cols + 2, board.rows + 2);
// draw info
String info = game.gameOver ? "Game Over" : String.format("Points %s", game.points);
writer.text(info, 2, 1);
// draw snake and food
for (int i = 0; i < cells.length; i++) {
for (int j = 0; j < cells[i].length; j++) {
Cell cell = cells[i][j];
String c = "";
if (cell.type == 1) {
c = "x";
}
else if (cell.type == -1) {
c = "o";
}
writer.text(c, j + 3, i + 3);
}
}
}
}
private static class Cell {
final int row, col;
// 0 - empty, > 0 - snake, < 0 - food
int type;
Cell(int row, int col, int type) {
this.row = row;
this.col = col;
this.type = type;
}
}
private static class Board {
final int rows, cols;
Cell[][] cells;
Board(int rows, int cols, Cell initial) {
this.rows = rows;
this.cols = cols;
cells = new Cell[rows][cols];
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
cells[row][col] = new Cell(row, col, 0);
}
}
cells[initial.row][initial.col] = initial;
food();
}
void food() {
int row = 0, column = 0;
while (true) {
row = (int) (Math.random() * rows);
column = (int) (Math.random() * cols);
if (cells[row][column].type != 1)
break;
}
cells[row][column].type = -1;
}
}
private static class Snake {
LinkedList<Cell> cells = new LinkedList<>();
Cell head;
Snake(Cell cell) {
head = cell;
cells.add(head);
head.type = 1;
}
void move(Cell cell, boolean grow) {
if (!grow) {
Cell tail = cells.removeLast();
tail.type = 0;
}
head = cell;
head.type = 1;
cells.addFirst(head);
}
boolean checkCrash(Cell next) {
for (Cell cell : cells) {
if (cell == next) {
return true;
}
}
return false;
}
}
private static class Game {
Snake snake;
Board board;
int direction;
int points;
boolean gameOver;
Game(Snake snake, Board board) {
this.snake = snake;
this.board = board;
this.direction = 0;
}
void update() {
if (direction == 0) {
return;
}
Cell next = next(snake.head);
if (next == null || snake.checkCrash(next)) {
direction = 0;
gameOver = true;
}
else {
boolean foundFood = next.type == -1;
snake.move(next, foundFood);
if (foundFood) {
board.food();
points++;
}
}
}
Cell next(Cell cell) {
int row = cell.row;
int col = cell.col;
// return null if we're about to go out of bounds
if (direction == 2) {
col++;
if (col >= board.cols) {
return null;
}
}
else if (direction == -2) {
col--;
if (col < 0) {
return null;
}
}
else if (direction == 1) {
row++;
if (row >= board.rows) {
return null;
}
}
else if (direction == -1) {
row--;
if (row < 0) {
return null;
}
}
return board.cells[row][col];
}
}
}