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:
@@ -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){};
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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 {};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user